> but the main reason why people want them is to fix the error handling
Why do you think so? Maybe I'm an odd case, but my main use case for enums is for APIs and database designs, where I want to lock down some field to a set of acceptable values and make sure anything else is a mistake. Or for state machines. Error handling is manageable without enums (but I love Option/Result types more than Go's error approach, especially with the ? operator).
> but I love Option/Result types more than Go's error approach
The thing is, these don't add much on their own. You'd have to bring in pattern matching and/or a bunch of other things* that would significantly complicate the language.
For example, with what's currently in the language, you could definitely have an option type. You'd just be limited to roughly an api that's `func (o Option[T]) IsEmpty() bool` and `func (o Option[T]) Get() T`. And these would just check if the underlying point is nil and dereference it. You can already do that with pointers. Errors/Result are similar.
A `try` keyword that expands `x := try thingThatProducesErr()` to:
x, err := thingThatProducesErr()
if err != nil {
return {zero values of the rest of the function signature}, err
}
Might be more useful in go (you could have a similar one for pointers).
* at the very least generic methods for flat map shenanigans
Accessing the value is then forced to look like this:
if value, ok := option.Get(); ok {
// value is valid
}
// value is invalid
Thus, there's no possibility of an accidental nil pointer dereference, which I think is a big win.
A Result type would bring a similar benefit of fixing the few edge cases where an error may accidentally not be handled. Although I don't think it'd be worth the cost of switching over.
It's better because you do not need to remember to check for nil, the compiler will remind you every time by erroring out until you handle the second return value of `option.Get()`.
> Of course, this is often left out, but you can just as easily do:
Unfortunately it gets brought up pretty much every time in these discussions.
Deliberate attempts to circumvent safety are not part of the threat model. The goal is prevention of accidental mistakes. Nothing can ultimately stop you from disabling all safeties, pointing the shotgun at your foot and pulling the trigger.
> my main use case for enums is for APIs and database designs, where I want to lock down some field to a set of acceptable values and make sure anything else is a mistake
Then what you are really looking for is sum types (what Rust calls enums, but unusually so), not enums. Go does not have sum types, but you can use interfaces to archive a rough facsimile and most certainly to satisfy your specific expectation:
type Hot struct{}
func (Hot) temp() {}
type Cold struct{}
func (Cold) temp() {}
type Temperature interface {
temp()
}
func SetThermostat(temperature Temperature) {
switch temperature.(type) {
case Hot:
fmt.Println("Hot")
case Cold:
fmt.Println("Cold")
}
}
Enums and sum types seem to be related. In the code you wrote, you could alternatively express the Hot and Cold types as enum values. I would say that enums are a subset of sum types but I don't know if that's quite right. I guess maybe if you view each enum value as having its own distinct type (maybe a subtype of the enum type), then you could say the enum is the sum type of the enum value types?
They can certainly help solve some of the same problems. Does that make them related? I don't know.
By definition, an enumeration is something that counts one-by-one. In other words, as is used in programming languages, a construct that numbers a set of named constants. Indeed you can solve the problem using that:
type Temperature int
const (
Hot Temperature = iota
Cold
)
func SetThermostat(temperature Temperature) {
switch temperature {
case Hot:
fmt.Println("Hot")
case Cold:
fmt.Println("Cold")
}
}
But, while a handy convenience (especially if the set is large!), you don't even need enums. You can number the constants by hand to the exact same effect:
type Temperature int
const (
Hot Temperature = 0
Cold Temperature = 1
)
func SetThermostat(temperature Temperature) {
switch temperature {
case Hot:
fmt.Println("Hot")
case Cold:
fmt.Println("Cold")
}
}
I'm not sure that exhibits any sum type properties. I guess you could see the value as being a tag, but there is no union.
const (
Hot Temperature = 0
Cold Temperature = 1
)
Isn't really a good workaround when lacking an enumeration type. The compiler can't complain when you use a value that isn't in the list of enumerations. The compiler can't warn you when your switch statement doesn't handle one of the cases.
Refactoring is harder - when you add a new value to the enum, you can't easily find all those places that may require logic changes to handle the new value.
Enums are a big thing I miss when writing Go, compared to when writing C.
> Isn't really a good workaround when lacking an enumeration type.
Enumeration isn't a type, it's a numbering construct. Literally, by dictionary definition. Granted, if you use the Rust definition of enum then it is a type, but that's because it refers to what we in this thread call sum types. Rust doesn't support "true" enums at all.
> The compiler can't complain when you use a value that isn't in the list of enumerations.
Well, of course. But that's not a property of enums. That's a property of value constraints. If Go supported value constraints, then it could. Consider:
type Temperature 0..1
const (
Hot Temperature = 0
Cold Temperature = 1
)
Then the compiler would complain. Go lacks this in general. You also cannot define, say, an Email type:
type Email "{string}@{string}"
Which, indeed, is a nice feature in other languages, but outside of what enums are for. These are separate concepts, even if they can be utilized together.
> Enums are a big thing I miss when writing Go, compared to when writing C.
Go has enums. They are demonstrated in the earlier comment. The compiler doesn't attempt to perform any static analysis on the use of the use of the enumerated values because, due to not having value constraints, "improper" use is not a fatal state[1] and Go doesn't subscribe to warnings, but all the information you need to perform such analysis is there. You are probably already using other static analysis tools to assist your development. Go has a great set of tools in that space. Why not add an enum checker to your toolbox?
[1] Just like it isn't in C. You will notice this compiles just fine:
> but all the information you need to perform such analysis is there.
No, it isn't, unlike C, in which it is. The C compiler can actually differentiate between an enum with one name and an enum with a different name.
There's no real reason the compiler vendor can't add in warnings when you pass in `myenum_one_t` instead of `myenum_two_t`. They may not be detecting it now, but it's possible to do so because nothing in the C standard says that any enum must be swappable for a different enum.
IOW, the compiler can distinguish between `myenum_one_t` and `myenum_two_t` because there is a type name for those.
Go is different: an integer is an integer, no matter what symbol it is assigned to. The compiler, now and in the future, can not distinguish between the value `10` and `MyConstValue`.
> Just like it isn't in C. You will notice this compiles just fine:
That's about as far as you can get from "compiling just fine" without getting to "doesn't compile at all".
And the reason it is able to warn you is because the compiler can detect that you're mixing one `0` value with a different `0` value. And it can detect that, while both are `0`, they're not what the programmer intended, because an enum in C carries with it type information. It's not simply an integer.
It warns you when you pass incorrect enums, even if the two enums you are mixing have identical values. See https://www.godbolt.org/z/eT861ThhE ?
type E int
const (
A E = iota
B
C
)
enum E {
A,
B,
C
}
What is missing in the first case that wouldn't allow you to perform such static analysis? It has a keyword to identify initialization of an enumerated set (iota), it has an associated type (E) to identify what the enum values are applied to, and it has rules for defining the remaining items in the enumerated set (each subsequent constant inherits the next enum element).
That's all C gives you. It provides nothing more. They are exactly the same (syntax aside).
> It warns you
Warnings are not fatal. It compiles just fine. The Go compiler doesn't give warnings of any sort, so naturally it won't do such analysis. But, again, you can use static analysis tools to the same effect. You are probably already using other static analysis tools as there are many other things that are even more useful to be warned about, so why not here as well?
> enum in C carries with it type information.
Just as they do in Go. That's not a property of enums in and of themselves, but there is, indeed, an associated type in both cases. Of course there is. There has to be.
> What is missing in the first case that wouldn't allow you to perform such static analysis?
Type information. The only type info the compiler has is "integer".
> It has a keyword to identify initialization of an enumerated set (iota),
That's not a type.
> it has an associated type (E)
It still only has the one piece of type information, namely "integer".
> and it has rules for defining the remaining items in the enumerated set
That's not type information
> That's all C gives you.
No. C enums have additional information, namely, which other integers that type is compatible with. The compiler can tell the difference between `enum daysOfWeek` and `enum monthsOfYear`.
Go doesn't store this difference - `Monday` is no different in type than `January`.
> Warnings are not fatal.
Maybe, but the warning tells you that they types are not compatible. The fact that the compiler tells you that the types are not compatible means that the compiler knows that the types are not compatible, which means that the compiler regards each of those types as separate types.
Of course you can redirect the warning to /dev/null with a flag, but that doesn't make the fact that the compiler considers them to be different types go away.
Whether you like it or not, C compilers can tell the difference between `Monday` and `January` enums. Go can't tell the difference between `Monday` and `January` constants. How can it?
Nobody said it was. Reaching already? As before, enums are not a type, they are a numbering mechanism. Literally. There is an associated type in which to hold the numbers, but that's not the enum itself. This is true in both C and Go, along with every other language with enums under the sun.
> The compiler can tell the difference between `enum daysOfWeek` and `enum monthsOfYear`.
Sure, just as in Go:
type Day int
const (
Monday Day = iota
Tuesday
// ...
)
type Month int
const (
January Month = iota
February
// ...
)
func month(m Month) {}
func main() {
month(January) // OK
month(Monday) // Compiler error
}
> Go doesn't store this difference - `Monday` is no different in type than `January`.
Are you, perhaps, mixing up Go with Javascript?
> How can it?
By, uh, using its type system...? A novel concept, I know.
Regardless of the rest of this thread, I appreciate this comment. It helped crystalize 'enum' in the context of 'sum' for me in a way that had previously been lacking. Thanks.
Traditionally, enums have been a single number type with many values (initialized in a counted one-by-one fashion).
Rust enums are as you describe, as they accidentally renamed what was historically known as sum types to enums. To be fair, Swift did the same thing, but later acknowledged the mistake. The Rust community doubled down on the mistake for some reason, now gaslighting anyone who tries to use enum in the traditional sense.
At the end of the day it is all just 1s and 0s. If you squint hard enough, all programming features end up looking the same. They are similar in that respect, but that's about where the similarities end.
Couldn't the zero value be nil? I get that some types like int are not nil-able, but the language allows you to assign both nil and int to a value of type any (interface{}), so I wonder why it couldn't work the same for sum types. i.e. they would be a subset of the `any` type.
Said "requirement" is only a human construct. The computer doesn't care.
If the humans choose to make an exception for that, it can be done. Granted, the planning that has taken place thus far has rejected such an exception, but there is nothing about Go that fundamentally prevents carving out an exception.
When enums make it from the language to the db, things are now brittle and it only takes one intern to sort the enums alphabetically to destroy the look up relations. An enum look up table helps, but now they are not enums in the language.
Why do you think so? Maybe I'm an odd case, but my main use case for enums is for APIs and database designs, where I want to lock down some field to a set of acceptable values and make sure anything else is a mistake. Or for state machines. Error handling is manageable without enums (but I love Option/Result types more than Go's error approach, especially with the ? operator).