Enums in Go
dizzy.zone
dizzy.zone
Having support from the language would be nice, though.
Literacy is an example. We're used to a world where it's just normal that other humans can make marks and interpret our own marks as to meaning. A thousand years ago that would be uncommon, ten thousand years ago vanishingly rare.
I really celebrate tools which take an idea that everybody agrees is a good idea, and bake it in so that everybody just has that, rather than agreeing it's a good idea but, eh, I have other things to do right now.
And it annoys me when I see these good ideas but they're left as an option, quietly for a few people to say "Oh, that's good to see" and everybody else misses out, as if "Literacy" was an optional high school class most of your peers didn't take and now they can't fucking read or write.
Example: Git has a force push feature, necessarily we can (if we have rights) overwrite a completely unrelated branch state, given any state X, now the state is our state Y instead. This isn't the default, that part is fine... Git also has "force-with-lease". This is a much better feature. Force-with-lease says "I know the current state of this branch is X, but I want to overwrite it anyway". If we're wrong, and X is not (any longer perhaps) the current state, the push fails. But force-with-lease isn't the default.
[Edited to fix clumsy wording in the last sentence]
Yes, you can counter that with GADTs and match against exhaustive subsets. But to ergonomically handle these cases, you need something like Pattern Synonyms or you drown in boilerplate.
match value {
A => { do_stuff() },
B => { unreachable!("B is not applicable here because <reason>") },
}
now when you add C you still have to match it(given the enum is exhaustive).Same as above: this solution stops working as soon as you're handling 5 out of 50 cases (or, more realistically, 10 out of 200). Lexical tokens are which always trigger the mentioned problems in my code - often you match against subsets, as there are _way_ too many of them to add them all explicitly.
Then, this "limitation" is not an argument for not running the exhaustive check. In the vast majority of cases where there are about 5 enum entries and most cases need their own path, they would be explicitly written out (i.e. no _ =>), and this works extremely well. I have had good experience, and I believe other people can attest this.
If you only have 1 case matched and everything else goes in _, later needs to add one case, you just do that, likely in every other language, there is nothing that can help. But what's described above is already a big improvement.
That's not what I wanted to express. What I wanted to say is, that even when using Haskell, which has all the possibilities to actually handle matches of subsets quite ergonomically, you can't be sure that there isn't at least one čase which isn't caught by the exhaustiveness checker (and sometimes it's just wrong, but then we're not talking about enums). So you always have to check all manually, but the checker makes that easier.
case FILE_NOT_FOUND:
/* This is normal, nothing to do. */
break;
This should still catch adding new value into the enum and not handling it.It's hard for me to think of an example where it would make even sense to "having to remember to handle the variant" rather than "handling the desired effect of the variant".
The Rust version is bith shorter and more readable - and probably more efficient - thanks to Rust enums and Rust error handling. I don't understand why golang doesn't copy Rust here. The error handling in particular could be a very simple change.
I am not a huge fan of go:generate and similar projects. They add a level of unknown that goes against the core Golang design values.
I'm not sure I would agree with that. go:generate is a core part of Go since v1.4 and the enum generators are the kind of thing that was intended. https://go.dev/blog/generate
That said, enums would be a welcome improvement to the language. But even then, I think go:generate has a place.
Still, the lack of enums and enum/sum types remains by far my biggest gripe about Go.
It's absolutely just a hunch and personal preference but I worry that keeping the enum value inputs in comments might not be great for new contributors and in terms of maintaining the codebase over time, so I think I prefer the approach taken by enumer.
https://github.com/search?q=enum+generator+language%3AGo&typ...
https://github.com/search?q=enum+generation+language%3AGo&ty...
Go 2 needs to have more usable enums. And while I'm not a big fan of "adding more stuff" to languages, it wouldn't hurt Go to learn a couple of things from Rust.
I'm not a language design expert, but I suspect there be worms in that there can.
The data structure for a token that makes most intuitive sense to me is a tagged union.
So I defined an „const iota“-style enum. Stuck it into a struct that has the appropriate fields to cover all the cases and it was fine.
Having some syntax sugar for tagged unions would be nice. Having exhaustiveness checks if you switch over them, could be useful in some cases.
But that’s not where my mental energy went at all.
Reading the bytes (runes) efficiently and correctly into the data structure however is the part that needs focus. Once the data is in shape, I‘m not „worried“ at all anymore. Sure a bit of extra support is nice, but also kind of superficial.
Also going further, dispatching on them is again never the tricky part. It’s handling the cases correctly and efficiently that has my focus.
In Clojure, a common thing is to write multimethods that dispatch on (namespaced) keywords. Similar in spirit and structure, but each method might now reside in a different namespace or not even be written by you. But I have never worried about exhaustive matching or similar. What’s in the method bodies is the important part.
https://pkg.go.dev/structs@master
What more sane languages would use attributes for, Go 1.23 will do it like this,
type myStruct struct {
_ struct.HostLayout
}
Lovely design.There are quite a few subtle caveats you can run into without proper support in the language like the article mentions. (E.g. Using iota, passing in an undefined enum)
There are people who want C's "surprise it's actually just an integer" which lets them use C's enums as bit flags, and that I agree isn't just a sum type, but the fact that Rust will let you sum things which aren't units isn't that this is "really" a tagged union, that's a possible implementation detail but not the core idea.
This is not C++ std::variant which really is just a tagged union. Option<OwnedFd> isn't a tagged union, it's "just" much nicer sugar for C's signed integer type. Option<&str> isn't a tagged union it's sugar for a fat pointer which can be null. And so on.
I think to do that you'd probably give up a lot of the simplicity Go is aiming for. I personally think that simplicity is somewhat illusion (Amos' "I want off Mr. Golang's Wild Ride" https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-... says it better than I could) but we should be clear that it's not that Go aimed to do what I want and missed but that it was never interested in that at all. An F1 car doesn't want to be useful for taking the kids to school, so it's silly if we're scoring it poorly for lack of child seat fixtures.
Following the addition of type sets for generics I think go actually has all the pieces for union types already and it’s a matter of putting them all together at the compiler level:
- allow type sets as types (currently they’re only valid for constraints), probably excluding those using “underlying types”
- implement match completeness for type switches over type sets
And there you go, you’ve got unions from which you can easily implement sums via type declarations:
type Foo int
type Bar struct {}
type FooOrBar interface { Foo | Bar }
func Thing(v FooOrBar) {
switch vv := v.(type) {
case Foo:
// you have a foo
case Bar:
// you have a bar
// a default case is required if the cases are not exhaustive, forbidden if they are
}
}
Is this perfect? Not even remotely, this suffers from the usual Go issues of zero values, unenforceable constructors, and nil interfaces.But these are issues of the language, they should be fixed in the language in a hypothetical Go 2, I don’t think there is a good reason to try and work around them here.
Also completeness requirements could probably be extended to all “trivial” switches (types or values, not generalised expressions) via a go.mod stricture, similar to the new loop semantics.
type Color int
const (
Red Color = iota
Green
Blue
)
var Colors = []string{ "Red", "Green", "Blue" }
Now (Colors[Red] == "Red") and (slices.Index(Colors, "Green") == Green).And that's exactly the kind of thing people are discussing here.
There is no language that completely isolates you from runtime hazards.
Although I agree it probably should be done quite a bit earlier.