Are You Sure You Want An Enum?
Table of Contents
Cooking with Enums #
I’ll pick on Go, but this applies to most languages without sum types. Go doesn’t have them. It doesn’t have enum types
either. If you search for enum in go
, you’ll probably find something like this:
type Flag int
const (
ReadOnly Flag = 1 << iota
NonBlock
Append
)
Here we’re trying to model a set of flags similar to the open
syscall. You can imagine this being used in a
function:
func Open(name string, flags Flag) {
isSet := func(f Flag) bool { return flags&f == f }
fmt.Println("ReadOnly : ", isSet(ReadOnly))
fmt.Println("NonBlock : ", isSet(NonBlock))
fmt.Println("Append : ", isSet(Append))
}
Does this have problems? Sure, but I find them rather pedantic. First, you can pass an uninitialized,
zero-valued Flag
to open:
func main() {
var flags Flag
// ...
Open("somefile", flags)
}
Given this API and assumed usage, however, it’s likely that we’re going to be specifying flags inline:
Open("somefile", ReadOnly|Append)
Or at the very least, any flag variables are probably going to be initialized directly and close to the invocation
of Open
, so this type of mistake isn’t a big concern. Furthermore, the uninitialized value has a valid meaning
(“no flags”). Like the syscall, Open
can probably be assumed to validate the set of flags and ensure that any
required modes are set. Passing no flags will fail fast.
Boiling the Pot #
So that mostly seems fine. Here’s another example:
type Day int
const (
Mon Day = iota + 1
Tue
Wed
Thr
Fri
Sat
Sun
)
func Schedule(d Day) {
// ...
}
Hmm, this seems more dubious. The zero value has no valid meaning; we must choose to either have it correspond to a
valid day, or leave it undefined (either implicitly as above, or with an explicit None
variable). Neither option
is attractive.
Furthermore, days of the week seem like something we’re more likely to get back as a struct member or function return
value. It’s much less obvious that Schedule
or other code expecting a day will perform validation of the kind that
made Open
safe.
Despite the above, you’ll see this pattern all the time. How big of an issue it is just depends on context.
Fire in the Kitchen #
Here’s another example:
type AccountId int
type AccountType int
const (
Checking AccountType = iota + 1
Savings
)
func DebitAccount(id AccountId, a AccountType) {
switch a {
case Checking:
// ...
case Savings:
// ...
default:
panic("invalid account type")
}
}
Now we have a full-on grease fire in the kitchen. If you see code switching on enum types like this, ensure your sprinklers and extinguishers have been tested recently.
We have all the problems from before with a new, worse one. The intention is clearly that DebitAccount
be able to
handle each account type. Enums are a terrible fit for this in any language, but especially in Go.
OSHA Compliance #
What’s the solution? Well, what do we want?
- A fixed set of types on which we will add operations.
- Guarantees that our code handles all cases if new types are added.
Y’know what that sounds like? Our old friend double dispatch, via the Visitor Pattern:
type AccountVisitor interface {
VisitCheckingAccount(*CheckingAccount)
VisitSavingsAccount(*SavingsAccount)
}
type Account interface {
Accept(AccountVisitor)
}
type CheckingAccount struct{}
func (c *CheckingAccount) Accept(v AccountVisitor) {
v.VisitCheckingAccount(c)
}
type SavingsAccount struct{}
func (s *SavingsAccount) Accept(v AccountVisitor) {
v.VisitSavingsAccount(s)
}
type DebitVisitor struct{}
func (d *DebitVisitor) VisitCheckingAccount(c *CheckingAccount) {
fmt.Println("Debit Checking")
}
func (d *DebitVisitor) VisitSavingsAccount(s *SavingsAccount) {
fmt.Println("Debit Savings")
}
func (d *DebitVisitor) Debit(a Account) {
a.Accept(d)
}
func main() {
checking := &CheckingAccount{}
savings := &SavingsAccount{}
debitor := &DebitVisitor{}
debitor.Debit(checking)
debitor.Debit(savings)
}
I swear this must be the most under-utilized pattern in programming. Don’t throw this particular baby away with the OOP bathwater. For Go in particular, given its lack of enum types, I find it especially useful even for simple cases.