How to define two separate types for arbitrary precision decimals so they can only be be used with the same type and not assigned be to each other

huangapple go评论72阅读模式
英文:

How to define two separate types for arbitrary precision decimals so they can only be be used with the same type and not assigned be to each other

问题

我的当前代码库定义了两种类型,如下所示:

type Price uint64
type Quantity uint64

这样做非常好,因为我不能意外地将Price类型传递给Quantity,否则编译器会报错。

现在,我需要将实现从uint64切换到使用shopspring/decimal库的任意精度十进制数。

我希望以下要求与先前的uint64实现相同:

  • 如果我将Price传递给期望Quantity的函数,或者反过来,编译器会报错。
  • 我可以在两个Quantity之间进行计算(例如调用Add)而无需任何额外的样板代码,但是对于不同类型之间的计算(例如将Price乘以Quantity),我需要显式地允许它,例如进行类型转换。
  • 我不想有重复的代码,例如为每个类型单独定义我想要使用的每个方法(即使它们委托给一个公共实现)。

我尝试了3种不同的实现,但都没有得到正确的结果。我是否遗漏了任何可以满足我的要求的方法?如果没有,那么推荐的做法是什么?

方法1

type Price decimal.Decimal
type Quantity decimal.Decimal

这种实现意味着我不能对类型为Price的变量使用decimal.Decimal的方法(例如Add()),因为根据Go规范,“它不会继承绑定到给定类型的任何方法”。

方法2

我可以使用类型别名,如下所示:

type Price = decimal.Decimal
type Quantity = decimal.Decimal

但在这种情况下,我可以将Price传递给期望Quantity的函数,因此我无法获得类型保护。一些文档说类型别名主要用于在重构期间提供帮助。

方法3

我可以尝试使用嵌入类型:

type Quantity struct {
    decimal.Decimal
}

这在大多数情况下都可以工作,但在这种情况下:

qty.Add(qty2)

qty2不是decimal.Decimal类型,所以我必须做一些丑陋的事情,比如:

qty.Add(qty2.Decimal)
英文:

My current code base defines two types like this:

type Price uint64
type Quantity uint64

This works out nicely as I can't accidentally pass a Price type into a Quantity or else the compiler will complain.

I now need to switch the implementation from uint64 to an arbitrary precision decimal using the shopspring/decimal library.

I'd like the following requirements that worked from the previous uint64 implementation:

  • If I pass a Price to a function expecting a Quantity and vice-versa, the compiler will complain
  • I can do calculations (such as calling Add) between two Quantity's without any extra boilerplate, but for doing calculations between different types (such as multiplying a Price by a Quantity), I need to explicitly allow it by doing something such as casting.
  • I'd like to not have duplicate code such as defining every single method I want to use separately for each type (even if it delegates to a common implementation)

I've tried 3 different implementations, but none of them work right. Is there any approach that I am missing that would do what I want? If not, what is the recommended way to do things?

Approach 1

type Price decimal.Decimal
type Quantity decimal.Decimal

This implementations means I can't use methods on decimal.Decimal (such as Add()) for variables of type Price since according to the Go spec "It does not inherit any methods bound to the given type".

Approach 2

I can use a type alias like this:

type Price = decimal.Decimal
type Quantity = decimal.Decimal

but in this case I can pass a Price into a function expecting a Quantity so I don't get the type protection. Some documentation says the type aliases are mainly for helping during refactoring.

Approach 3

I can try to use an embedded type:

type Quantity struct {
    decimal.Decimal
}

This works in most cases, but in this case:

qty.Add(qty2)

qty2 isn't a decimal.Decimal so I'd have to do ugly things like

qty.Add(qty2.Decimal)

答案1

得分: 1

你可以使用泛型来实现这种方法。对于自己编写的类型来说,这样做会更容易。如果你想要对外部类型实现这个方法,你需要一个包装器。

示例:

type Number[K any] uint64

func (n Number[K]) Add(n2 Number[K]) Number[K] {
	return n + n2
}

