Sum types in Go
Sum types (and their cousins, option types) are a much-requested language feature for Go. (I have no statistics on that, it's just a gut feeling.) Here is how to trivially implement sum types in pure Go, no package or compiler hack needed.
What is a sum type?
In a nutshell, a sum type is a type that combines two or more other types into one. These “other types” are called variants.
For instance, you might want to have a function return either a data value or an error. (This special case of a sum type is called an option type.)
In pseudo-code:
func f() Option {
result, err := DoSomething()
if err != nil {
return Error("oops:", err)
}
return Data(result)
}
The Option type combines two variants, Data and Error. If all goes well, the function returns the Data variant, else the Error variant.
Can a sum type be implemented in Go?
It can, but it is necessary to tweak Go's interface concept a bit.
Interfaces describe a common behavior by listing one or more functions. Each type that implements those functions is an instance of that interface.
A sum type, however, does not necessarily have any functions to implement. It only needs to represent different sorts of values! You would thus need to use an empty interface to represent a sum type. But that's not going to work either: Any type satisfies the empty interface.
So we have to use a trick.
Take a look at this interface:
type Option interface {
isOption()
}
The function isOption() serves no other purpose than to make this interface distinct from an empty interface.
Only types that implement isOption are Option variants.
And note that isOption() is unexported. This prevents any third-party code to add variants to the Option type.
In other words, this function “seals” the Option interface.
The two variants, Data and Error, are therefore implemented as follows:
type Data[T any] struct {
Value T
}
func (Data[T]) isOption() {}
and
type Error struct {
Err error
}
func (Error) isOption() {}
That's all! Our Option type is ready to use.
How to use the Option type
Here is a function that returns an Option type instead of the well-known (value, error) tuple:
func DoSomething(b bool) Option {
if b {
return Data[int]{Value: 42}
}
return Error{
Err: fmt.Errorf("oops"),
}
}
Seasoned Gophers may frown on this, because it is considered bad style to return an interface! And there is a reason for this: If the caller gets an interface back, it must analyze the return value to determine the concrete type behind the interface.
However, this return value analysis is an intentional part of the Go sum type approach. Different variants of a sum type require different action, so it's a natural part of sum type handling to branch into variant-specific return type handling.
No worries, determining the variant and acting upon it requires only a switch on the return value's type and a separate case for each of the possible variants, like so:
func main() {
opt := DoSomething(true)
switch option := opt.(type) {
case Data[int]:
fmt.Println(option.Value)
case Error:
fmt.Println(option.Err)
}
default:
}
Beware: Whenever you write such a sum type switch, you have to include all existing variants as cases.
Luckily, there is a linter for this. alecthomas/go-check-sumtype checks sum type switches for exhaustiveness. To let this linter know if an interface represents a sum type, you need to place the comment string //sumtype:decl above the interface definition:
//sumtype:decl
type Option interface {
isOption()
}
go-check-sumtype then verifies that each interface decorated with this comment accounts for all possible variants and has a default clause.
Conclusion
That's all that's needed for writing sum types in Go. However, keep in mind that this is not idiomatic Go code. Consider carefully if you need a sum type or if you can get away with more idiomatic Go concepts. Other people who read your code want to understand it easily, without having to wrap their brains around uncommon constructs.
If you want to closely inspect the code, it's available on the Go Playground.
