Home > Mobile >  (How) can I implement a generic `Either` type in go?
(How) can I implement a generic `Either` type in go?

Time:04-10

With the new generics in Go 1.18, I thought it might be possible to create a 'Either[A,B]' type that can be used to express that something could be either of type A or type B.

A situation where you might use this is in situations where a function might return one of two possible values as a result (e.g. one for 'normal' result and one for an error).

I know the 'idiomatic' Go for errors would be to return both a 'normal' value and an error value, returning a nil for either the error or the value. But... it sort of bothers me that we are essentially saying 'this returns A and B' in the type, where what we really mean to say is 'this returns A or B'.

So I thought maybe we can do better here, and I thought this might also be a good exercise to see/test the boundaries of what we can do with these new generics.

Sadly,try as I might, so far I have not been able solve the exercise and get anything working/compiling. From one of my failed attempts, here is an interface I'd like to implement somehow:

//A value of type `Either[A,B]` holds one value which can be either of type A or type B.
type Either[A any, B any] interface {

    // Call either one of two functions depending on whether the value is an A or B
    // and return the result.
    Switch[R any]( // <=== ERROR: interface methods must have no type parameters
        onA func(a A) R),
        onB func(b B) R),
    ) R
}

Unfortunately, this fails rather quickly because declaring this interface isn't allowed by Go. Apparantly because 'interface methods must have no type parameters'.

How do we work around this restriction? Or is there simply no way to create a 'type' in Go that accurately expresses the idea that 'this thing is/returns either A or B' (as opposed to a tuple of both A and B).

CodePudding user response:

The Either could be modeled as a struct type with one unexported field of type any/interface{}. The type parameters would be used to ensure some degree of compile-time type safety:

type Either[A, B any] struct {
    value any
}

func (e *Either[A,B]) SetA(a A) {
    e.value = a
}

func (e *Either[A,B]) SetB(b B) {
    e.value = b
}

func (e *Either[A,B]) IsA() bool {
    _, ok := e.value.(A)
    return ok
}

func (e *Either[A,B]) IsB() bool {
    _, ok := e.value.(B)
    return ok
}

If Switch has to be declared as a method, it can't be parametrized in R by itself. The additional type parameter must be declared on the type definition, however this might make usage a bit cumbersome because then R must be chosen upon instantiation.

A standalone function seems better — in the same package, to access the unexported field:

func Switch[A,B,R any](e *Either[A,B], onA func(A) R, onB func(B) R) R {
    switch v := e.value.(type) {
        case A:
            return onA(v)
        case B:
            return onB(v)
    }
}

A playground with some code and usage: https://go.dev/play/p/g-NmE4KZVq2

CodePudding user response:

A solution finally came to me. The key was defining the 'Either' type as a 'struct' instead of an interface.

type Either[A any, B any] struct {
    isA bool
    a   A
    b   B
}

func Switch[A any, B any, R any](either Either[A, B],
    onA func(a A) R,
    onB func(b B) R,
) R {
    if either.isA {
        return onA(either.a)
    } else {
        return onB(either.b)
    }
}

func MakeA[A any, B any](a A) Either[A, B] {
    var result Either[A, B]
    result.isA = true
    result.a = a
    return result
}

func MakeB[A any, B any](b B) Either[A, B] {
  ... similar to MakeA...
}

That works, but at the 'price' of really still using a 'tuple-like' implementation under the hood were we store both an A and a B but ensure it is only possible to use one of them via the public API.

I suspect this is the best we can do given the restrictions Go puts on us.

If someone has a 'workaround' that doesn't essentially use 'tuples' to represent 'unions'. I would consider that a better answer.

  • Related