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 twoQuantity
'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)
CodePudding user response:
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] { ... }
CodePudding user response:
Oops, stephenbez already tried what I suggested. Delete!