Safer Enums in Go

95
21
13 days
(npf.io)
by tempodox

Comments

tempest_
13 days

Having learned some Rust in the last few years (coming from python) I now want sum types everywhere. Anything less is a disappointment.

vlunkr
12 days

Everyone check off "immediately making a Golang conversation about Rust"'on your HN bingo.

gnulinux
12 days

I'm absolutely baffled by this attitude in literally every single programming language thread in HN.

PLT and PL Design don't live in vacuum, it's a vast field that has been developing exponentially in the last years (just like any other CS discipline).

Any development in Haskell, Rust etc... Or other relatively mainstream frontier languages are fundamentally relevant to any other language's design. Period. All this bickering about "Rust in every thread" is excruciatingly misguided. Lessons we can learn from Rust will be highly influential on other languages and it's worth discussing these. Please keep this in mind before complaining for the 999th time.

vlunkr
12 days

It's fine to compare language design, and it can make for interesting discussion, but that's not what HN has been lately. It's just "Rust is better because it has x feature." If I wanted that level of discourse I'd go to /r/ProgrammerHumor and find the 10,000th "Python is better than Javascript" meme.

Is my comment any better? No probably not.

gnulinux
12 days

But this particular chain started as a discussion of how sum types are missed in every other language. Isn't that a very important observation?

Every programming language is an experiment: it tests whether an well-defined, particular model of computation ends up having advantages on human engineering results. Maybe it causes fewer bugs. Maybe it makes development faster. Maybe it makes certain features easier to implement.

So whether one language has a feature that's highly missed in other languages is the very basis of the debate.

I personally am not a Rust programmer, but not knowing Rust, but knowing another programing language that has sum types, it seems understable to me that it worths mentioning this for every language that doesn't have sum types. And when you do do that you might as well mention Rust as an example. (Maybe because it's more mainstream?)

metadat
12 days

It's a common phenomenon on HN, typically referred to as "a casual dismissal".

stouset
12 days

Are you really surprised or upset to see those types of comments when the article itself is fundamentally about trying to adapt Go to become more Rust-like?

Personally I think people can't help but notice the trend of Go users rediscovering bug-by-bug and frustration-by-frustration why Rust was designed the way it is.

morelisp
12 days

> the article itself is about trying to adapt Go to become fundamentally more Rust-like?

