Agree with this. There are couple of observations I'd make, though.
Firstly, "enums" using iota should always be defined as either
const (
Red Colour = iota + 1
Blue
Green
)
or
const (
Unknown Colour = iota
Red
Green
Blue
)
to avoid the zero value problem.
Secondly, and this is a personal preference, I've really enjoyed not having to work with sum types. In practice other programmers seem to use them when a public interface would have been sufficient, and it's convenient to be able to do:
// original package
type Position struct {
X, Y int
Rot Rotation
}
type Action interface {
Apply(Position)
}
type MoveForward struct {
Steps int
}
func (m MoveForward) Apply(p Position) {
switch p.Rot {
case Up:
p.Y += m.Steps
...
}
}
// second package wrapping the first
type WarpToPoint struct {
X, Y int
}
func (w WarpToPoint) Apply(p movement.Position) {
p.X, p.Y = w.X, w.Y
}
> I've really enjoyed not having to work with sum types
Your example of what you prefer uses (a shitty approximation of) a sum type in p.Rot.
(This is also the most basic possible use of a sum type; they are not only useful for enums, it's just to point out that even a large amount of "simple" Go code would benefit from them.)
I understand that p.Rot is a shitty approximation of a sum type, but it still works and almost certainly won't break anything since Go forces you to explicitly typecast. The important thing is that the list of possible actions wasn't sealed by the type system, which in the original example it was.
I want to reiterate that I am aware sum types can be useful. I just don't think they're useful _enough_ to outweigh being a footgun for calling code.
I would argue that this misses the use case of sum types, which typically don't have behaviour (or they'd just be interfaces!).
For example, consider an AST for a programming language. AST nodes don't have any behaviour (though they might have some methods for convenience, though nothing that implements behaviour). You want to do optimizations and printing and compilation and so on, but on top of the pure AST.
If the caller of your interface does not specify a value for your enum, they have implicitly specified the zero value. Whether that’s desirable behavior or not is up to you. For most clients, this behavior can be surprising if the zero value is a meaningful one (i.e. one that implies an intentional choice).
IME it’s useful to explicitly define the zero value’s meaning as “UNSPECIFIED”, to simplify the problem of trying to guess if the client intended to pass a zero value.
> Enums exist specifically to be compared with one of their possible values, how can you have a zero value problem?
Because there's not actually support for enums in go.
There's support for constant values, and automatically assigning sequential values to them. That happens to be useful for solving the same kinds of problems that enums solve, but they're not equivalent.
Firstly, "enums" using iota should always be defined as either
or to avoid the zero value problem.Secondly, and this is a personal preference, I've really enjoyed not having to work with sum types. In practice other programmers seem to use them when a public interface would have been sufficient, and it's convenient to be able to do: