Home > Software engineering >  How to define two separate types for arbitrary precision decimals so they can only be be used with t
How to define two separate types for arbitrary precision decimals so they can only be be used with t

Time:01-14

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)

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!

  • Related