The article does not even mention Rust. Not every attempt to make something 'safer' is an attempt to make it 'fundamentally more Rust-like'. (And you're also misusing the word "fundamental" - even if we grant this is "Rust-like" this is adapting their code to make their Go programs practically more Rust-like.)

stouset
12 days

I should have said "the article itself is fundamentally about…", and not "Go to become fundamentally more…". Apologies. I've edited my comment.

pjmlp
11 days

Not only Rust, there is plenty of programming language design experience between 1950 and 2009, but naturally that was PhD stuff not worth taking into consideration.

vlunkr
12 days

No, I'm not surprised, that's why it's on the bingo card. They are very different languages, so endlessly comparing them is tiresome.

tempest_
12 days

Aw come on there was a whole two hours that this post was about Go before I showed up.

You gotta give the people what they want!

politician
12 days

The ‘safe’ keyword in the title is a Siren’s song to certain types of Rustaceans, so it’s not surprising.

tialaramex
12 days

I was reading this because hey, safer enums. But I agree, Sum types are a must have.

Particularly, decryption should be Authenticated, and so the correct return type of a decrypt function is a Sum type, and if you don't have Sum types you can't do that.

alkonaut
13 days

The C# of my day job often feels like banging rocks together with its c-style enums and lack of sum types. But there is always an article about go that makes you appreciate what you do have.

munchler
13 days

> The C# of my day job often feels like banging rocks together with its c-style enums and lack of sum types.

Hah, I agree. I switched over to F# and never looked back. It plays nicely enough with C# that my colleagues couldn't stop me (but didn't join me either).

> But there is always an article about go that makes you appreciate what you do have.

Yes. Go seems like a time machine back to about 1985.

OliverGilan
13 days

Starting a new project and trying to decide between Go and F# of all things. Go seems fast and very widely used which means resources and help is widely available and it’ll be easier to find contributors. Meanwhile coming from the C# world at my job I really like the syntax of F# from what I’ve seen so far. The type system seems incredibly expressive in a way Go’s isn’t, access to C# libraries means I can do pretty much anything I’ll need to, and I enjoy FP paradigms even though I’m not looking for full FP (I generally like loops and escape hatches from FP if I need to do something specific). What’s your experience with F#? I’m worried about support and finding other people familiar with the language, not a huge fan of nuget, and I prefer to use VSCode but C# has been impossible to program in VSCode imo.

munchler
13 days

I’ve had a great experience with F#. It’s now my preferred language in almost all cases, and it’s plenty fast enough for the vast majority of applications. I use Visual Studio and the UX is excellent. (A lot of F# developers use Paket instead of Nuget.)

MS seems committed to F# as at least a source of future C# language enhancement ideas (although C# will never fully catch up). There’s also a vibrant F# community on Stack Overflow, Reddit, etc. I never have trouble getting help if I need it.

pjmlp
11 days

Unfortunely switching to F# also means throwing away what actually makes .NET relevant in terms of end to end developer experience, and the Redmond overloads don't seem to care that much to change that, as the C in CLR seems to refer to C# and no longer to Common, in the ways VS and VS4Mac are designed.

mastax
12 days

I do like the way they added type-switch and switch expressions to get some of the sum-type ergonomics out of the existing object oriented type system.

throwaway858
13 days

C# has sum types

masklinn
13 days

C# does not have sum types.

It has "enumeration types" but their behaviour is exactly that of C's, with various additional conveniences but critically (and unlike Java for instance) no more type-safety.

alkonaut
13 days

Not without squinting really hard and making some OO monstrosity with internal impls of an outer abstract type it doesn’t. And that’s not really language support it’s banging your rocks together really hard trying to bend it into being ML.

ebingdom
13 days

I'm pleasantly surprised by these comments. A few years ago people were so entrenched in their object-oriented upbringing that sum types were considered "weird functional stuff" and thus unsuitable for "real" work (what a shame to have that attitude). People are finally coming around to good ideas from other paradigms.

Rust's popularity is certainly helping that, though I wish they hadn't repurposed the existing word "enum" to serve as a new synonym for a concept with an existing name ("algebraic data types").

epage
13 days

> A few years ago people were so entrenched in their object-oriented upbringing that sum types were considered "weird functional stuff"

A step further: I was hearing talk of enums being a code smell and that they should all be replaced with interfaces as they represented alternative implementations and you might want to be more extensible or insert a mock for testing

As an aside, tagged unions weren't a big deal to me (though I thought it neat to merge tagged unions with enum constants) but what blew me away was when I learned they could have methods and implement traits. Would love it if Rust took it a step further and made the variants types. There are cases were id like to have builder methods on variants. The closest I can get is having an explicit struct and implementing From to the enum. Also wish there was variant visibility so I wouldn't have to wrap my enums in structs to hide the variants.

nicoburns
13 days

“algebraic data types” includes product types (aka structs, classes and tuples) as well as sum types (aka enums and probably other names)

ebingdom
13 days

Rust's "enums" also include products. Each case can have multiple arguments. That's a sum of products. "Algebraic data type" is the proper term for what Rust calls "enums", since they encompass both sums and products.

(It is also true that structs are products. But that isn't relevant.)

kibwen
12 days

Rust's syntax makes plenty of concessions to familiarity, as a way of making up for the much more important and irreducible unfamiliarity that it brings to the table (e.g. ownership). Originally Rust used the keyword `tag` here (for "tagged unions"), then vacillated between `enum` and `union` for years and years prior to 1.0.

morelisp
13 days

I also like sum types, but I have intense whiplash from "this language is AN ABSTRACT MESS that needs a PhD in type theory to use productively" five years ago, to today's "this language is COMPLETELY USELESS because it doesn't have them".

sidlls
12 days

I can't count the number of C++ codebases I've worked on where either myself or some other engineer implemented an ad-hoc, incomplete "sum type-like" enumeration system, e.g. in linting, pre-compilers, etc. In my case, I've done it without even knowing what these were--my background is physics, not computer science. The concept isn't terribly difficult to understand, even if the jargon name for it isn't widely known, or is associated with more type-theory oriented languages.

ravi-delia
12 days

I don't think anyone ever struggled with sum types (or at least, I hope not), they associated them with things like Haskell's monads, which are less commonly accessible (though they're undeserving of their reputation too). The hate was a little silly, but algebraic data types are clearly just an extension of the concepts everyone already knew.

morelisp
12 days

> I don't think anyone ever struggled with sum types... algebraic data types are clearly just an extension of the concepts everyone already knew.

You're definitely in a bubble.

nyanpasu64
12 days

Rust is an abstract mess that needs a PhD (or type theory or guess-and-check) to use productively; it's more to do with overuse of generics and wildcard trait impl and trait resolution and higher-order functions and lifetime HKTs and reverse type inference, than sum types. Rust enums require less generics to use productively than C++ std::variant (which requires std::get_if or std::visit), because enums have language-level `match` and `if let` syntax as a fundamental binding block which can take the place of combinator method chaining (but the community instead sees it as an opportunity to invent and add even more methods).

cwzwarich
13 days

> Rust's popularity is certainly helping that, though I wish they hadn't repurposed the existing word "enum" to serve as a new synonym for a concept with an existing name ("algebraic data types").

What keyword would you prefer they use instead, "datatype" like SML?

kibwen
12 days

If I were designing a language I'd call them or-types, to indicate a type that is "this OR that", as distinct from and-types, which are "this AND that". The product vs. sum terminology strikes me as needlessly forbidding, although it is undoubtedly cute that one can extrapolate algebraic data types to calculus and such.

CraigJPerry
12 days

>> Rust's popularity

Is Rust actually popular? I know that question is inviting downvote oblivion on here but on the off chance of attracting a thoughtful comment (and because I secretly want to really fall for rust, i love the idea and think we badly need the next step on from C-based foundations, but the pragmatic side of me just rolls around laughing at rust) here’s where i’m coming from:

    - There’s not much in the way of job postings
    - There’s no halo product yet, servo appears to have gone nowhere? If it makes it into Linux *that* will launch the lang for sure
    - There’s no halo company championing it, mozilla has never moved away from churning out c++ and js primarily, microsoft made some noise but then crickets
    - it’s been around about as long as Go yet its google trends search traffic is around 1/16th that of Go’s
    - The only large rust code bases on github are servo and the rust compiler/stdlib itself
The only metric i can find where rust scores well is the stack dev survey and the number of tutorials posted online. If there was actual traction alongside these two metrics, rust would be a rocketship. But without any obvious successes in the world so far, it’s beginning to look like a handy way to identify devs susceptible to stockholm syndrome.

There’s all sorts of weird signs in the community that makes me think the median rust “user” has a rust career something like:

    1. Downloaded rust
    2. Installed the wrong plugin in VSCode (why is the correct plugin - rust analyzer - so low on downloads and votes, yet the dead plugin is still getting tons of traffic? Is noone actually reading the documentation?)
    3. Followed a hello world tutorial, marveled at the genuinely awesome compiler errors
    4. Full of enthusiasm, started a side project and… gave up after a few nights but who knew, they wanted to solve a problem not add lifetimes onto the existing stack of problems to wrestle with.
AFAICT the future’s not looking bright for rust right now. It’s coasting on a wave of enthusiasm from relatively junior developers who aren’t making impressive things with the language yet.

This is a strongly typed language with lifetimes understood by the compiler - that’s a rocket suit compared to a language like the tire fire that is javascript. Enums? Pah excuse me while i pass a string literal…

With this rust exo-skeleton surrounding a developer, it should be unlocking the ability to manage 10million+ LOC code bases like it’s hello world.

stouset
12 days

Reframing your question, it's incredibly impressive how popular Rust has become without a halo product, halo company, etc. Rust proponents will readily argue that Go is only as popular as it is because of being championed by Google. Meanwhile Rust has had to grow to the place where it is organically.

My personal belief is that over time Rust will inevitably eclipse C and C++. But it will take time. C and C++ will never fully go away, but projects that would have been written in them will be over time be preferentially written in Rust. The Linux kernel starting to write drivers in it is a harbinger of this, and once that effort has made it into released kernels we'll quickly start to see other parts of the kernel adopt Rust.

> AFAICT the future’s not looking bright for rust right now. It’s coasting on a wave of enthusiasm from relatively junior developers who aren’t making impressive things with the language yet.

Frankly this is my perspective of Go, which is (from my vantage point) mostly popular amongst junior developers. Rust is not even remotely marketed towards novices, and in many of the communities it's openly acknowledged that it's perhaps not the best choice for someone just setting out. C and C++ programmers are the ones primarily flocking toward Rust, while Python, Ruby, and Node developers are the ones who are primarily adopting Go as they're starting to see the value of static types, even as anemic as Go's type system is.

Anecdotally I know many, many engineers who have grown disillusioned with Go once they've worked on larger projects and had to repeatedly reinvent wheels that other languages provide as built-in features. Many of those engineers have gone on to champion Rust, and the flow of engineers between those two languages from my point of view heavily favors Rust.

But without a big champion like Microsoft, Google, or Sun/Oracle, this growth is going to happen organically and take time. Maybe there will be an enormous driver like Kubernetes for Go, Rails for Ruby, or ML for Python, but even without that I think Rust will gradually continue to snowball.

CraigJPerry
12 days

>> incredibly impressive how popular Rust has become

But it hasn’t become popular yet, it’s not competing with something like Go or C++, it’s competing with Erlang or Clojure for popularity

https://madnight.github.io/githut/#/pull_requests/2021/4

P.s. thumbs up, that was a thoughtful reply! But the predictable down votes arrived too :-)

pjmlp
11 days

Mozilla and Firefox.

Or do you think anyone would care if it was some random joe/jane posting about this Rust language they were developing.

Tozen
13 days

One of the "offshoot" or inspired by languages of Golang, is Vlang (https://vlang.io/), and the other is Odin. Vlang has both sum types and real enums. It had those, including generics, for quite a while.

tandr
12 days

From a first glance, Odin looks like a nice language to use! Looks like they have fixed ocd-causing idiosyncrasies of Go - "for value, index" and not the other way around, "defer" belongs to surrounding scope, and not the whole function.

Thank you!

Tozen
12 days

Your welcome. I keep my eye on both Odin and Vlang. And if a person has experience with Go, they offer some different features and freedoms that might have been wanted, but couldn't get. Additionally, it's not too difficult to switch among the languages as well, if one is strongly familiar with one of them.

tandr
3 days

Problem is to switch people around you to use something else. Second company, second fight Java vs Go, and I am loosing again... Makes me wonder if all my efforts to learn something new are for nothing, for no real benefit.

Splizard
13 days

Since 1.18 was released it's now trivial to implement compile-time (safe) 'sum types' in Go. They're fully supported.

It's about 300loc to implement and then sum types can be defined declaratively just like any other type (and without any boilerplate).

There hasn't been any fanfare about this capability because it doesn't require any special syntax or new keywords. Just know that the suggestion that "Go 1.18+ doesn't support sum types and/or enums", is very wrong.

Strum355
13 days

Not going to link it anywhere? I have some doubts given how widely it wouldve been shared if the claim is actually true, and your not linking of an example kinda just furthers that

Splizard
13 days
esprehn
13 days

That code appears to be using reflection to build up runtime tables?

https://github.com/qlova/tech/blob/c6379c9c32e5b7b2973bc02ba...

https://github.com/qlova/tech/blob/c6379c9c32e5b7b2973bc02ba...

That's a big difference from other languages where this is all handled at compile time.

suremarc
12 days

This is cool, but it doesn't seem like Switch is implemented for Int, and the use of runtime reflection limits the contexts in which this implementation can be used, compared to a language construct that gets compiled into efficient machine code. I wouldn't want this to be endemic in my codebase or my dependencies.

Splizard
12 days

Enum supports an exhaustive Switch as well, here's an example based on the blog post: https://go.dev/play/p/Ef78vLyw33g

arriu
12 days

Is it just me or is the syntax on these worse than the issues it’s solving?

masklinn
12 days

It's not you, this is a worse version of std::variant, which isn't exactly great in the first place.

parhamn
13 days

I hope at some point golang drops its "too complicated" defense against doing more sensible things that aren't _that_ complicated but add a lot of safety. I say this as someone who loves go and has written A LOT of it.

More and more people will use Rust and Typescript and realize what a joke go's type system is compared to those. But you don't need to go as far as those languages have -- lacking sum types, exhaustive switches, discriminant unions, proper enums, unchecked nils, etc is just lazy. Most reasonably sized software projects end up having some ad-hock crappy workaround to any one of these issues.

After years of waiting for generics. We got the most stunted and simplistic implementation of generics imaginable. You can't even do something as simple function chaining.

morelisp
12 days

> You can't even do something as simple function chaining.

You make this sound like something got lazy about rather than something with major implications for the language semantics/runtime. Serious question: Would you prefer a) the current generics implementation, b) a runtime capable of code generation, c) generics cannot participate in interface implementation, d) some other option?

