Go Enums Suck(zarl.dev) |
Go Enums Suck(zarl.dev) |
How long will it be this time until the Go devs accept that Java Enums are a safer and better abstraction over integers for the cases where you'd want an Enum? And that they allow something like EnumSet, which are type-safe bitsets, without everyone having to do that by hand?
Yeah it has evolved a bit since, but keeping the language simple is a worthwhile goal, so they didn't make rapid changes. It was intentional and thoughtful. If you want lots of language features, pick another language. I'll take my simple one.
Also: go enums do suck.
Python was the same way a few years back. People would give elaborate lectures on why Perl's features were so bad, only that they agreed to add many such features to Python within a decade, even at the extreme act of breaking backwards compatibility.
This is seems to be a common arc to so many things. When you start you are all about principles and as you age, you realise practicalities of every day life demand making lots of tradeoffs and deviations from founding principles.
You don't think that most of the complexity of modern languages is unjustified?
In some cases these statements have some merit, but, as in most cases, they demonstrate that the authors didn't really do their homework before making a language. Or they willfully ignored all of these issues. I don't know which is worse.
Go does have generics, though.
> Why don't you just use duck typing?
Go doesn't have duck typing. It has structural typing, which is not duck typing. Duck typing is dynamic typing (at runtime), structural typing is static (at compile time).
> Packages?
Go does have packages.
> Why don't you just use vendoring?
In Go it's recommended to use versioned modules, not vendoring.
That's exactly why I love it so much
To prove your point further, at the time I got the impression that most of the community was against that decision and didn't see the point in introducing generics in the language.
You make it sound as though the developers were opposed to generics, which isn't accurate. Perhaps some in the community expressed such sentiments. The plan has always been to possibly include generics at some point, which they then did.
To be explicit though, this comment is spot on. As someone who was part of the original Java development group when it was called "First Person Inc", I found the language "equivalence" concept debates the most interesting. For example, is Boolean a first class type? Or is it just a one bit integer? Is integer always signed? If you have 1 bit integers, 8 bit integers, 16, 32, and 64 bit integers, why not make 1024 bit integers a type too? Why is the number of bits fixed? If you want to be super radical, is it bits in an integer or is it digits? Is the integer type (radix, digits)? At one time there were discussions about real (signed), integer (unsigned), frac (fractional) and float (split).
And then a product manager type walks in and says something like "Love the architectural purity y'all are going for here but nobody else uses all these things so let's not make something that is so complicated we'll never ship it."
The author does a good job of exploring the characteristics of "good" enums, and I think it would be even better if it was understood that if your language is going to be used to implement finite state machines (which most programming languages do) then having strong protections against injecting invalid states into those machines is essential. If the language provides a way, that is great, otherwise you end up like the author did generating 30 - 50 lines of code for something that should take 3 - 5 lines to express.
[1] This is an inside joke, IYKYK
That's literally what enums are: A set of named constants.
You might be thinking of what is traditionally known as sum types, which some people have recently started calling enums[1]. Indeed, Go does not have sum types.
[1] Presumably because of Rust using the wrong term when specifying its sum types
The article is an advertisement for the authors own Go package that addresses the "problem."
func (o Operation) IsValid() bool {
if o == Unknown {
return false
}
return true
}
Why, oh why don't people just write return o != Unknown
This is so common in the code that I'm seeing on the Internet, on GitHub etc. Is it because people don't understand booleans?For this example we only have a single condition but as soon as you add more conditions it start to get out of hand.
I prefer the original code because it makes the codebase as a whole easier to read. But I don't think there are any 'hard facts' to support using either of these styles over the other in these simple cases.
type myEnum = (value1, value2, value3, value4)
Naturally that is too advanced and slows compile times."Enumeration types appear to be a simple enough feature to be uncontroversial. However, they defy extensibility over module boundaries. Either a facility to extend given enumeration types has to be introduced, or they have to be dropped. A reason in favour of the latter, radical solution was the observation that in a growing number of programs the indiscriminate use of enumerations (and subranges) had led to a type explosion that contributed not to program clarity but rather to verbosity. In connection with import and export, enumerations give rise to the exceptional rule that the import of a type identifier also causes the (automatic) import of all associated constant identifiers. This exceptional rule defies conceptual simplicity and causes unpleasant problems for the implementor."
I am confused by this assertion. I mean, if I had a module `Source` defining an enum:
type MyEnum = (value1, value2, value3, value4)
and another module wanted to import it: import ( MyEnum ) from "Source"
I don't see how I have automatically imported all of the associated constant identifiers. Unless he was assuming that this would force me to import `value1`, `value2`, etc. as distinct identifiers? But it seems these ought to be namespaced inside `MyEnum`, e.g. MyEnum.value1
And one could easily imagine an import syntax to selectively import Enum values if desired: import ( MyEnum: ( value1, value3 ) ) from "Source"
Of course, I'm just making up syntax, but I hope the meaning is clear.Oberon-07 minimalism doesn't make Go better.
Sarcasm ?
Cries in I18N...
Go was developed as a highly opinionated language in which this style of imperative code is apparently preferred. Similarly, the ternary operator shines as a way to use a single assignment to obtain a value that follows from a concise boolean expression, but the ternary operator is wholly omitted from Golang.
Although, being opinionated is not in itself a bad thing. If you want to create something, it helps to have a viewpoint that gives you something to say.
func (t operation) IsValid() bool { _, ok := strOperationMap[s] return ok }
Tbh it has been updated to us an array instead for string indexing.
Sorry to say, but that's the simplest truth. Many go years and years in this industry with little improvement to quality/succinctness/readability. If you cared, and tried, you would.
Programming skill is uniquely distinct from domain knowledge, by the way. e.g. all of the research code written by domain experts that is full on spaghetti.
You could make a case that concurrency primitives weren't mainstream in programming languages at the time. But there's not a strong case for saying that Go introduced new concurrency primitives unless you just ignore the history of programming and programming languages. Nothing in Go's concurrency model was new. Not quite mainstream, sure. But not new. [EDIT: By primitives I take you to mean built-in to the language, not brought in via libraries like pthreads or something.]
I'd also question the statement that "native binaries" were not mainstream. That seems to ignore a lot of code out there, including the C++ code that Go was (in part) meant to replace at Google.
Defer as syntax is maybe new? But some form of finally construct was in a lot of languages used at the time Go was developed. Defer flattens the code by reducing indentation levels, but it introduced nothing new in terms of concepts that weren't already being used by programmers of mainstream languages.
> Garbage collected but always native.
Ok, sure. There were no other native garbage collected languages. Ignoring history, this is true.
> No thread access, native channels and coroutines instead.
If we ignore history again, also new with Go.
> Defer is pretty much net new in language design terms.
I can't think of an equivalent in the form of syntax, so sure. This is a point to Go. It's a small change, but useful for flattening code.
> No while loop?!?
I don't know why the exclamation mark. They have one named type of loop with `for`, but they definitely have a while loop:
for x <= 10 {
...
}
That's a while loop, it's not an infinite loop, it's not a do-while loop. That they reduced their looping constructs to one name (and then determine which actual loop kind by what's between `for` and `{`) does not mean they actually removed while loops. This does simplify the syntax, maybe.> “If err != nil”!!?
[edit: missed this one]
if (some_c_lib_fun(...) == -1) {
// check the errno
}
> Calling the evolution boneheadedI didn't. Why are you putting this here?
I also have great distaste for error handling in go but that’s a distinct argument to have.
Also note that none of Wirth's Oberon variants have achieved any commercial success.
Modula-2, which had enums, on the other hand did enjoy a limited success, across UNIX, PC and Amiga, and is nowadays even available as standard GCC fronted.
Go would have been a failure if the authors weren't Google employees, like it happened with their Limbo.
Nowadays I would bet there are more people using Flutter/Dart than either Xamarin, Cordova or React Native on mobile apps.
Also anyone using JavaScript libraries like scss, is dependent on Dart, at least until they remember to rewrite it in Rust, as is now fashionable on JavaScript tooling ecosystem.
// The file handle will be freed at OS level when exiting current scope
using var file = File.OpenHandle("somefile");You could imitate that with IDisposable, but it would be overkill compared to just using finally (in C#).
[0] https://www.ocamlwiki.com/index.php?title=Enums_in_OCaml
But way more important: this OCaml wiki is an AI generated mess full of, well, bullshit. https://discuss.ocaml.org/t/whats-up-with-ocamlwiki/13605
... and now that you mention it, I do remember the variants terminology, esp. around the polymorphic variants feature. It's been 20+ years since I used OCaml, I'm afraid...
Edit: I guess if you've never seen that this is, uh, controversial. Or something. Anyway, Java enums are full-strength classes, look at the planet example here https://docs.oracle.com/javase/tutorial/java/javaOO/enum.htm...
This is more like a Rust enum than a C one, I think you'll find.
It's not clear why Rust got confused.
https://www.typescriptlang.org/docs/handbook/2/narrowing.htm...
In C#, List<T> and List<U> follows the same assignment rules as T and U, and at runtime are represented by distinct types. That means that going from List<T> to object to List<U> causes a runtime error at the point of casting.
In Java, every generic type is erased to object at runtime, so the runtime type is just List, and you could cast List<T> to object to List<U> and only get an error later, when you try calling U methods on the contents of the list.
(Yes in C# List is a concrete vector type and in Java it is a random-access collection interface, but that is not relevant here)
The type erasure has occasional benefits, like allowing objects that are polymorphic in their type argument when that’s still safe semantically (a simple example being emptyList() and emptySet()), where the type system isn’t expressive enough to otherwise allow it. This is a bit like the “unsafe” escape in other languages.
if they do the compiler will warn you that an List<T> to List<U> cast is naughty
but in that case the only methods you can call on it are that of the erased type anyway
in practice I don't think I've ever seen a bug as a result of this type of erasure (and I've probably worked with at least several million lines of Java)
C#, which is often cited as an example for "generics done right" chose another path, which allowed generics at runtime - they made a hard break and just threw backward compatibility out of the window iirc. The reason Javas designers didn't do that is not only introduced generics far later in its lifecycle, but Java also has always followed the hard rule that breaking backward compatibility is something which should only ever used as a last resort and never between two versions directly following each other.
Sure, or, more relevant, `type monthOrdinal = 1-12` or `type email = {string}@{string}`. Any advanced type system will allow for that, of course, but Go does not. It does not even pretend to claim to be an advanced language. It has, quite explicitly, chosen to not be.
Yes, you are right that if Go had value constraints then an enum type could utilize those constraints, but, again, nothing to do with enums themselves. You are confusing unrelated features.
Actually I think you are. For example, almost all statically typed languages since Pascal do not have value constraints but support typed enums as closed sets. There's no advanced type system needed - no need to define enums as integers and then put additional constraints in the type system to try and restrict this. There is also no need to model enums as integers in the type system in order to use integers as a runtime representation.
Which is an interesting choice: Give a noose for developers to hang themselves with for every single other type other than enums – the types they are going to use most often – and not think twice, but then go full on helicopter parent when using enums – the one type that isn't particularly interesting.
It's a neat parlour trick, don't get me wrong, but I guess that's why almost all of the popular statically typed languages since Pascal (C, C++[1], Typescript[2], etc.) didn't bother with closed enums. They put their time into features that actually mattered to developers instead.
[1] Added later in life, granted.
[2] Ironically, does support value constraints except in the case of using enum.
Sum types and closed enums don't need to constrain existing sets of values, they define the set of values. Again, I think you might be confusing the type system with runtime representation.
> It's a neat parlour trick, don't get me wrong,
It's a step towards sum types which are the mathematical dual of product types. Not a parlour trick at all, every modern language should have algebraic data types.
This is not true, iota is stable in its ordering. https://go.dev/ref/spec#Iota
Real enumerations/discriminated unions is the one thing I consistently wish for in the Go annual surveys
See e.g. https://adaic.org/resources/add_content/docs/craft/html/ch05...
I'm not aware of a good online presentation focused exclusively on Ada's enumeration types and their various uses. It's not even singled out in the Rationale documents for the design of the language and the (3?) design revisions since the 1980 launch; maybe the AARM (Annotated Ada Reference Manual) has more focused discussions? I'm not sure, I haven't looked at these since ~20y ago.
One added benefit is it serializes/deserializes safely (even when you add / remove values), so you can persist and read back values without a problem - even to a different language.
Rust's enums are great. No "auto" boilerplate if not mapping to an integer, exhaustive pattern-matching, sub-types etc.
Rust doesn't have enums. It has sum types – that for some reason it arbitrarily decided to call enums.
Sum types are great. There is a good case to be made that Go would benefit from the addition of sum types. But until that day there isn't much more you can do with enums. That's all enums are – a set of named constants.
Not the first time a word has been used for several nearly-unrelated concepts, and it won't be the last.
I make heavy use of rust and Python enums (Are they both misnamed?) Those + structs are generally the base of how I structure code.
The "enums" in the article also seem to be of the same intent. Is this a "no true Scotsman" scenario?
Some research implies the difference is a True Enum involves integer mapping, while a Sum Type is about a type system. I think both the Rust and Python ones can do both. (repr(u8) and .value for Rust/Python respectively)
The use case is generally: You have a selection of choices. You can map them to an integer or w/e if you want for serialization or register addresses etc, but don't have to. Is that a sum type, or an enum? Does it matter?
Another thought:
Maybe:
#[repr(u8)]
enum Choice {
A = 1
B = 2
}
Is an enum, while enum Choice {
A(C)
B(D)
}
Is a sum type?Oh, so it's a lot like Python then.
> This is fine however it is nothing but an integer under the covers this means what we actually have is:
Oh, so it's a lot like C++ then.
> But what you notice here is we have no string representation of these Enums so we have to build that out next
Have to!
Yes this still all sucks. (But at least there's ugly historical precedent!)
At the same time, Go has generators built in and can generate enum tables, enum to strings, and other things they have shown. I am unsure why they didn't do it the "Go" way.
{ "enums": [ { "package": "cmd", "type": "operation", "values": [ "Escalated", "Archived", "Deleted", "Completed" ] } ] }
My point was the "Go" way to do this isn't parsing a custom format (like your JSON), but it's to use go generate.
You just declare an int type, and then a list of constants of that type.
People are complaining about 'iota' here, but I think it's slick and great. It combines so nicely with eliding types and values from subsequent const declarations:
type MyEnum int
const (
Value1 MyEnum = iota
Value2
Value3
...
)
Nice and simple. Most of the syntax is just the enum value identifiers. And it works well for bit flags too: type MyFlags int
const (
Flag1 MyFlags = 1 << iota
Flag2
Flag3
...
)
Most of the above syntax isn't specific to enums (so you're already getting a lot of other things from it). The only enum-specific syntax is iota and the eliding type/value rule.People seem to want their languages to have all sorts of guardrails, but I find many of these cumbersome. Go gives me the one enum guardrail I care about: The enums are different types, so I can't use a MyEnum as a MyFlag, or vice versa.
I've worked on giant Go codebases, with Go-style enums all over the place, and the lack of compiler-enforced exhaustive enum switches just hasn't been a problem. And it's nice to be able to use non-exhaustive switches, when you want them. Go is simple and flexible here.
The article criticizes Go incorrectly with statements such as these:
> This also means any function that uses these or a struct that contains these can also just take an int value.
> Anywhere that accepts an Operation Enum type will just as happily accept an int.
This is just not true. Here's an example you can run: https://go.dev/play/p/8VGufuxgK6b
The above example tries to assign an int variable to a MyEnum variable, and gives the following error: "cannot use myInt (variable of type int) as MyEnum value in variable declaration"
This error directly contradicts what is claimed in the article. Perhaps they mean that MyEnum will accept an integer literal, in which case I would argue that a guardrail here is silly, because again the problem just doesn't really come up in practice. Regardless, the author is not being very precise or clear in their thinking.
Is this a real problem? If there's a function signature that accepts `Operation`, the caller must explicitly cast the `int` to `Operation`. At that point, it's the caller's own fault.
So I'm not really following what this is solving. As demonstrated in the article, sometimes you want string constants, sometimes you want `iota`, other times you want `1 << iota`. I like that Go doesn't dictate which I have to use if I declare an "enum".
You would think that, but that isn't always the case: https://play.golang.com/p/Ze3pfNEVTVs
It's very easy to create an enum value that isn't actually in the defined range
I like philosophy and I read it as a total amateur. Naming is a big topic in modern philosophy [1] with a huge amount of depth. I think of it in terms of my naïve understanding of Wittgenstein's later work and the idea that the meaning of a word actually comes from its usage within the context of a set of collaborating agents.
If I say to a programmer "use a vector", that will mean something specific if we are writing C++ and I want to use a resizable array. And it could mean something totally different in the context of a 3d rendering engine.
I think of how often I see words like "Context", "Session", "Kernel" and all of their myriad uses.
So I see articles like this as just a pointless argument because we are crossing some boundary between distinct language games. The author of this article thinks "Enum" means one thing. But it is actually the case that "Enum" is unspecified outside of some particular context. And in this case, the author is bringing some outside context and trying to reuse it inappropriately.
Oh and you don't have to use large power-hungry IDEs that don't integrate with any sort of config management to get a decent experience! (/hj)
If I ever learn Haskell it's over for y'all though.
(Agree with OP btw, using codegen to get the enums I want is a workable remedy for Go's lack of enums.)
//go:generate go-enum -type=State
type State int
const (
Unknown State = 0
Disconnected = 1
Connected = 2
)
which then generates a separate file with implementations such as: func (i State) MarshalJSON() ([]byte, error) { ...
[0] https://pkg.go.dev/github.com/searKing/golang/tools/go-enum#...Not a full typesafe enum type, the same clunky "enums" (assigned constants) available in C, but they bother to implement an auto-incremented counter.
So you can't depend on the enum for exhaustiveness warnings e.g. on switch statements, type checking, or correctness, but you do get a useless numeric association autogenerated with iota - so that you can lose the association if you re-order your enum values that you have serialized earlier and want to reload in the future.
Don't serialize to raw integers unless you absolutely have to. Serialize to a string value: it's future/oopsie proof and helps with debugging. The nature of iota is pushing people away from bad habits.
But yes, getting warnings about missing enums in switch statements is very handy. But Golang's type system never aspired to be as rigid and encompassing as C++, Haskell, Rust, etc.
Well, didn't have to aspire to all that to at least make an effort to be more helpful, especially in trivial aspects, like having an actual enumerated type, or an Optional/Error type...
Rigid makes it sound bad. I would suggest "reliable" instead.
iota is a great idea, especially if you have to define bit mask constants, e.g. 1 << iota. I wish other languages (yes, also those with enums) had it as well.
* no null safety
* working with errors
* poor type system
This is sort of like how network protocols can’t statically guarantee enums are valid either. When sending bits over the wire, you can send any bits you like. There can be “values reserved for future use,” but to deny their use, you need a runtime check.
A similar solution works in Go. A runtime check in a constructor function will fix it. The enum’s value would need to be returned as an unexported field in a struct, which is the only way to guarantee that it’s not writable, except by copying it from another valid value.
I don’t see a particular reason why Go couldn’t make this easier.
Im also curious now if the Typescript checker was written in a way that it could be adapted to new languages easily.
In the case of Goplus, it compiles to Go. Speaking of which, Vlang allows transpiling from Go and possibly to Go, but from and to C is more of their priority.
The intent of the Goplus author and contributors seems to be that their people could easily switch between both, but that their version is more feature rich.
Use uint64s with minimal bit overlap.
Maybe nice to include in this ultra-advanced enum libray
I understand the evolution of C, it made perfect sense back when it was invented. And the limitations were necessary due to the wide array of architectures and extremely limited computers of the time in every dimension (CPU speed, IO speed, RAM size, disk size, etc).
Many of those dimensions have been improved by several orders of magnitude, and both compilers and runtimes can afford to be comprehensive. Yet we get this ham-strung language out of the gate.
Very disappointing.
Hey! Just like in C! I digress, I think the issue is that the author comes from another language where enums are a thing. In go, they aren’t. Enums should be types. Types that don’t infer to an int. Use an interface. Be happy.
I'd be willing to bet that there's just a better way to do whatever the actual real-world example they want to achieve is (this was not entirely clear to me from the examples in the post).
Like I said though, that doesn't mean that (real) enums wouldn't be an even better way to do it than whatever the Go way is for a given problem, so I don't want to quibble too much since I think this is one of my biggest day-to-day complaints about Go, but it's worth pointing out that the premise can be flawed and that it's still a problem in the language, these two things aren't completely orthogonal.
TL;DR — Instead of pulling in a code generator and another library, it may be good to think of alternate ways to do the same thing without a lot of extra code footprint.
Which is exactly what they do in the post.
They still have every reason to complain about Go's oft suggested lame substitute.
Well, yeah, this is also the reason they deserve quite a bit of ridicule from actual Go users.
That’s two options. What are the others that are meaningfully different? You have to be able to deal with simple “sum types” in the sense of: this type could take on the value of one of these X predetermined constants. This requirement doesn’t disappear just because the language doesn’t directly support it.
Other languages either substitute enum with primitive type, string, or use strong type system tricks.
Go do duck typing, .. so..
So much so that much of the dotnet official stuff, ie asp.net, use static classes with string fields instead of enums.
Unfortunately that doesn't play well with libraries that have enum support like entity framework. PITA.
One saving grace is the ability to create extension methods on enums.
Developers with a background in other languages assume all enums' use cases need string representation. Well, no. They are needed sometimes, but not always.
The same with the ability to pass int to the enum. Author says:
> Anywhere that accepts an Operation Enum type will just as happily accept an int.
Well, this is simply not true. [1] You'll have to cast your int into your enum, which is totally fine if it's your intent. Granted, there are plenty of valid cases where you need validated input, especially for the public libraries. But hey, not every code is a publicly facing library, and not all need this validation. Why spend CPU and memory on something that probably won't be needed, and that can be implemented with the existing language primitives?
In the end, the author did a great job of solving his own requirements around enums and even wrote a code generator that helps him generate this for millions of enum types per second. :)
>Well, this is simply not true.
As comment above [1] pointed out `printOperation(2)` is still valid.
My understanding is that on a practical level, these kinds of issues arise from misusing types (or not caring about them) and naively putting variables of one type into the function that expects another. Examples with number constants typed in manually do not hold ground.
I had the joy of a numeric ID (an int) getting a B added to the end of it to distinguish the product as being the "same" but sourced from another vendor... (and vendor is part of the system so this is layers of silly, but useful on the floor if there is a problem).
This is the down side of "fleets" of applications with differing degrees of type safety working in concert.
Should an enum be fixed, and its change need to be reflected in every system? Cause current go out of the box isnt that. Should it be open ended as your suggesting ... because a "new int value" can flow through the system in an "Unsafe" way depending on the permissiveness or quality of your code.
I dont like any of the answers, but I candidly dont have a lot of problems with the current enum system in go. Is it great, no. But if your validating at your borders/boundries (storage, api, etc) and being responsible I dont really give it much thought.
Golang is an absolutely ideal language for writing REST API microservices, that exchange JSON, with a practical and blue-collar mindset.
Plus it compiles to small-ish native executables. Which renders Docker superfluous in many uses cases, and also makes it well-suited for writing DevOps tooling (e.g. Docker, everything from HashCorp, etc).
It's not trying to out-cool Haskell and Rust on online message boards. But I would never in a million years evangelize either of those two languages for routine REST API work in most real-world shops, whereas I could suggest Golang without losing professional credibility.
In the wider context of programming languages, enum is fairly well defined concept. Features like being able to convert a value to a string and do exhaustive checking on switch statements are widely implemented. The iota feature in Go is clearly imitating C’s enum keyword. It is fair to compare Go’s built-in ability to declare an enum-like type against other language’s ability to declare the concept.
To be clear, I’m not saying every language has to have every feature. I’m just saying the lack of a feature in a language is not a sufficient reason to excuse its lack.
I am—for some reason—reminded of philosophers who go looking for metaphysical problems, disputes, etc. where there aren’t any.[1]
"model" is another one like that since I'm using it to refer to both data models and ML models. And "prompt" since it can be an LLM prompt or a terminal prompt for the user (this is a CLI tool).
Differentiating all these overlapping terms in ways that aren't super confusing is definitely a challenge.
What I'm saying is the thing he wants and the thing Go has may share a name but they aren't the same thing. Just like C++ has "vectors" and OpenGL has "vectors" that share some superficial similarities but are ultimately totally different things.
If someone wrote an article that said "OpenGL vectors suck" and then mentioned it missed a bunch of features available in std:vector as justification, most people would recognize this error and dismiss the discussion.
I have a far easier time delving in to previously unknown Go code for the first time compared to something like Scala (or even Java). Go is a solid language for those who value that and want to enable the experience for others.
Also, we have been doing computer language design for quite awhile now. This isn’t a new frontier. The deficiencies in Go aren’t in areas of “oh, we never thought of that!”, but are in very well known areas with known solutions.
I find Go code is obscured with house keeping code that isn’t necessary in better languages.
For example, to encode a JSON structure with a dynamic top-level key you need to write a custom marshaller OR marshal twice. That's... awful. Like bonkers level insane.
Go is not simple, it is idiotically designed to deliberately exclude common sense features that ironically makes it less simple and more error prone to code in and read Go.
Other languages are objectively better than Go for every imaginable use case. Rust is better for embedded. Kotlin is better for back end. I could go on.
The creator of Go is very open and candid that he thinks his target audience, Google Engineers, are too stupid to use "advanced" features like oh I don't know, sane error handling? and any number of basic things other languages have.
I know how cringe it is to start flame wars about programming languages, but srsly, Go, PHP, Perl, JS and a few others really are objectively worse (for every context and use case) than widely used alternatives.
Golang _has_ sane error handling. It just considers errors a normal and expected situation.
When you perform a http request, and the result is successful you expect the result to be assigned a variable, right? Then why would you expect non-successful outcome to be returned in a different way? Why is it different? Why do you unwind the stack? Something terrible happened? Definitely not, it's as real life as 200 OK.
For unrecoverable things golang has panics, and if you don't like the idiomatic way of handling errors, you can just throw them like exceptions.
Disappointing it maybe but productive it is not considering people can easily move to far better languages.
It allows to set discriminants explicitly and it allows unit-only enums (enums with only discriminants and no structures associated with them). You can control the underlying type of the discriminant too.
With a little bit of derive macro sugar you can even iterate through all the values of enum.
//go:generate go run ./internal/enumhelper -flag1 -flag2
To include an optional type would be against go's identity.
I can't tell if this is sarcasm.
If it isn't, I don't see how what you've mentioned is minimalistic or how adding an option type would be against Go's identity.
> The encoded value can change any time you re-compile your program
Any value (not just enums) can change any time you re-compile your program, if some programmer goes in and messes it up.
The real, much softer criticism would be that Go requires its programmer's to understand the potential consequences of inserting or shuffling enum values (where iota is involved). It's a much weaker case against iota than what they stated.
What, then, is the enum for? It is specifically to restrict what your code will do. To that end, it is a good way to restrict data your code can introduce. It is not a restriction on what is happening outside of your code, though.
You can, of course, make similar arguments for structs. Or really any data in the code. How do you know your number won't go over some arbitrary size?
Enums are, largely, the most restrictive data in code. Which is why we discuss them more, I think. If folks did work with more big numbers, I'm sure we would be more curious on why things don't act like common lisp where big numbers basically work with no extra work.
This is what an enumeration is, partially because that's precisely what the word "enumeration" means; the ability to assign an ordinal number to each value in the enumeration. To "enumerate" a set is to assign integers to them. In Python, for instance, see the "enumerate" function, which does exactly enumeration on the output of some iterator.
Sum types can be used to represent enumerations, but it's very restrictive subset of sum types. Trying to understand what a "sum type" is through the lens of a single integer would be a very strange way to approach them. Nor are sum types a "superset" of an enumeration; a base sum type is not an enumeration. You need to add more things to it to get an enumeration. In a Venn diagram they're the classic two cicles with some overlap in the middle but with distinct bits on each side.
I do not understand the strangely active desire some people seem to have to erase the distinction between these two things, as if some advantage will result, as if sum types will somehow become more useful than they are or as if they will somehow lose their abilities if we don't also call them enumerations. There is no advantage to smudging these two unique things together. Not saying that you are promoting this per se, the__alchemist, just that I've seen it a lot and I don't get it. It's like someone wanting to claim that database and files are really the same thing; well, sure, there's some overlap, but each does many things the other doesn't and trying to squint until they actually are the same thing is generally the exact wrong direction to go to attain understanding.
To put it another way, when adding an "enumeration" into a network protocol, you allocate some fixed number of bits to hold a given sized integer. When you add "a sum type" into a network protocol, you have a lot more work to do in general.
To put it yet another way, enumerations have meaningful implementations of a ".Next()" that a sum type really doesn't. If you have a sensible implementation of a given method on one type of thing and it's not sensible on some other thing, then clearly they can not be the same thing.
(I say multiple times that a sum types doesn't have such an implementation in this message. By that I mean that while it is trivial to have a "data Color = Red | Green | Blue | RGB Int Int Int" and implement an iterator to walk through all possible values, it is not something that is generally done for all sum types, and if the sum type also includes functions or other complex values it isn't in general possible at all in common programming languages. Again, writing an interator for "all possible functions" is perfectly theoretically possible, but in engineering terms not something anyone would actually do. All enumerations can be iterated.)
Example: For a network protocol, see the first code sample I posted.
For a `.Next()`, add the method. (I did this recently)
Regarding sum types into a network not working, this again sounds like the wrapped enums. One way to do this is use an integer for the enum variant at index 0, and conditionally assign bytes of an appropriate size based on the type wrapped for the next set of bytes.
Why? Why is it so important that they be seen as the same thing to you? What benefit is gained from it? What benefit is gained from blending together a data structure that is fixed bit size from a family of data structures of variable size, a fairly fundamental difference? What benefit is gained from failing to consider the fundamentally different uses they are put to? What benefit is gained from looking at someone list a set of differences between the two, and basically saying, "yeah, they're different, but what if not?"
I can name further properties that differ between them. All sum types can embed arbitrary other existing sum types within themselves, without practical limit. Enumerations can not, because A: they may collide on which numbers they use and B: even if you remap them, you can run out of integers, especially with smaller values like byte-sized enumerations. Enumerations may have further structure within themselves, such that particular bits have particular meanings or values a certain number apart may have relationships to each other, or other arithmetic operations can be given some meaning; sum types themselves do not generally have any such relationships. (At least, I've never seen a sum type in two clauses of the sum type are somehow related; that'd be bad design of a sum type anyhow. Even if you did this to an internal integer contained in a sum type, it would be that integer composed in to the sum type that had that relationship, not the sum type.) Sum types have a rich concept of pattern matching that can be applied, enumerations generally do not (some languages can do some pattern matching with bits but there's still no deep structure matching concept in them).
I mean, how many differences are necessary before they are not the same thing? They can not fit into the same amount of memory; one is fixed in size, the other highly variable. One is simple to serialize into memory, the other has lots of complicated machinery. Each has operations generally valid on one but not the other (enumeration, pattern matching, sum type's composition whereas enums can not generally). The range of valid values (or domain, whichever you prefer) is not the same. There are languages that have enumerations without sum types, in that enumerations appeared in mainstream languages decades before sum types were a mainstream conversation. In what other ways could they be different?
It strikes me like arguing that ints and strings are the same, because honestly, what's the difference between 11 and "11" anyhow? Even if you're working in a language that strives to make the distinction as small as possible, you're still going to get in trouble if you believe they really are completely the same thing. And any programmer who goes through like truly thinking 11 and "11" are the same thing is in for a lot of confusion as concepts they should be understanding as separate, even if at times superficially related, are actually the same.
Then again, so does Go. Go doesn't have an enum keyword like C, but that isn't what defines enums.
But it’s a red herring in any case since enum types are such a basic programming language feature. No need to evoke the Cool Kids languages at all.
Those are strong words for a language with all the flaws I just mentioned. :D Yes, green threads are great for network programming, but it's not the only language with them, and one feature does not make it "ideal". If I had to pick the best networking language... I'd probably say Elixir.
But even if we agree that it's ideal, it doesn't change my point.
When I hear "These aren't really enums", my first reaction is to dive in and do research. (I'd been down this road before, probably after a similar HN comment...), but I haven't found usable or practical conclusions. It seems like the distinction is too subtle to be of use.
Stated more succinctly, let's call Rust enums "Choices", as I think this is causing semantic trouble. "Choices" are an excellent tool.
I'm looking at this from an engineering perspective; not a CS or abstract mathematics one.
I am curious what your pure Enum, and pure SumDataType look like in practice. I am also curious what existing implementations of either exist. Are they Haskell conventions?
But the correct and only sane way to do this is Either<Error, Success> that you can then pass on, map over both or either of the two, flatMap to chain with other Eithers, fold into a single thing etc etc. Not endless sprinkling of
if err != nil { log.Fatal(err) }
everywhere (and no, those operations are not obscure, esoteric or difficult to learn or understand - they're the same for other types like Option, List etc and are trivial to learn in a day for people who aren't familiar with them)
+ not making the compiler distinguish between null and non-nully values (as eg Kotlin, Rust and Haskell does) in itself as well is inexcusable for a modern language
func myfunc(url string) Result[string] {
tup := FromTuplePtr(http.Get(url))
return FlatMap(tup, func(r http.Response) Result[string] {
return Map(FromTuple(io.ReadAll(r.Body)), func(b []byte) string {
return string(b)
})
})
}> in itself as well is inexcusable for a modern language
In Go, you can assign nils only to pointers
Also, did you come up with this on your own, or were you exposed to it?
+ other languages get close, eg Kotlin has nullable types (which is a poor substitute) and Result (which is also poor because it's not a true Either)
that said lots of languages these days have libraries that do it (Arrow, Vavr and countless others)
IMO the killer simple language that Go tries and fails to be would be something like a Kotlin+Arrow with heavily reduced syntax and features, eg
no exceptions (use Either or a correct Result type)
no loops (use map, fold etc)
no nulls (use a correct Option/Maybe type)
etc etc
= in such a language, we learn that methods return things, those things will be what they say they are (guaranteed by the compiler), they will tell you what you can do with them, and if a program compiles, you can be pretty damn sure it works as intended
insert "all the languages are broken, I should create a new language" meme here...
Definitely do.
In Go we just have to emulate it, badly, by manually writing code to forward the error up the stack so you can finally top-level print “error bad thing happen” or maybe some unholy stringification of wrapped errors possibly collected along the way.
The introduction rule for enum values in C is _not_ type safe. You know how you can tell? Well typed programs go wrong. a language absolutely does not need value constraints of any kind to get this right.
Because that's where it is unsafe: You can introduce a value of the same type that is outside of the enumerable range. You cannot introduce a value of a different type, though. It is type safe.
Yeah, any language with a type system worth its salt has value constraints, but if you choose to forego them as C and Go have, you're not going to bother adding them just for enums. It would be kind of silly to leave developers with a noose to hang themselves with everywhere else, but then think you need to tightly hold their hand just for enums.
In fact, I'd argue that if you are short on time and need to make compromises to reach a completion state, enums are the last place you would want to take the time to add value constraints. The types more often used would find much greater benefit from having value constraints.
Case in point: Typescript. When was the last time you cared that its enums behave just like C and Go? Never, I'm sure, because it having value constraints everywhere else more than makes up for it. Giving up value constraints for safer enums is a trade you would never consider.
C’s type system is unsound, and not all compileable programs respect its dynamic requirements. We cope with this by referring to some code as “not type safe”.
foo bar = NOT_FOO;
You say this “typedef enum {…} foo” is not a type, naming a set of values, but just a convenient alias for whatever the representation is, thus all “enum” (regardless of actual decl) name the same set, and every constructor expression shares the same “type”. Consistent with the language specification, and passes the type checker, so you could say this code is “type safe”? but it’s one hell of a foible and not consistent with any lay (non PLT) understanding of type safety, where typesafety means the type written in the code and the runtime representation won’t desync (no runtime type errors).
If you simply forbid UB and refer to only strictly conforming programs, I will accept this modified meaning of “type safe”, but grumble that this meaning is not very good
edit to encompass parent edit: as a typescript nonprogrammer, I have nothing to add :) I am confused why you are putting the features in opposition. gradual + value-sensitive typing is a good feature, but doesn’t conflict with sums. in ocaml, we support both, real sum types as well as variant [`A | `B] etc that are structural in the way you’d want C to be
Along with every other programming language under the sun. A complete type system is not exactly an easy feat – especially if you want it to be usable by people.
> We cope with this by referring to some code as “not type safe”.
Value constraints are an application of types, so yes, if C/Go had value constraints then violation of those constraints would leave it to not be type safe. But they don't have value constraints. Insofar as what the types can constrain, the safety is preserved.
It seems all you are saying is that C (and Go) do not have very advanced type systems. But that shocks nobody. Especially in the case of Go, that was an explicit design decision sung by its creators. You'd have to be living under a rock to not know that.
Was there something useful you were trying to add?
Yes, the clarification about value safety, which you’ve done quite well.
Not every language is unrepentantly unsound.
I continue to identify a confusion in this thread between a property of the languages, and a property of particular code, but I have clearly exhausted your patience. thank you.
For sure. Coq does a decent job, but it's also a complete bear to use. Tradeoffs, as always.
> I continue to identify a confusion in this thread between a property of the languages, and a property of particular code
Go on. The original statement was that C and Go do not have type-safe enums. But there is no evidence of that being the case. The types are safe.
Indeed, the types are limited. An integer type, for example, cannot be further narrowed to only 1-10 in these languages. But the lack of a feature does not imply lack of type-safety. It only implies a lack of a feature.
… where I am using type safety to mean “no runtime type errors/UB manifest”, ie, the property that a sound typesystem would guarantee _if we had one_. You seem to be saying that just because our type system is impoverished, does not make its resulting claim of “program is type safe” any less valid, whereas I am saying “type safety is a semantic property of programs, not of languages, and this value safety idea seems like it’s what PLTers think type safety means”.
It’s a violation of the C semantics to assign the wrong value to an enumeration, so I would say that fact the language doesn’t do anything at all to enforce or check this promotes this beyond “lack of a feature” and straight into “type unsafe”. However, I’d feel less strongly if at least initializers were checked.
As you say, different language design philosophies lead to this, and it’s not surprising. Most of these ideas came _after_ C anyway!
phone dying… no response soon.