Go Enums Still Suck
zarl.dev
zarl.dev
Depending on the project, maybe that is ok. I work on projects with multiple teams and zounds of developers. In this sphere, productivity is NOT increased with these kinds of things as they save moments during writing but cost more during onboarding and reading. The more you have to keep in your head to make the code make sense, the harder it is for new team members to spin up.
Productivity in the kinds of orgs I work in is improved when individuals can look at a small section of code and understand it directly. If you are going up call chains to understand things, you are costing productivity. When you have to slow down and mentally parse something, you are costing productivity.
I would take a hand written and hand maintained version of what this utility has as output over the utility every single time.
Of course now I do, 20 of them, because when there's no built in, there will be packages made by the community.
Mercury 0.378,2439.7,3.3e23,57910000,88,0.0000000001,0,false
There is no way to infer what any of the values are. They are practically magic numbers. How do you know if you swapped a pair of numbers there and what that will do to the application? You have to mentally map all those things.It is vastly, overwhelmingly more new-dev friendly to use:
MERCURY: Planet{
planet: mercury,
Gravity: 0.378,
RadiusKm: 2439.7,
MassKg: 3.3e23,
OrbitKm: 57910000,
OrbitDays: 88,
SurfacePressureBars: 0.0000000001,
Moons: 0,
Rings: false,
},``` type planet int // Gravity[float64],RadiusKm[float64],MassKg[float64],OrbitKm[float64],OrbitDays[float64],SurfacePressureBars[float64],Moons[int],Rings[bool] ```
Ideally of course with a switch to change to the second, more verbose definition.
https://www.jetbrains.com/help/rider/Inline_Parameter_Name_H...
Short code with multiple levels of abstractions, with dynamic inheritance, and with a lot of dependency inversion, etc. etc... I don't like it. I will spend a lot more time (probably) just digging into code while trying to understand it.
https://go.dev/play/p/uUNHxM8zqY9
Not sure I understand what this adds over a list of plain old types.
But don't be confused: this is not the lost productivity of a missed optimization. We're enabling orders of magnitude of change by giving the developers tools for thinking rather than a mere code to grunt at the machine.
Reading vast plains of mostly-obvious code, though, attempting to extract the author’s insights from them—that can be genuinely difficult. It’s also very taxing, because it’s never going to be all obvious, you see? Only mostly so. At some point somebody, somewhere, needed to tweak something just a bit to avoid a momentary difficulty, or their brain slipped, or they were new and didn’t know a subtle detail of how things are supposed to be done. Whatever the reason, I now need to remain in a state of constant vigilance for the entire journey.
Give me two pages of code to stare at for an hour until they click any day. As funny as this sounds, that is just plain less effort (and less perception of wasted effort as well, if we’re being honest).
(Now, I wouldn’t object to having both options—a concise description and its long and borung expansion for when I’m just not smart enough to get it—but that’s a language design problem that has for the most part remained unsolved for several decades now. So I’m not holding my breath.)
So perhaps this tool would be very useful, if you're going to use it a ton. Or use it only once and not really have to access the internals. But if it's something that is bespoke, and you update it infrequently, that's ripe for frustration on re-reads.
Rust invites you to aim for brevity and crafting smart solutions whenever you can. Don't get me wrong, sometimes this leads to beautiful code. It can be super satisfying to write. But there is only so much fun in reading yet another 10-step iterator method chain which works due some arcane edge case in the middle of it.
I vastly prefer reading dumb and obvious Go code. It's just easier to understand and to work with.
Low cognitive load is the biggest thing Go got right. That being said it doesn’t get it perfectly right and there are a few places it overdoes it.
cause or effect? who knows.
What I don't want is abstractions that increase cognitive load.
The case presented in the article requires many mixed types in a list. The longer the list, the more you have to keep in your head. In Go, it is common to use table driven tests and you can get something like:
cases := struct{
wins int
losses int
is_eligible bool
}{
{23, 13, true},
{34, 1, true},
{3, 0, false},
}
Each case is abstracted and minimal. But it is very clear as you read that what each is because there are few items in the list. If it was a dozen properties, like in the post, you visually cannot parse that as fast. When the list of properties gets long, you are now mentally counting "ok, the 9th parameter is a float, oh, hm, the 9th param is showing an int... oh, I forgot the 7th param."Abstractions should be clear and make understanding easier. With so many parameters, labeling them makes them clear. Abstractions are not just for reducing line count.
Where am I going wrong? What is the better alternative?
It seems that over the past five or so years, they've been adopted more as pattern matching has. If they're not an explicitly feature of the language, then they can be replicated (like with Java sealed classes).
I'm not sure if Go would ever implement them, or add something that we can more easily replicate sum types with, but god damn are they my favorite language feature that so few languages properly support.
I wouldn't have expected Go to implement them, but after generics and range over functions, I feel like anything could be game.
What is different is that Pascal added an additional layer over enumerations – what is basically a poor man's sum types – to hide that there are enums under the hood. In fact, one has to perform an explicit conversion if they want to access the enum. Sum types predate enums, and no doubt they were trying to stay close to sum type semantics without the overhead. Something later languages, like C and Go, chose to forego in favour of exposing the enums naturally.
But if you're going to go to all the trouble of implementing a poor man's sum types, why not just do it properly? It's not 1970 anymore. We've solved the overhead problems. Sum types are perfectly viable these days.
Go neither has the 1970 Pascal enumerations, nor the 1976 ML sum types.
Both approaches too advanced to implement on 2009, apparently.
We don't want the poor minds Go is targeted at, as per Rob Pike's words, to have imposter syndrome learning the language.
As it is, is a non starter to even bother.
While sum types first appeared in Algol 68, there were some unsolved issues at the time. So Pascal came along with its half-assed 'sum types' as a tradeoff between the benefits of true sum types and what was tenable given the constraints of the time. But there is no reason for any language created in the last 50 years to follow in that path. We've long solved the problems they had.
Go benefits from enums because it has a very basic type system.
Trying to improve on enums by extending the type system, along the lines of what Pascal did or otherwise, is hilariously nonsensical. If you are going to build a better type system, just get rid of enums completely. You don't need them.
Sure, it was a reasonable compromise in 1970, but it is not 1970 anymore.
An idiotic hack, remains an idiotic hack.
Yes, enums are a hack, forever and always. Nobody disputes this.
What it does not lack, however, is enumerations. Enumerations establish the number of something. Go has that feature. That's all enums are. Nothing more, nothing less. Other languages have additional features, like value constraints, that can enhance the use of enums, but those are entirely different features.
The idea that Go doesn't enums because it lacks those other features is like saying that C doesn't have loops because it doesn't have the equivalent of Go's range operator. How ridiculous. Obviously C has loops. Anything else that might enhance those loops is a separate feature.
type
Planet = (Unknown, Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune);
PlanetContainer = record
gravity: real;
radiusKm: real;
massKg: real;
orbitKm: longint;
orbitDays: integer;
surfacePressureBars: real;
moons: integer;
rings: boolean
end;
var
planets : array [Planet] of PlanetContainer; const PlanetIndex = 1..9;
Innersolarsystem = Mercury..Earth;Rust got the basics right. Pattern matching, no nulls, traits, sum types, and the sugar around try with the ?-notation is also great.
Despite it all, this is not a dealbreaker, and Go shines for striking a great balance of a sensical language. But I always cringe a little inside when fanboys defend even the most awkward and annoying aspects of Go.
funnily enough, I would argue that struct embedding is one of the things that Go actually got right. It's simple and elegant, and I think it could've been a useful addition to Rust.
Explicit composition is much more reliable.
Alternatively, it forces you to use the type system to make a better abstraction.
forcing people to type something never forces them to think about it.
At least newer CNCF projects aren't going into Go as much as they previously did.
type planet int // Gravity[float64],RadiusKm[float64],MassKg[float64], ...
Sorry, but what? Is this really an ad-hoc DSL embedded in a regular comment that's then preprocessed by… something, to generate the actual Go code? Certainly there's something that sucks here!Edit: I'd find it marginally less sucky if there at least were some special prefix like `//#` or `//my-preprocessor` or whatever to mark magical semantically significant comments. And what's the deal with the `field[type]` syntax that seems totally ad hoc, why not use the standard Go syntax `field type`?
I think codegen is generally fine for repetitive tasks. It's just a shame that golang doesn't give you a way to do this built-in. Even java has this feature, even if it's lacking in some regards.
See also '//go:embed', which also turns a variable into a magic type via an ad-hoc comment-only DSL.
In go, it is idiomatic to say "macros slow down compilation and often require a second pass, go compilation is fast, macros are bad", and then also to have an extra "make sure go generate is up to date" CI step which parses your go codebase 10 more times, forks dozens of processes, and isn't type-safe since of course it's not it's literally a comment you can typo "//gog:enerate" and no one will notice.
I've certainly never used them and agree they are horrible in every way.
Those plugins would try to point out that the comments that they would preprocessed would look somewhat different from regular comment, e.g.
//this is a regular comment
//#while comments that start with '#' would be preprocessed.
It's still an issue that would shoot the foot of the next person who would maintain your code though.
Not sure why they don't just use a struct with some fields, then just create instances of those instead of magic comments and a preprocessor.
https://go.dev/play/p/uUNHxM8zqY9
Mercury = {"Mercury",190.0,...}
It would not be any more verbose and would let them define constants for use elsewhere if they wish. If they want to control values for validity they can use a NewPlanet func.
I do agree go's enums suck but they need to take a step back and look at what they've created and what would be an easier way to create this if they really need it. It seems like they want real types here, not an enum and if they do want constant planet types it's very easy to make them without all this busywork.
There's not much more you can do with them. A enumeration is literally assigning a number to something. That is all. Enums suck, fundamentally.
Other language features, like value constraints, can help improve the experience of using enums, but, really, if we are inventing a language, why have enums at all? If you think you have a use case for enums, a more advanced type system will offer a better model for your problem.
Sure, enums were a clever hack in the days of yore when we didn't have a good grasp of type theory, but are pointless in a modern language. Of course, Go is purposefully trying to be a language of yore, so it stands to reason that it would have them and all the suckiness that goes along with it, but, again, if we want it to be a modern language then you don't need to hack on top of enums with new features to make them nicer. You can get rid of them entirely.
You're seeing someone try to address their issues with X and suggesting "here's how you use X." It doesn't track.
I have this ASDL generator I use for all my grammar parsing experiments (to build the AST to parse into) that it isn't all that complicated -- mostly because it has zero error checking -- and, if I understand TFA correctly, could be adapted to what they are attempting to do in about an hour. And most of the complexity comes from using mustache as the template language and having to read their docs every single time I want to change something.
True, it does add an extra build step and the reason it's 'simple' is because I've rewritten it at least three times (but that's mostly to learn how a new (to me) parser generator works) but it's now at the point where I can add new AST nodes in conjunction with filling out the grammar rules without having to really think about it. Oh, and I do kind of enjoy shaving yaks so there's that.
Or was it just very dry and although the text did not say so explicitly, the examples did some how show go enums in the act of sucking?
const (
Flag1 = 1 << iota
Flag2 = 1 << iota
Flag3 = 1 << iota
...
)
The magic const shorthands even let you omit the explicit definitions for everything after Flag1, but I think it's easier to get the idea when all the definitions are explicit. [Flags]
public enum Options
{
Default = 0,
Option1 = 1, // can also be defined as 1 << 1
Option2 = 2, // can also be defined as 1 << 2
Option3 = 4 // can also be defined as 1 << 3
}
The above is a choice, and an enum can be defined normally if it's not a bitmask. .ToString() would even format it correctly if multiple flags are toggled, it can be easily parsed with Enum.Parse<MyEnum>(text) and more. And hell, this is just tolerable implementation. It makes a lot of sense given historical context of C enums, which it's a direct improvement over, but not a step away in what is now considered to be the right direction represented by Rust enums instead.(Luckily, unlike Go, C# allows to trivially define Result<T, E> class/struct and a method on it in the form of Map(ok => .., err => ...))
Go at least does not pretend it has anything like enums or sum types, and that is to its credit. You create a Go integer type, it’s flagrantly shit, but it’s not subtle about it.
They are not ideal by modern standards when you look at Rust. But to state that they are worse than Go's is a new and I can't find a way in which this kind of statement would defensible.
(* Pascal in 1970 *)
Planet = (Unknown, Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune);
// C in 1989
enum Planet {Unknown, Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune};
Plus type system guarantees, and in Pascal's case, run time capabilities to query enum values and ranges.Go's hack has barely anything to do with that.
Funnily enough, Algol 68 had sum types, although controversially so due to overhead concerns. If only Go had copied the state of programming before 1970, then we wouldn't have to listen to all the nonsense about enums sucking. Of course they suck – they are, and always will be, a hack to work around a limited type system.
package main
import "fmt"
type planet int
const (
unknown planet = iota // invalid
mercury // Mercury 0.378,2439.7,3.3e23,57910000,88,0.0000000001,0,false
venus // Venus 0.907,6051.8,4.87e24,108200000,225,92,0,false
earth // Earth 1,6378.1,5.97e24,149600000,365,1,1,false
mars // Mars 0.377,3389.5,6.42e23,227900000,687,0.01,2,false
jupiter // Jupiter 2.36,69911,1.90e27,778600000,4333,20,4,true
saturn // Saturn 0.916,58232,5.68e26,1433500000,10759,1,7,true
uranus // Uranus 0.889,25362,8.68e25,2872500000,30687,1.3,13,true
neptune // Neptune 1.12,24622,1.02e26,4495100000,60190,1.5,2,true
)
func main() {
landingOn := venus
fmt.Printf("Landing on %d\n", landingOn)
landingOn = 42 // What a hack!
fmt.Printf("Landing on %d\n", landingOn)
}
https://go.dev/play/p/9AzJeHB0YbTOf course. Same as C:
#include <stdio.h>
enum planet {
unknown,
mercury
/* ... */
};
int main() {
planet landingOn = mercury;
printf("Landing on %d\n", landingOn);
landingOn = planet(42); // What a hack!
printf("Landing on %d\n", landingOn);
}
Value constraints have nothing to do with enums. Enums are exclusively about numbering – and as your code shows, the numbering works just fine. Value constraints are an independent feature. One that Go (and C) lack completely, not just in the case of enum values. You can compile with broken values throughout. Consider: type EmailAddress string
var email EmailAddress = "Not an email address" // A-OK!
Compare that with a language with a more advanced type system like, say, Typescript: type EmailAddress = `${string}@${string}`
let email: EmailAddress = 'Not an email address' // Compiler error
You can probably make a good case that Go would benefit from value constraints, among other features, but if you're going to start adding such features, why have enums at all? Again, they're a hack to work around a limited type system. If your type system isn't limited...That is, with the caveat that Pascal does allow the union to be converted to its underlying enum representation. If we provide such conversion to get at the enum, then sure:
Program Lander(output);
type
planet = (unknown, mercury);
var
landingOn: Integer;
begin
landingOn := Ord(mercury);
writeln('Landing on ', landingOn);
landingOn := 42;
writeln('Landing on ', landingOn);
end.
But you're ultimately comparing apples and oranges.The program is correct – it complies and runs just fine – but the original ask was faulty. Without any kind of real enum support in Pascal the language, there is no way to perform the same operations as is possible in languages that do have natural enum support. This program is the best you are going to do in the absence of that support. This was all detailed earlier.
Again, you are trying to compare apples and oranges. Rust[1] may try as it might to call sum types enums, but enums and sum types – along with Pascal's pseudo 'sum types' – are different models. I understand where your confusion stems from as there are a lot of similarities, but there is also a difference.
[1] And Swift originally, but its documentation is updated to make clear that, contrary to what the name implies, the language's enum construct is actually sum types and not enums.
GCC: error: invalid conversion from 'int' to 'planet'
clang: error: assigning to 'enum planet' from incompatible type 'int'
Yeah, with casts C let's you reinterpret anything as anything else. Similar to `as any` in TypeScipt.
The correct title should read "Go named constants suck", but then I'm not sure what's wrong about them.
...which, indeed, is quite viable as enums are just a hack to work around a limited type system anyway.
I'm coming from a 4k screen, so that does play into it, but i honestly just could not read the article without changing the text properties here.
It's not just using a 4K monitor. It's very difficult to read on mobile as well (and even harder to change properties there, short of reader mode).
With that said, golang is too opinionated for its level of adoption, too out-of-touch with emerging consensus (and I’m being generous with “emerging” here, the Either monad is more than an emerging consensus around the right default for error handling), and too insular a leadership to be, in my personal opinion, a key contender outside some narrow niches.
I’m aware that there are avid advocates for golang on HN, and that I’m liable to upset some of them by saying so, so I’m going to use some examples to illustrate my point and to illustrate that I’ve done my homework before being critical.
Many, including myself, became aware of what is now called golang via this presentation at Google in 2007 (https://youtu.be/hB05UFqOtFA) introducing Newsqueak, a language Pike was pushing back in the mid-90s with what seems to be limited enthusiasm no greater than the enthusiasm for its predecessor Squeak. Any golang hacker will immediately recognize the language taking shape on the slides.
I’ve been dabbling with golang for something like a decade now, because I really want to like it. But like a lot of the late labs stuff it seems to have suffered from the dangerous combination of the implications of Richard Gabriel’s Worse is Better observation: it was simpler, faster, cheaper, and ultimately more successful to incrementally adapt innovations from Plan9 into Linux (and other Unices), to adapt innovations from sam and acme into nvim/emacs (and now VSCode), and to adapt channel-based and other principled concurrency from Newsqueak/golang (not to mention Erlang and other more full-throated endorsements of that region of the design space) into now countless other languages ranging from things like TypeScript and Rust at the high end of adoption all the way to things like Haskell at more moderate levels of adoption. Ironically enough, the success of UTF-8 (a compromise for the non-ASCII world but the compromise that made it happen at all) is this same principle in action via the same folks!
And golang would be fine as yet another interesting language serving as a testbed for more pragmatic applications of radical ideas: but it’s got corporate sponsorship that puts Sun Microsystems and Java to shame in scale and scope, but done quietly enough to not set off the same alarm bells.
The best example of this is probably this GitHub issue: https://github.com/golang/go/issues/19991 (though there are countless like it). I’ve worked with Tony Arcieri, he’s brilliant and humble and hard-working and while we haven’t kept in touch, I keep an eye out, and he’s clearly passionate about the success of golang. But proposal after proposal for some variation of the Either monad has died on procedural grounds for nearly a decade, all while being about the only thing that everyone else agrees on in modern industrial PLT: TypeScript supports it, Rust supports it, C++ de-facto supports it via things like abseil and folly, and of course the hard-core functional community never even bothered with something worse in the modern era. You can even kind of do it, but there are intentional limitations in the way generics get handled across compilation units to ensure it never gets adopted as a community-driven initiative. Try if you don’t believe me (my golang code has a Result type via emacs lisp I wrote).
Another example is the really weird compilation chain: countless serious people have weighed in here, I’ll elide all the classics because most people making these arguments have their own favorite language and they’ve all been on HN dozens of times, but a custom assembly language is a weird thing to have done, almost no one outside the hardcore golang community thinks it’s sane, the problems is creates for build systems and FFI and just everything about actually running the stuff are completely unnecessary: there are other IRs, not all of them are LLVM IR if you’ve got some beef with LLVM IR, and given that go doesn’t seriously target FFI as more than a weird black sheep (cgo) there’s, ya know, assembly language. It’s a parting shot from the Plan9 diehards with the industrial clout to make it stick.
The garbage collection story is getting better but it’s an acknowledged handicap in a MxN threading model context, it’s not a secret or controversial even among the maintainers. See the famous “Two Knobs” talk.
Raw pointers, sum types, dependency management, build, generics that never get there, FFI: solved problem after solved problem killed by pocket veto, explained away, minimized, all with mega-bucks, quiet as a gopher corporate sponsorship fighting a Cold War against Sun and the JVM that doesn’t exist anymore marketed by appealing to the worst instincts of otherwise unimpeachable luminaries of computing.
There is great software written in golang by engineers I aspire to as role models (TailScale and Brad respectively as maybe the best example). I had to get serious about learning golang and how to work around its ideologically-motivated own-goals because I got serious about WebRTC and Pion (another great piece of software). But it sucks. I dread working on that part of the stack.
Go enums do suck, but that’s because we pay a very heavy price for golang being mainstream at all: we’ve thrown away ZooKeeper and engineer-millennia of garbage-collector work and countless other treasures, it sucks oxygen out of the room on more plausible C successors like D and Jai and Nim and Zig and V and (it pains me to admit but it’s true) Rust.
Yes there is great software in golang, tons of it. Yes there are iconic legends who are passionate about it, yes it brought new stuff to the party and the mainstream.
But the cost was too high.
It is clearly the right way, but Go's error handling already has 80% of its benefits. Switching over to a Result type at this point would require a lot of churn for little benefit.
Sum types on the other hand...
> it sucks oxygen out of the room on more plausible C successors like D and Jai and Nim and Zig and V and (it pains me to admit but it’s true) Rust.
D is dead, never even heard of Jai, Nim hasn't caught on, can't believe you're mentioning V. Rust competes mostly with C++ and is unlikely to appeal to C enthusiasts. Zig is promising, but it's in very early stages.
Go is used because it fills the glaring hole of a reasonable, modern, fast and GCd language. It reflects poorly on PL designers that we don't have a properly designed modern language in the space and have to make do with Go. Some combination of Go+Rust would be perfect, but it just does not exist.
It's clearly not. Either<T, Err> creates a dependence between T and Err that should not exist. They are logically independent variables. The state of Err has no reason to imply anything about the state of T.
I'd be inclined to agree that, for all practical purposes, it is the best we've got, but that doesn't mean it is right. An ideal language can do better.