vocram
13 days

> The one tiny drawback is that the globals have to be variables instead of constants, but that’s one of those problems that really only exists in the theoretical realm. I’ve never seen a bug from someone overwriting a global variable like this, that is intended to be immutable.

Even if it’s a silly bug to make, it really annoys me a lot knowing you could change an enum value. Is it so hard for Go to support consts of any-type?

ebingdom
13 days

Although an improvement over C, Go seems to have the same unofficial motto: "I don't need you to X, just trust me to Y", as in:

1) I don't need you to prevent me from mutating this variable, just trust me to not mutate it.

2) I don't need you to ensure I handle every case of this enum, just trust me to update all the relevant code whenever I add a new case.

3) I don't need you to prevent me from reading the result without checking the error, just trust me to always handle errors.

4) I don't need you to track nullability in the type system, just trust me not to dereference null pointers.

5) I don't need you to let me hide the default constructor, just trust me to always use the smart constructor that establishes the invariants that I need.

6) Until recently: I don't need you to let me abstract away this type, just trust me to keep all the copies of the function/type in sync.

This philosophy may make sense for a low-level language like C, but not for a high-level language for building applications, services, etc. It amazes me that people voluntarily give up the guarantees of other languages in favor of Go for these use cases. It's almost as if the language wants bugs to slip into your code.