// 这些是用作参数的虚拟类型,用于区分不同的 Number 类型。
type (
	price    struct{}
	quantity struct{}
)

func main() {
	var somePrice Number[price]
	var someQuantity Number[quantity]

	// 没有错误
	somePrice.Add(somePrice)
	// 无法将类型为 Number[quantity] 的变量 someQuantity 用作参数传递给 somePrice.Add,因为它的类型不是 Number[price]
	somePrice.Add(someQuantity)
}

现在,如果你想对 decimal.Decimal 这样的外部类型进行类似操作,但你无法编辑源代码使其像这样工作,你必须为任何需要参数类型与接收器类型协变的方法编写包装器。

例如,假设你正在使用 https://github.com/shopspring/decimal 库:

package main

import "github.com/shopspring/decimal"

type Number[K any] struct{ decimal.Decimal }

// 包装器用于强制实现接收器、参数和返回值之间的协变类型。
func (n Number[K]) Add(d2 Number[K]) Number[K] {
	return Number[K]{n.Decimal.Add(d2.Decimal)}
}

// 这些是用作参数的虚拟类型,用于区分不同的 Number 类型。
type (
	price    struct{}
	quantity struct{}
)

func main() {
	var somePrice Number[price]
	var someQuantity Number[quantity]

	// 没有错误
	somePrice.Add(somePrice)
	// 无法将类型为 Number[quantity] 的变量 someQuantity 用作参数传递给 somePrice.Add,因为它的类型不是 Number[price]
	somePrice.Add(someQuantity)
}

你需要为每个具有协变类型的方法编写一个包装器。

另外,你可以创建自己的库或者分叉现有的库,并直接在第一个示例中的方法中添加此功能:

例如,你的 decimal.go 分叉版本可能如下所示:

//          +++++++
type Decimal[K any] struct { ... }

//             +++                +++         +++
func (d Decimal[K]) Add(d2 Decimal[K]) Decimal[K] { ... }
英文:

You can use this approach with generics. It's easier to do for a type you write yourself. If you want to implement it with an external type, you will need a wrapper.

Example:


type Number[K any] uint64

func (n Number[K]) Add(n2 Number[K]) Number[K] {
	return n + n2
}

// These are dummy types used as parameters to differentiate Number types.
type (
	price    struct{}
	quantity struct{}
)

func main() {
	var somePrice Number[price]
	var someQuantity Number[quantity]

	// no error
	somePrice.Add(somePrice)
	// cannot use someQuantity (variable of type Number[quantity]) as type Number[price] in argument to somePrice.Add
	somePrice.Add(someQuantity)
}

Now if you want to do this for an external type like decimal.Decimal, which you can't edit the source for to make it work like this, you must write wrappers for any methods where you need the parameter types to be covariant with the receiver type.

Example, here I'm assuming you're using the https://github.com/shopspring/decimal library:

package main

import "github.com/shopspring/decimal"

type Number[K any] struct{ decimal.Decimal }

// Wrapper to enforce covariant type among receiver, parameters and return.
func (n Number[K]) Add(d2 Number[K]) Number[K] {
	return Number[K]{n.Decimal.Add(d2.Decimal)}
}

// These are dummy types used as parameters to differentiate Number types.
type (
	price    struct{}
	quantity struct{}
)

func main() {
	var somePrice Number[price]
	var someQuantity Number[quantity]

	// no error
	somePrice.Add(somePrice)
	// cannot use someQuantity (variable of type Number[quantity]) as type Number[price] in argument to somePrice.Add
	somePrice.Add(someQuantity)
}

You will need a wrapper for each method with covariant types.

Alternatively, you can make your own library or fork the existing one and add this feature directly with the method in the first example:

For example, your fork of decimal.go could look like:

//          +++++++
type Decimal[K any] struct { ... }

//             +++                +++         +++
func (d Decimal[K]) Add(d2 Decimal[K]) Decimal[K] { ... }

huangapple
  • 本文由 发表于 2023年1月13日 09:01:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/75103855.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定