BurntSushi/go-sumtype

add checks for simple enums

BurntSushi opened this issue · 3 comments

In Go, creating a simple enumeration with integer values is a common idiom. We should try to apply exhaustiveness checks to those as well.

I think there are a few variations on this pattern, and we should try to enumerate them here.

The first is to just assign names to integers:

const (
    Variant1 = iota
    Variant2
    Variant3
)

The problem with this pattern is that the variants are just integers, so it might be hard to reliably do exhaustiveness checks here. I think the problem is, "How do we know which switch statements to apply exhaustiveness checks to?"

Another is to create a specific type:

type MySum int

const (
    Variant1 MySum = iota
    Variant2
    Variant3
)

This case is easier to detect, since the switch is typically written as switch value { ... } where value has type MySum. The problem here (and is also present with the above case) is that MySum isn't actually sealed. Anyone could create a new variant in an ad hoc way.

Finally, another pattern which is less common is to create a sealed enum by making it impossible for clients to create ad hoc variants:

type MySum struct {
    v int
}

var (
    Variant1 = MySum{0}
    Variant2 = MySum{1}
    Variant3 = MySum{3}
)

In this case, we have the same level of safety as we do with the interface-based sum types because we can find all possible inhabitants of MySum by only looking at the package in which the type is defined.

I'm in favor of the 2nd approach, as it feels more natural to write, and it addresses exhaustiveness problem of the 1st approach. While it fails to address a definition of sealed, as the 3rd approach does address, consider the following:

We define the seal as those types that are defined contiguously, with a disregard for comments and whitespace. This does away with ad hoc variants, and still covers the common case of wanting to sum over enums that are defined in one place.

@henrywallace Your work around is certainly a way to determine the set of legal variants, but it is not enough to call it sealed. :-) If a sum is sealed, then that means we can rely, 100%, on the semantics of the language to guarantee that we can actually witness all of its variants within a single package.

So I think the question here isn't whether we can come up with a heuristic to determine variants, but whether or not we want to require that the sum is actually sealed. The existing sum types based on interfaces do require sealing, so it would be nice to be consistent. However, the sealed enum approach isn't one I've seen in a lot of Go code, where the first two approaches to enums are quite a bit more common.

One possible path forward is to start with option (3) since it is both the most conservative and most solid with respect to guarantees. We could then additionally implement option (2) (perhaps with a heuristic similar to what you propose, although I might lean towards "every top-level value with type X in the package in which X was define" for simplicity). What I mean is, even if we went with option (2) initially, we'd still probably want to support option (3) as well. So... maybe starting with option (2) is fine after all. :-)

There is another tool that does this: https://github.com/nishanths/exhaustive