masklinn
13 days

> This philosophy may make sense for a low-level language like C

Even for a low level language like C it really doesn't, it makes the implementation easier but all the things you mention (and more) are useful both to ensure the foundations are solid (security, correctness) and to make it fast: the weak type system and feeble guarantees of C are why compilers lean so much on UBs to infer constraints they can then optimise based on.

cesnja
13 days

There are linters available that help you do the "just trust me to" part.

Edit: https://github.com/nishanths/exhaustive is available in golangci-lint

mseepgood
13 days

> but not for a high-level language for building applications, services, etc

That's wrong, otherwise dynamically typed languages like Python wouldn't be so popular.

ebingdom
13 days

Just because something is popular doesn't mean it's right.

kortex
12 days

I think python is finding the loosey-goosey dynamic everything makes for bad application scalability, both for maintainability but also performance.

Python with types, on the other hand, is pretty great for catching bugs before runtime.

pjmlp
11 days

Given that it shares common designers, augmented with their Plan 9 and Inferno experience, it isn't surprising.

At least they accepted some Oberon-2 influence as well.

morelisp
13 days

While it would be nice to also have some way to mark some stored value as immutable, the reason it doesn't support "consts of any type" is because consts in Go are specifically not an extant value. They're a syntactic object more like a macro.

cletus
13 days

