Open derekperkins opened 7 years ago
@derekparker there's a discussion for making a Go2 proposal in #19412
I read through that earlier today, but that seemed more focused on valid types, where this is focused on valid type values. Maybe this is a subset of that proposal, but also is a less far-reaching change to the type system that could be put into Go today.
enums are a special case of sum types where all the types are the same and there's a value associated to each by a method. More to type, surely, but same effect. Regardless, it would be one or the other, sum types cover more ground, and even sum types are unlikely. Nothing's happening until Go2 because of the Go1 compatibility agreement, in any case, since these proposals would, at the very least, require a new keyword, should any of them be accepted
Fair enough, but neither of these proposals is breaking the compatibility agreement. There was an opinion expressed that sum types were "too big" to add to Go1. If that's the case, then this proposal is a valuable middle ground that could be a stepping stone to full sum types in Go2.
They both require a new keyword which would break valid Go1 code using that as an identifier
I think that could be worked around
A new language feature needs compelling use cases. All language features are useful, or nobody would propose them; the question is: are they useful enough to justify complicating the language and requiring everyone to learn the new concepts? What are the compelling use cases here? How will people use these? For example, would people expect to be able to iterate over the set of valid enum values, and if so how would they do that? Does this proposal do more than let you avoid adding default cases to some switches?
Here's the idiomatic way of writing enumerations in current Go:
type SearchRequest int
const (
Universal SearchRequest = iota
Web
Images
Local
News
Products
Video
)
This has the advantage that it's easy to create flags that can be OR:ed (using operator |
):
type SearchRequest int
const (
Universal SearchRequest = 1 << iota
Web
Images
Local
News
Products
Video
)
I can't see that introducing a keyword enum
would make it much shorter.
@md2perpe that isn't enums.
package main
import (
"fmt"
)
func main() {
type SearchRequest int
const (
Universal SearchRequest = iota
Web
)
const (
Another SearchRequest = iota
Foo
)
fmt.Println("Should be false: ", (Web == Foo))
// Prints: "Should be false: true"
}
I totally agree with @derekperkins that Go needs some enum as first class citizen. How that would look like, I'm not sure, but I suspect it could be done without breaking the Go 1 glass house.
@md2perpe iota
is a very limited way to approach enums, which works great for a limited set of circumstances.
int
As soon as you need to represent a string or another type, which is very common for external flags, iota
doesn't work for you. If you want to match against a external/database representation, I wouldn't use iota
, because then ordering in source code matters and reordering would cause data integrity issues.
This isn't just an convenience issue to make code shorter. This is a proposal that will allow for data integrity in a way that is not enforceable by the language today.
@ianlancetaylor
For example, would people expect to be able to iterate over the set of valid enum values, and if so how would they do that?
I think that is a solid use case, as mentioned by @bep. I think the iteration would look like a standard Go loop, and I think they would loop in the order that they were defined.
for i, val := range SearchRequest {
...
}
If Go were to add anything more than iota, at that point why not go for algebraic data types?
By extension of ordering according to the definition order, and following the example of protobuf, I think that the default value of the field would be the first defined field.
@bep Not as convenient, but you can get all these properties:
package main
var SearchRequests []SearchRequest
type SearchRequest struct{ name string }
func (req SearchRequest) String() string { return req.name }
func Request(name string) SearchRequest {
req := SearchRequest{name}
SearchRequests = append(SearchRequests, req)
return req
}
var (
Universal = Request("Universal")
Web = Request("Web")
Another = Request("Another")
Foo = Request("Foo")
)
func main() {
fmt.Println("Should be false: ", (Web == Foo))
fmt.Println("Should be true: ", (Web == Web))
for i, req := range SearchRequests {
fmt.Println(i, req)
}
}
I don't think compile-time checked enums are a good idea. I believe go pretty much has this right right now. My reasoning is
Overall, just a huge -1 for me. Not only doesn't it add anything; it actively hurts.
I think current enum implementation in Go is very straightforward and provides enough compilation time checks. I actually expect some kind of Rust enums with basic pattern matching, but it possibly breaks Go1 guaranties.
Since enums are a special case of sum types and the common wisdom is that we should use interfaces to simulate sum types the answer is clearly https://play.golang.org/p/1BvOakvbj2
(if it's not clear: yes, that is a joke—in classic programmer fashion, I'm off by one).
In all seriousness, for the features discussed in this thread, some extra tooling would be useful.
Like the stringer tool, a "ranger" tool could generate the equivalent of the Iter
func in the code I linked above.
Something could generate {Binary,Text}{Marshaler,Unmarshaler} implementations to make them easier to send over the wire.
I'm sure there are a lot of little things like this that would be quite useful on occasion.
There are some vetting/linter tools for exhaustiveness checking of sum types simulated with interfaces. No reason there couldn't be ones for iota enums that tell you when cases are missed or invalid untyped constants are used (maybe it should just report anything other than 0?).
There's certainly room for improvement on that front even without language changes.
Enums would complement the already established type system. As the many examples in this issue have shown, the building blocks for enums is already present. Just as channels are high level abstractions build on more primitives types, enums should be built in the same manner. Humans are arrogant, clumsy, and forgetful, mechanisms like enums help human programmers make less programming errors.
@bep I have to disagree with all three of your points. Go idiomatic enums strongly resemble C enums, which do not have any iteration of valid values, do not have any automatic conversion to strings, and do not have necessarily distinct identity.
Iteration is nice to have, but in most cases if you want iteration, it is fine to define constants for the first and last values. You can even do so in a way that does not require updating when you add new values, since iota
will automatically make it one-past-the-end. The situation where language support would make a meaningful difference is when the values of the enum are non-contiguous.
Automatic conversion to string is only a small value: especially in this proposal, the string values need to be written to correspond to the int values, so there is little to be gained over explicitly writing an array of string values yourself. In an alternate proposal, it could be worth more, but there are downsides to forcing variable names to correspond to string representations as well.
Finally, distinct identity I'm not even sure is a useful feature at all. Enums are not sum types as in, say, Haskell. They are named numbers. Using enums as flag values, for instance, is common. For instance, you can have ReadWriteMode = ReadMode | WriteMode
and this is a useful thing. It's quite possible to also have other values, for instance you might have DefaultMode = ReadMode
. It's not like any method could stop someone from writing const DefaultMode = ReadMode
in any case; what purpose does it serve to require it to happen in a separate declaration?
@bep I have to disagree with all three of your points. Go idiomatic enums strongly resemble C enums, which do not have any iteration of valid values, do not have any automatic conversion to strings, and do not have necessarily distinct identity.
@alercah, please don't pull this idomatic Go
into any discussion as a supposedly "winning argument"; Go doesn't have built-in Enums, so talking about some non-existing idoms, make little sense.
Go was built to be a better C/C++ or a less verbose Java, so comparing it to the latter would make more sense. And Java does have a built-in Enum type
("Java programming language enum types are much more powerful than their counterparts in other languages. "): https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html
And, while you may disagree with the "much more powerful part", the Java Enum
type does have all of the three features I mentioned.
I can appreciate the argument that Go is leaner, simpler etc., and that some compromise must be taken to keep it this way, and I have seen some hacky workarounds in this thread that kind of works, but a set of iota
ints do not alone make an enum.
Enumerations and automatic string conversions are good candidates for the 'go generate' feature. We have some solutions already. Java enums are something in the middle of classic enums and sum types. So it is a bad language design in my opinion. The thing about idiomatic Go is the key, and I don't see strong reasons to copy all the features from language X to language Y, just because someone is familiar with.
Java programming language enum types are much more powerful than their counterparts in other languages
That was true a decade ago. See modern zero-cost implementation of Option in Rust powered by sum types and pattern matching.
The thing about idiomatic Go is the key, and I don't see strong reasons to copy all the features from language X to language Y, just because someone is familiar with.
Note that I don't disagree too much with the conclusions given here, but the use of idiomatic Go is putting Go up on som artsy pedestal. Most software programming is fairly boring and practical. And often you just need to populate a drop-down box with an enum ...
//go:generate enumerator Foo,Bar
Written once, available everywhere. Note that the example is abstract.
@bep I think you misread the original comment. "Go idiomatic enums" was supposed to refer to the current construction of using type Foo int + const-decl + iota, I believe, not to say "whatever you are proposing isn't idiomatic".
@rsc Regarding the Go2
label, that's counter to my reasoning for submitting this proposal. #19412 is a full sum types proposal, which is a more powerful superset than my simple enum proposal here, and I would rather see that in Go2. From my perspective, the likelihood of Go2 happening in the next 5 years is tiny, and I'd rather see something happen in a shorter timeframe.
If my proposal of a new reserved keyword enum
is impossible for BC, there are still other ways to implement it, whether it be a full-on language integration or tooling built into go vet
. Like I originally stated, I'm not particular on the syntax, but I strongly believe that it would be a valuable addition to Go today without adding a significant cognitive burden for new users.
A new keyword is not possible before Go 2. It would be a clear violation of the Go 1 compatibility guarantee.
Personally, I am not yet seeing the compelling arguments for enum, or, for that matter, for sum types, even for Go 2. I'm not saying they can't happen. But one of the goals of the Go language is simplicity of the language. It's not enough for a language feature to be useful; all language features are useful--if they weren't useful, nobody would propose them. In order to add a feature to Go the feature has to have enough compelling use cases to make it worth complicating the language. The most compelling use cases are code that can not be written without the feature, at least now without great awkwardness.
I would love to see enums in Go. I am constantly finding myself wanting to restrict my exposed API (or working with a restricted API outside of my app) in which there are a limited number of valid inputs. To me, this is the perfect spot for an enum.
For example, I could be making a client app that connects to some sort of RPC style API, and has a specified set of actions / opcodes. I can use const
s for this, but there is nothing preventing anybody (myself included!) from just sending an invalid code.
On the other side of that, if I am writing the server side for that same API, it would be nice to be able to write a switch statement on the enum, that would throw a compiler error (or at least some go vet
warnings) if all the possible values of the enum are not checked (or at least a default:
exists).
I think this (enums) is an area that Swift really got right.
I could be making a client app that connects to some sort of RPC style API, and has a specified set of actions / opcodes. I can use consts for this, but there is nothing preventing anybody (myself included!) from just sending an invalid code.
This is a horrible idea to solve with enums. This would mean you can now never ever add a new enum value, because suddenly RPCs might be failing or your data will become unreadable upon rollback. The reason proto3 require that generated enum-code supports a "unknown code" value is that this is a lesson learned by pain (compare it with how proto2 solved this, which is better, but still very bad). You want the application to be able to handle this case gracefully.
@Merovius I respect your opinion, but politely disagree. Making sure only valid values are used is one of the primary uses of enums.
Enums aren't right for every situation, but they are great for some! Proper versioning and error handling should be able to handle new values in most of the situations.
For dealing with external processes having an uh-oh state is a must, certainly.
With enums (or the more general and useful sum types) you can add an explicit "unknown" code to the sum/enum that the compiler forces you to deal with (or just handle that situation entirely at the endpoint if all you can do is log it and move on to the next request).
I find sum types more useful for inside a process when I know have X cases that I know I must deal with. For small X it's not hard to manage, but, for large X, I appreciate the compiler yelling at me, especially when refactoring.
Across API boundaries the use cases are fewer, and one should always err on the side of extensibility, but sometimes you do have something that can truly only ever be one of X things, like with an AST or more trivial examples like a "day of the week" value where the range is pretty much settled at this point (up to choice of calendrical system).
@jimmyfrasche I might give you Day of the Week, but not AST. Grammars evolve. What might be invalid today, could totally be valid tomorrow and that might involve adding new node-types to the AST. With compiler-checked sum-types, this wouldn't be possible without breakages.
And I don't see why this can't just be solved by a vet-check; giving you perfectly suitable static checking of exhaustive cases and giving me the possibility of gradual repairs.
I'm playing around with implementing a client for a server API. Some of the arguments and return values are enums in the API. There are 45 enum types in total.
Using enumerated constants in Go is not feasible in my case since some of the values for different enum types share the same name. In the example below, Destroy
appears twice so the compiler will issue the error Destroy redeclared in this block
.
type TaskAllowedOperations int
const (
_ TaskAllowedOperations = iota
Cancel
Destroy
)
type OnNormalExit int
const (
_ OnNormalExit = iota
Destroy
Restart
)
Hence I will need to come up with a different representation. Ideally one that allows an IDE to show the possible values for a given type so that the users of the client would have an easier time using it. Having enum as a first class citizen in Go would satisfy that.
@kongslund I know it's not a perfect implementation, but I just made a code generator that might be of interest to you. It only requires that you declare your enum in a comment above the type declaration and will generate the rest for you.
// ENUM(_, Cancel, Destroy)
type TaskAllowedOperations int
// ENUM(_, Destroy, Restart)
type OnNormalExit int
Would generate
const(
_ TaskAllowedOperations = iota
TaskAllowedOperationsCancel
TaskAllowedOperationsDestroy
)
const(
_ OnNormalExit = iota
OnNormalExitDestroy
OnNormalExitRestart
)
The better part is that it would generate String()
methods that exclude the prefix in them, allowing you to parse "Destroy"
as either TaskAllowedOperations
or OnNormalExit
.
https://github.com/abice/go-enum
Now that the plug is out of the way...
I personally don't mind that enums are not included as part of the go language, which was not my original feeling toward the matter. When first coming to go I often had a confused reaction as to why so many choices were made. But after using the language, it's nice to have the simplicity that it adheres to, and if something extra is needed, chances are good someone else has needed it too and made an awesome package to help out with that particular problem. Keeping the amount of cruft to my discretion.
Many valid points have been raised in this discussion, some in favor of enum support and also many against it (at least as far as the proposal said anything about what "enums" are in the first place). A few things that stuck out for me:
The introductory example (Enums in Go today) is misleading: That code is generated and almost nobody would write Go code like that by hand. In fact, the suggestion (How it might look like with language support) is much closer to what we actually already do in Go.
@jediorange mentions that Swift "really got (enums) right": Be that as it may, but Swift enums are a surprisingly complicated beast, mixing all kinds of concepts together. In Go we deliberately avoided mechanims that overlapped with other language features and in return obtain more orthogonality. The consequence for a programmer is that she doesn't have to make a decision which feature to use: an enum or a class, or a sum type (if we had them), or an interface.
@ianlancetaylor's point about the usefulness of language features must not be taken lightly. There's a gazillion of useful features; the question is which ones are truly compelling and worth their cost (of extra complexity of the language and thus readability, and of implementation).
As a minor point, iota-defined constants in Go are of course not restricted to ints. As long as they are constants they are restricted to (possibly named) basic types (incl. floats, booleans, strings: https://play.golang.org/p/lhd3jqqg5z).
@merovius makes good points about the limitations of (static!) compile-time checks. I am very doubtful that enumerations that cannot be extended are suitable in sutuations where extension is desirable or expected (any long-lived API surface evolves over time).
Which brings me to some questions about this proposal which I believe need to be answered before there can be any meaningful progress:
1) What are the actual expectations for enums as proposed? @bep mentions enumerability, iterability, string representations, identity. Is there more? Is there less?
2) Assuming the list in 1), can enums be extended? If so, how? (in the same package? another package?) If they cannot be extended, why not? Why is that not a problem in practice?
3) Namespace: In Swift, an enum type introduces a new namespace. There's significant machinery (syntactic sugar, type deduction) such that the namespace name doesn't have to be repeated everywhere. E.g., for enum values of an enum Month, in the right context, one can write .January rather than Month.January (or worse, MyPackage.Month.January). Is an enum namespace needed? If so, how is an enum namespace extended? What kind of syntactic sugar is required to make this work in practice?
4) Are enum values constants? Immutable values?
5) What kind of operations are possible on enum values (say, besides iteration): Can I move one forward, one backward? Does it require extra built-in functions or operators? (Not all iterations may be in order). What happens if one moves forward past the last enum value? Is that a runtime error?
(I've corrected my phrasing of the next paragraph in https://github.com/golang/go/issues/19814#issuecomment-322771922. Apologies for the careless choice of words below.)
Without trying to actually answer these questions this proposal is meaningless ("I want enums that do what I want" is not a proposal).
Without trying to actually answer these questions this proposal is meaningless
@griesemer You have a great set of points/questions -- but labelling this proposal meaningless for not answering these questions makes little sense. The bar for contribution is set high in this project, but it should be allowed to propose something without having a PhD in compilers, and a proposal should not need to be a ready to implement design.
The introductory example (Enums in Go today) is misleading: That code is generated and almost nobody would write Go code like that by hand. In fact, the suggestion (How it might look like with language support) is much closer to what we actually already do in Go.
@griesemer I have to disagree. I shouldn't have left the full uppercasing in the Go variable name, but there are plenty of places where handwritten code looks nearly identical to my suggestion, written by Googlers who I respect in the Go community. We follow the same pattern in our codebase quite often. Here's an example pulled from the Google Cloud Go library.
// ACLRole is the level of access to grant.
type ACLRole string
const (
RoleOwner ACLRole = "OWNER"
RoleReader ACLRole = "READER"
RoleWriter ACLRole = "WRITER"
)
They use the same construct in multiple places. https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/bigquery/table.go#L78-L116 https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/storage/acl.go#L27-L49
There was some discussion later about how you can make things more terse if you're ok using iota
, which can be useful in its own right, but for a limited use case. See my previous comment for more details. https://github.com/golang/go/issues/19814#issuecomment-290948187
@bep Fair point; I apologize for my careless choice of words. Let me try again, hopefully phrasing my last paragraph above more respectfully and clearer this time:
In order to be able to make meaningful progress, I believe the proponents of this proposal should try to be a bit more precise about what they believe are important features of enums (for instance by answering some of the questions in https://github.com/golang/go/issues/19814#issuecomment-322752526). From the discussion so far the desired features are only described fairly vague.
Perhaps as a first step, it would be really useful to have case studies that show how existing Go falls (significantly) short and how enums would solve a problem better/faster/clearer, etc. See also @rsc's excellent talk at Gophercon regarding Go2 language changes.
@derekperkins I would call those (typed) constant definitions, not enums. I'm guessing our disagreement is due to a different understanding of what an "enum" is supposed to be, hence my questions above.
(My previous https://github.com/golang/go/issues/19814#issuecomment-322774830 should have gone to @derekperkins of course, not @ derekparker. Autocomplete defeated me.)
Judging from @derekperkins comment, and partially answering my own questions, I gather that a Go "enum" should have at least the following qualities:
Does that sound right? If so, what else needs to be added to this list?
Your questions are all good ones.
What are the actual expectations for enums as proposed? @bep mentions enumerability, iterability, string representations, identity. Is there more? Is there less?
Assuming the list in 1), can enums be extended? If so, how? (in the same package? another package?) If they cannot be extended, why not? Why is that not a problem in practice?
I don't think enums can be extended for two reasons:
Namespace: In Swift, an enum type introduces a new namespace. There's significant machinery (syntactic sugar, type deduction) such that the namespace name doesn't have to be repeated everywhere. E.g., for enum values of an enum Month, in the right context, one can write .January rather than Month.January (or worse, MyPackage.Month.January). Is an enum namespace needed? If so, how is an enum namespace extended? What kind of syntactic sugar is required to make this work in practice?
I understand how the namespacing came about, as all of the examples I mentioned prefix with the type name. While I wouldn't be opposed if someone felt strongly about adding namespacing, I think that is out of scope for this proposal. Prefixing fits into the current system just fine.
Are enum values constants? Immutable values?
I would think constants.
What kind of operations are possible on enum values (say, besides iteration): Can I move one forward, one backward? Does it require extra built-in functions or operators? (Not all iterations may be in order). What happens if one moves forward past the last enum value? Is that a runtime error?
I would default to standard Go practices for slices/arrays (not maps). Enum values would be iterable based on declaration order. At a minimum, there would be range support. I lean away from letting enums be accessed via index, but don't feel strongly about it. Not supporting that should eliminate the potential runtime error.
There would be a new runtime error (panic?) caused by assigning an invalid value to an enum, whether that be through direct assignment or type casting.
If I summarize this correctly then enum values as you propose them are like typed constants (and like constants they may have user-defined constant values) but:
Does that sound about right? (This would match the classic approach languages have taken towards enums, pioneered some 45 years ago by Pascal).
Yes, that's exactly what I'm proposing.
What about switch-statements? AIUI that is one of the main drivers for the proposal.
Being able to switch on an enumeration is implied, I think, since you can switch on basically anything. I do like that swift has errors if you haven’t fully satisfied the enum in your switch, but that could be handled by vet
@jediorange I was specifically referring to the question of that last part, of whether or not there should be an exhaustiveness-check (in the interest of keeping the proposal complete). "No" is, of course, a perfectly fine answer.
The original message of this issue mentions protobufs as the motivator. I'd like to explicitly call out that with the semantics as given now, the protobuf-compiler would need to create an additional "unrecognized"-case for any enum (implying some name-mangling scheme to prevent collisions). It also would need to add an additional field to any generated struct using enums (again, mangling names in some way), in case the decoded enum-value isn't in the compiled-in range. Just like it is currently done for java. Or, probably more likely, continue to use int
s.
@Merovius My original proposal mentioned protobufs as an example, not as the primary motivator for the proposal. You bring up a good point about that integration. I think it should probably be treated as an orthogonal concern. Most code that I've seen converts from the generated protobuf types into app level structs, preferring to use those internally. It would make sense to me that protobuf could continue unchanged, and if the app creators want to convert those into a Go enum, they could handle the edge cases you bring up in the conversion process.
@derekperkins Some more questions:
What is the zero value for a variable of enum type that is not explicitly initialized? I assume it can't be zero in general (which complicates memory allocation/initialization).
Can we do limited arithmetic with enum values? For instance, in Pascal (in which I programmed once, way back when), it was surprisingly often necessary to iterate in steps > 1. And sometimes one wanted to compute the enum value.
Regarding iteration, why is a go generate produced iteration (and stringify) support not good enough?
What is the zero value for a variable of enum type that is not explicitly initialized? I assume it can't be zero in general (which complicates memory allocation/initialization).
As I mentioned in the initial proposal, this is one of the stickier decisions to make. If the definition order matters for iteration, then I think it would similarly make sense to have the first defined value be the default.
Can we do limited arithmetic with enum values? For instance, in Pascal (in which I programmed once, way back when), it was surprisingly often necessary to iterate in steps > 1. And sometimes one wanted to compute the enum value.
Whether you are using numerical or string based enums, does that mean that all enums have an implicit zero based index? The reason I mentioned before that I lean towards only supported range
iterations and not index based, is that doesn't expose the underlying implementation, which could use an array or a map or whatever underneath. I don't anticipate needing to access enums via index, but if you have reasons why that would be beneficial, I don't think there is a reason to disallow it.
Regarding iteration, why is a go generate produced iteration (and stringify) support not good enough?
Iteration isn't my main use case personally, though I do think it adds value to the proposal. If that were the driving factor, maybe go generate
would be sufficient. That doesn't help guarantee value safety. The Stringer()
argument assumes that the raw value is going to be iota
or int
or some other type representing the "real" value. You would also have to generate (Un)MarshalJSON
, (Un)MarshalBinary
, Scanner/Valuer
and any other serialization methods you might use to ensure that the Stringer
value was used to communicate vs whatever Go uses internally.
@griesemer I think I may not have fully answered your question about the extensibility of enums, at least in regards to adding/removing values. Having the ability to edit them is an essential part of this proposal.
From @Merovius https://github.com/golang/go/issues/19814#issuecomment-290969864
any package that ever wants to change a set of enums, would automatically and forcibly break all their importers
I don't see how this is different than any other breaking API change. It's up to the creator of the package to respectfully handle BC, just the same as if types, functions or function signatures change.
From an implementation perspective, it would be quite complex to support types whose default value was not all-bits-zero. There are no such types today. Requiring such a feature would have to count as a mark against this idea.
The only reason the language requires make
to create a channel is to preserve this feature for channel types. Otherwise make
could be optional, only used to set the channel buffer size or to assign a new channel to an existing variable.
I'd like to propose that enum be added to Go as a special kind of
type
. The examples below are borrowed from the protobuf example.Enums in Go today
How it might look with language support
The pattern is common enough that I think it warrants special casing, and I believe that it makes code more readable. At the implementation layer, I would imagine that the majority of cases can be checked at compile time, some of which already happen today, while others are near impossible or require significant tradeoffs.
SearchRequest(99)
orSearchRequest("MOBILEAPP")
. Current workarounds include making an unexported type with options, but that often makes the resulting code harder to use / document.Things to Consider
enum
on top of the type system, I don't believe this should require special casing. If someone wantsnil
to be valid, then the enum should be defined as a pointer.I don't have any strong opinions on the syntax. I do believe this could be done well and would make a positive impact on the ecosystem.