I like the simplicity and how opinionated Go is. There's typically one way to do things and that's incredibly useful.

But lack of real enums for me falls into the "How is this a thing?" bucket.

Like Java introduced what are (IMHO) a pretty good enum almost 20 years ago. Java enums are classes, basically. They can have state and methods. Super-useful.

I like Hack but Hack's enums are a little weird in the sense that you have a bit of an odd syntax to indicate if automatic coercion is allowed.

I like the idea in this post as being better than straight strings or ints but there are still 2 downsides:

1. As a variable (not a constant) it gives the compiler less options on how this can be optimized; and

2. (This is the big one) One of the best things about enums is using them in switch statements such that adding a new value will cause a compiler error (assuming you don't use a default branch, which should be discouraged for this).

Rust enums and match expressions, for example, cover all of this.

cube2222
13 days

Not that it's a great solution, but there's https://github.com/BurntSushi/go-sumtype for (2).

majewsky
13 days

These sumtypes don't mesh well with JSON/YAML/etc. serialization though. If you have

  type MySumType interface {
    sealed()
  }
like in the code example for go-sumtype, serialization is easy enough:

  type MySumType interface {
    json.Marshaler
    sealed()
  }
But deserialization runs into a roadblock, because you cannot implement interfaces like json.Unmarshaler on an interface type. Something like

  type MySumType interface {
    json.Marshaler
    json.Unmarshaler
    sealed()
  }
will not work: Given `var s MySumType`, json.Unmarshal() will croak because it cannot call UnmarshalJSON without knowing the concrete type of `s`. So for deserialization, you'd need an extra wrapper type that provides the json.Unmarshaler implementation.
catlifeonmars
12 days

> But deserialization runs into a roadblock, because you cannot implement interfaces like json.Unmarshaler on an interface type.

In addition to introducing the wrapper type, this could also be fixed by using a JSON decoder implementation that supports type/location hooks. In other words, this is more of a limitation of the custom unmarshaling mechanism chosen by the builtin encoding/json package.

masklinn
12 days

> So for deserialization, you'd need an extra wrapper type that provides the json.Unmarshaler implementation.

You'll probably need something along those lines either way, for the simple reason that there is no single mapping between sum types and JSON (or equivalent / similar)

For instance serde provides 3 different serialisation schemes, and once in a while you still need a bespoke deserialiser.

Splizard
13 days

Right, the builtin switch statement is not exhaustive but you can quite easily create a Switch method on your enum type in Go 1.18+ that is exhaustive.

henvic
13 days

> The one tiny drawback is that the globals have to be variables instead of constants, but that’s one of those problems that really only exists in the theoretical realm.

I'd argue that needing this enums safety so bad that you go out of your way to create a struct with a protected field also lies in the same category of something you need that is mostly in the theoretical realm too.

Don't get me wrong, sometimes you want that kind of protection indeed and it's easy to mess up if you need to add validation everywhere. However, most times you can get away with just rethinking your code.

More importantly, I think the downside of using variables rather than constants makes it less safe than the alternative of relying on you always having to do validation.

tedunangst
12 days

The alternative of using plain strings is prone to typos, which variable names are not (or less so).

henvic
12 days

uh? I'm saying "just use regular constants". The risk of typos is when you're defining them, just as much as the risk of defining bad variable values.

vlunkr
12 days

I had the same thought. You're just swapping one non-issue for another.

sidlls
12 days

"I’ve never seen a bug from someone overwriting a global variable like this, that is intended to be immutable."

It can cause headaches in test code (where developers try to override the package variable value for test cases, and forget to restore/change it back when done, etc., i.e. they engage in bad practices that I shouldn't have to catch in a PR). It can easily cause production applications to blow up unexpectedly when the variable is changed in a function and that mistake passes through all the tests and the PRs. This practice is one of the worst in the go community, in my opinion.

The first time I saw these package level globals in go code I cringed. It is an absolutely terrible design decision to not have package level immutables that aren't PODs.

catlifeonmars
12 days

> It is an absolutely terrible design decision to not have package level immutables that aren’t PODs.

FWIW here is a proposal to expand const in Go: https://github.com/golang/go/issues/21130

mieubrisse
13 days

We tried out struct-based enums like this for a while and they're definitely better than the vanilla version, but they still have the problem of "how do you iterate over all the values?" and "how do you create an enum value from a string?". We chanced upon https://github.com/dmarkham/enumer a few months ago and have been very happy so far.

mariusor
13 days

I think sometimes it's important to accept the shortcomings of the language we're using instead of chasing high overhead abstractions like the one presented in the OP and in the enumer package.

I hate that go doesn't have enums, I hate that it doesn't have tagged structs, but if I spend my time trying to massage the language into simulacra of them, I'm losing time that I could spend writing productive code, or I muddle the waters for future developers that will have to deal with those abstractions in the future. Overall I think it's not worth it, outside of highly theoretical implementations like this, that can exist independent of code that actually does things.

xienze
13 days

> The one tiny drawback is that the globals have to be variables instead of constants, but that’s one of those problems that really only exists in the theoretical realm. I’ve never seen a bug from someone overwriting a global variable like this, that is intended to be immutable.

Eh yeah, but if you’re writing a library you have to assume someone will do it, because it’s possible to do. Which makes it a non-starter IMO.

lenkite
12 days

I prefer immediately initialized struct vars

    var Animal = struct {
        Cat          string
        Dog          string
        Dodo         string
    } {
       "Cat",
       "Dog",
       "Dodo",
    }
xaduha
12 days

Ditto, surely this is better than what this blog post suggests?

    var Constants = struct {
        FooBar          string
        FizzBuzz        string
    } {
       "FooBar",
       "FizzBuzz",
    }
If you must have

    type FlagID string
then this should work also

    var Constants = struct {
        FooBar          FlagID
        FizzBuzz        FlagID
    } {
       "FooBar",
       "FizzBuzz",
    }
atombender
12 days

That's not a type. That's just some string constants. You cannot define a variable of type Animal, which is kind of the point of enums.

Worse, they're not constants, they're mutable. So how is that different from this?

  var (
    Cat  = "Cat"
    Dog  = "Dog"
    Dodo = "Dodo"
  )
xaduha
12 days

In my example it would be accessible as Constants.FooBar and it would be of a type FlagID just as required. What else do you need? Mutability was dismissed in the blog post too and if you're modifying something called Constants.FooBar it's kinda on you anyway.

goodoldneon
12 days

Your example wouldn't work for function param types, right? The following code compiles even though I'm not passing a valid flag ID:

  type FlagID string
  
  var Constants = struct {
    FooBar   FlagID
    FizzBuzz FlagID
  }{
    "FooBar",
    "FizzBuzz",
  }
  
  func p(flag FlagID) {
    fmt.Println(flag)
  }
  
  func main() {
    p("hi")
  }
xaduha
12 days

You're right and OP mentioned it at the start too. I guess your editor would show you what is expected there and that should be good enough for most cases.

If it is some sort of a library though, then there should be a better way to do this that to pass some enums as parameters to functions. But yes, it is a shortcoming that Go should probably fix.

goodoldneon
11 days

This type of thing has been surprising to me as I've learned Go. It seems like Go makes a great effort around safety in a lot of ways, but sometimes will avoid safety and say "just be careful" instead

atombender
12 days

Your example does not use a type, so I fail to see how it's an equivalent example. What does your example give you aside from "namespacing" the values under a generic struct?

xaduha
12 days

Read the whole comment, FlagID is there and function signature

   func IsEnabled(id FlagID) bool {
would be satisfied just fine
atombender
12 days

I was not replying to your proposal, but to the comment by "lenkite", which only defines "var Animal" and no type.

xaduha
12 days

It is the same thing, just needs a bit of imagination.

edumucelli
13 days

Simplicity is one thing, but lacking things such as Enums is just plain bad. Is this a programming language from the 70's

ebingdom
13 days

I'd say it just as you did but with "algebraic data types" instead of "enums".

politician
12 days

I write a lot of Go. Adding enums nor debugging them has never been a big problem for me.

The way that I tackle the problem is to use iota, and then to emit the numeric value into logs.

Later, if I have some time to get fancy, I’ll implement fmt.Stringer using a switch statement.

Sometimes, I’ll implement a ParseFoo function to convert the string representation into the typed representation.

All of this work takes about 5 minutes, and no thought. That’s what we’re talking about here: ~5 minutes of effort per enum.

IMO, you should be spending far more time thinking about which values to add to the enum than the mechanics of string support.

HL33tibCe7
12 days

Those 5 minutes add up, and in my experience Go newcomers get confused about iota (and the frankly hideous way enums have to be written in general) quite a lot. One of Go’s best aspects is its readability, so it’s unfortunate that it misses the mark here IMO.

Tozen
10 days

I'm really not sure why Go decided against a distinct enum type, instead of repurposing const and getting odd with iota. It comes off as a kind of unnecessary stubbornness, for what should have been easy to add to the language early on.

If you look at one of the Go inspired languages, such as Vlang (that has the enum type), it really should not have become an issue. Now, with Go being advanced in age, it comes off as just odd. It becomes reminiscent of the generics debate, where there was such long denial of it being necessary, then suddenly it is understood it should have been done earlier.

politician
12 days

I think the most distinct enum types I've had in a project was maybe 20? At some point we need to realize that this is bikeshedding.

donatj
13 days

> The one tiny drawback is that the globals have to be variables instead of constants, but that’s one of those problems that really only exists in the theoretical realm. I’ve never seen a bug from someone overwriting a global variable like this

Same goes for the original problem. I’ve heard a lot of people complain about Go’s enum types accepting hard coded values, it’s a conceptual problem, sure, but is it a real problem in practice? It’s not something I would ever do by accident.

I don’t find my self passing random hard coded values to methods. I would have to look up the actual expected enum value, at which point I would think “I should make this a constant” at which point it fails to compile and I find the enum. Even then, in reality I would have noticed the type and the enum long before that. Hard coded values in and of themselves are a code smell. Like sure, in theory a typo could compile, but it seems like a hard mistake for a non-malicious developer to make.

suremarc
13 days

Am I the only one missing in the last example that the zero value, FlagID{}, would be an invalid enum? In which case this is not fundamentally different from the other cases, where you always have to check for the possibility of an invalid value.

titzer
13 days

Virgil has enums where each enum value can have additional fields, so you can write tables and name individual rows. Under the hood the enum value is just an integer and "accessing a field" is just an access of a constant in an array.

It's turned out to be incredibly useful.

https://github.com/titzer/virgil/blob/master/doc/tutorial/En...

mssdvd
13 days

How come the authors did not want to add proper sum-types?

masklinn
13 days

Because they ignored essentially everything we learned after 1990 if not 1980.

Comment was deleted :(
vips7L
13 days

Just another poor language choice by the designers.

assbuttbuttass
13 days

I can understand the theoretical arguments for sum types and closed enums, but I have never once been bitten in practice by this issue.

HL33tibCe7
12 days

The fact that these workarounds are even necessary is completely absurd, and I’m saying that as a dyed-in-the-wool Golang fan

bborud
12 days

I would guess that the cost of not having enums in Go by now is far higher than any cost of adding them. It is one of those things that annoys Go developers on a nearly daily basis.

tonfreed
12 days

I think I'd rather just sanity check an incoming const than trust developers with global state.

All it takes is one idiot to mutate one of those vars and all sorts of hell can break loose.

bravogamma
13 days

C++ programmer here. I was about to start a deep dive into Go today. Hearing there are no enums is giving me serious pause. How do large systems (e.g. Docker?) deal with this?

randomdata
13 days

It has enums, but they are C-style enums rather than sum types. Which stands to reason given that C and Go were born to the same father.

masklinn
13 days

> It has enums, but they are C-style enums rather than sum types.

It doesn't. The closest it has is TFA's second example:

    type FlagID int
    const (
        FooBar FlagID = iota
        FizzBuzz
    )
which is the underlying behaviour of C enums, but not actually that. Crucially it does not at any point hint or imply the set could be in any way closed.

That it doesn't have C-style enums is a point in its favour, I would say. Not much of a point, mind, but still...

randomdata
13 days

> It doesn't.

It does. By definition, an enum is simply a set of named values, which you code example provides, and is behaviourally the same as C.

> it does not at any point hint or imply the set could be in any way closed.

While true, that is a feature of sum types, which I already indicated Go does not have. This is slightly different to enums.

masklinn
13 days

> It does.

No.

> By definition, an enum is simply a set of named values

That is not the definition of "an enum", no. "an enum" does not imply a complete absence of any sort of type safety.

"a C-style enum" might, but that is not the distinguishing characteristic of C-style enums, the lying is, otherwise literally every language which has constants has C-style enums, including every language with sum types.

> and is behaviourally the same as C.

Except for all the ways in which C enums mislead users into assuming any sort of non-existent type-safety guarantees.

There is a critical distinction between "C-style enum", aka a misleading pile of garbage, and "just a bunch of constants". The latter is what Go provides.

> While true, that is a feature of sum types

That is not correct. For instance Java enums are not sum types, but are a type-safe, closed, set of values.

randomdata
13 days

> "an enum" does not imply a complete absence of any sort of type safety.

Sum types are where you expect to find type safety. Some languages call sum types enums, which I expect is where the confusion lies. Neither Go nor C have sum types.

> literally every language which has constants has C-style enums

I think that ultimately that's a fair assertion, but one might argue that the definition does imply some kind of defined set. Both C and Go define syntax for characterizing enums in an established set which the machine can determine where the set boundaries lie. A language with only constants relies on human interpretation of what defines the set.

> For instance Java enums are not sum types, but are a type-safe, closed, set of values.

That's a sum type, also known as a tagged union. Neither Go nor C has those, as has been established multiple times now.

Splizard
13 days

Go 1.18 supports type-safe enums and sum types. It takes 300loc to implement.

morelisp
13 days

In C the enum declaration is - by idiom and also syntax - contained a single declaration. You can construct values outside those, but as a default assumption the compiler/linter has a reasonable list of values to claim "these are the enum values which need handling".

In Go the values are not in a single declaration, and not even in a single TU. The strongest hint that this is an enum and not some other kind of integral newtype is the use of `iota` on the first declaration, but `iota` is also used for other purposes.

You're technically correct that they're "behaviorally the same", but declaration structures matter a lot.

randomdata
13 days

> but `iota` is also used for other purposes.

According to the Go spec[1] iota exists specifically for enum generation. The closure of the enum is also defined. There is no other intended purpose. If a developer has found a new way to overload it in some new way they can equally do the same in C, so that is moot. Enums are not sum types in either language, with no expectation of behaving like sum types, there is no debate about that. They are simply enums.

[1] https://go.dev/ref/spec#Iota

masklinn
13 days

> According to the Go spec[1] iota exists specifically for enum generation.

This assertion is nowhere in the Go spec, not even implicitly.

`iota` is a convenience sequence generator, it is no more "specifically for enum generation" than sqlite's AUTOINCREMENT qualifier is. Or excel's cell-sequencing system.

Further demonstrating that the goal was not to replicate C enums, iota can not be advanced manually save by adding intermediate discarded case.

> If a developer has found a new way to overload it in some new way they can equally do the same in C

No, they can not, literally the second example of your link has to be written and maintained entirely by hand in C, to say nothing of the second example block.

> Enums are not sum types in either language, with no expectation of behaving like sum types, there is no debate about that. They are simply enums.

Go still does not have enums, and your generalised statements about enumerated types remain incorrect regardless of sum types.

randomdata
13 days

> This assertion is nowhere in the Go spec, not even implicitly.

The first line explicitly defines their use for defining enums. The only way it could be more clear would be to define enum, but that should be unnecessary given that the definition is usually well established, although certainly a couple of languages have tried to muddy those waters in recent times.

> `iota` is a convenience sequence generator

More importantly, it defines a set of values. The set is what differentiates an enumeration from a general bag of constants. C reaches for a enum keyword instead, but they achieve the same result of establishing a set.

> Go still does not have enums

It has enums. It doesn't have sum types. Yes, some languages call sum types enums, which is no doubt where your confusion lies.

morelisp
12 days

> The closure of the enum is also defined

No it's not, you can write totally heterogeneous types in a single const block, as well as intermix iotas and literals. iota may count the same sequence spread across multiple types. There's not even a guarantee that the type definition and the const block exist in the same TU!

> There is no other intended purpose.

It is often used to generate private context keys, for example.

nicoburns
13 days

It’s also a feature of enums in languages like Java which don’t provide full sum types.

vips7L
13 days

Java does provide a form of sum types via sealed classes, but they’re definitely not like TypeScript’s:

   type A = string | number;
Splizard
13 days

No, it does. Go 1.18 supports closed enums.