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.
type SearchRequest int
var (
SearchRequestUNIVERSAL SearchRequest = 0 // UNIVERSAL
SearchRequestWEB SearchRequest = 1 // WEB
SearchRequestIMAGES SearchRequest = 2 // IMAGES
SearchRequestLOCAL SearchRequest = 3 // LOCAL
SearchRequestNEWS SearchRequest = 4 // NEWS
SearchRequestPRODUCTS SearchRequest = 5 // PRODUCTS
SearchRequestVIDEO SearchRequest = 6 // VIDEO
)
type SearchRequest string
var (
SearchRequestUNIVERSAL SearchRequest = "UNIVERSAL"
SearchRequestWEB SearchRequest = "WEB"
SearchRequestIMAGES SearchRequest = "IMAGES"
SearchRequestLOCAL SearchRequest = "LOCAL"
SearchRequestNEWS SearchRequest = "NEWS"
SearchRequestPRODUCTS SearchRequest = "PRODUCTS"
SearchRequestVIDEO SearchRequest = "VIDEO"
)
// IsValid has to be called everywhere input happens, or you risk bad data - no guarantees
func (sr SearchRequest) IsValid() bool {
switch sr {
case SearchRequestUNIVERSAL, SearchRequestWEB...:
return true
}
return false
}
enum SearchRequest int {
0 // UNIVERSAL
1 // WEB
2 // IMAGES
3 // LOCAL
4 // NEWS
5 // PRODUCTS
6 // VIDEO
}
enum SearchRequest string {
"UNIVERSAL"
"WEB"
"IMAGES"
"LOCAL"
"NEWS"
"PRODUCTS"
"VIDEO"
}
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)
or SearchRequest("MOBILEAPP")
. Current workarounds include making an unexported type with options, but that often makes the resulting code harder to use / document.enum
on top of the type system, I don't believe this should require special casing. If someone wants nil
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.
@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.
```go
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.
@derekperkins Most other API changes can be orchestrated for gradual repair. I really recommend to read Russ Cox description, it makes a lot of things very clear.
Open enums (like the current const+iota construct) allow gradual repair, by (for example) a) defining the new value without using it, b) updating reverse dependencies to handle the new value, c) start using the value. Or, if you want to remove a value, a) stop using the value, b) update reverse dependencies to not mention the to-be-removed value, c) remove the value.
With closed (compiler-checked for exhaustiveness) enums this is not possible. If you remove the handling of a value or define a new one, the compiler will complain about a missing switch-case immediately. And you can't add handling of a value before defining one.
The question isn't about whether the individual changes could be considered breaking (they can, in isolation), but about whether there is a non-breaking sequence of commits over the distributed code-base that doesn't break.
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.
@ianlancetaylor I'm definitely not going to be able to speak to the full implementation, but if enums were implemented as a 0 based array (which is what it sounds like @griesemer is in favor of), then 0 as the index seems like it could double as "all-bits-zero".
With closed (compiler-checked for exhaustiveness) enums this is not possible.
@Merovius If exhaustiveness were checked by go vet
or similar tooling as suggested by @jediorange vs enforced by the compiler, would that alleviate your concerns?
@derekperkins About their harmfulness, yes. Not about their lack of usefulness. The same issues of version-skew also happen for the most of the use cases that they are usually considered for (syscalls, network protocols, file formats, shared objects…). There is a reason proto3 requires open enums and proto2 doesn't - it's a lesson learned from many outages and data corruption incidents. Even though Google is already pretty careful to avoid version skew. From my perspective, open enums with default cases are just the correct solution. And apart from alleged safety against invalid values, they don't really bring a lot to the table, from what I can tell.
All of that being said, I'm not a decider.
@derekperkins In https://github.com/golang/go/issues/19814#issuecomment-322818206 you are confirming that (from your viewpoint):
In https://github.com/golang/go/issues/19814#issuecomment-322895247 you are saying that:
And in https://github.com/golang/go/issues/19814#issuecomment-322903714 you are saying the "ability to edit them is an important part of this proposal".
I'm confused: So iteration is not a primary motivator, fine. That leaves at a minimum an enum declaration of enum values that are constants, and they cannot be extended outside the declaration. But now you're saying the ability to edit them is important. What does that mean? Surely not that they can be extended (that would be a contradiction). Are they variables? (But then they are not constants).
In https://github.com/golang/go/issues/19814#issuecomment-322903714 you are saying that enums could be implemented as 0-based array. This suggests that an enum declaration introduces a new type together with an ordered list of enum names which are 0-based constant indices into an array of enum values (for which space is reserved automatically). Is that what you mean? If so, why wouldn't it be sufficient to just declare a fixed size array and a list of constants indices to go with it? The array bounds check would automatically ensure that you can't "extend" the enum range, and iteration would be possible already.
What am I missing?
I'm confused: So iteration is not a primary motivator, fine.
I have my own reasons why I want enums, while also trying to take into account what others in this thread, including @bep and others, have expressed as necessary parts of the proposal.
That leaves at a minimum an enum declaration of enum values that are constants, and they cannot be extended outside the declaration. But now you're saying the ability to edit them is important. What does that mean? Surely not that they can be extended (that would be a contradiction). Are they variables? (But then they are not constants).
When I say to edit them, it is to @Merovius's point that they are open enums. Constants at build time, but not locked down forever.
In #19814 (comment) you are saying that enums could be implemented as 0-based array.
This is just me speculating beyond my paygrade as to how I imagine it might be implemented behind the scenes, based on your https://github.com/golang/go/issues/19814#issuecomment-322884746 and @ianlancetaylor's https://github.com/golang/go/issues/19814#issuecomment-322899668
"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."
I don't know how you would plan on doing that for any non-integer enum, hence my question about whether that arithmetic would require each member of the enum to be implicitly assigned an index based on the declaration order.
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.
Again, I don't know how the compiler works, so I was just trying to continue the conversation. At the end of the day, I'm not trying to propose anything radical. Like you mentioned before, "This would match the classic approach languages have taken towards enums, pioneered some 45 years ago by Pascal", and that fits the bill.
To anyone else who has expressed interest, please feel free to chip in.
Another question is whether one can use these enums to index into arrays or slices. A slice is often a very efficient and compact way of representing an enum->value mapping and requiring a map would be unfortunate, I think.
@derekperkins Ok, I'm worried that puts us (or at least me) back to square one: What is the problem you're trying to solve? Do you simply want a nicer way to write what we currently do with constants and perhaps iota (and for which we use go generate to get string representations)? That is, some syntactic sugar for a notation that you (perhaps) find overly burdensome? (That's a fine answer, just trying to understand.)
You mentioned that you have your own reasons for wanting them, perhaps you can explain a bit more what those reasons are. The example you gave in the very beginning doesn't make much sense to me, but I am probably missing something.
As is stands, everybody has a little bit a different understanding of what this proposal ("enums") entails, as has become clear from the various responses: There's a huge range of possibilities between Pascal enums and Swift enums. Unless you (or somebody else) describes very clearly what is proposed (I'm not asking for an implementation, mind you), it will be difficult to make any meaningful progress or even just debate the merits of this proposal.
Does that make sense?
@griesemer It totally makes sense and I understand the bar to be passed that @rsc talked about at Gophercon. You obviously have a much deeper understanding than I ever will. In #21473, you mentioned that iota for vars wasn't implemented because there wasn't a compelling use case at the time. Is that the same reason that enum
was not included from the beginning? I would be very interested to know your opinion on whether or not it would add value to Go, and if it would, where would you start the process?
@derekperkins Regarding your question in https://github.com/golang/go/issues/19814#issuecomment-323144075: At the time (in Go's design) we were only considering relatively simple (say Pascal or C-style) enums. I don't recall all the details but there was certainly a feeling that there was not enough benefit for the extra machinery required for enums. We felt that they were essentially glorified constant declarations.
There are also problems with these traditional enums: It's possible to do arithmetic with them (they are just integers), but what does it mean if they go "out of (enum) range"? In Go they are just constants and "out of range" doesn't exists. Another one is iteration: In Pascal there were special built-in functions (I think SUCC and PRED) to advance the value of a variable of enum type forward and backward (in C one just does ++ or --). But the same issue appears here as well: What happens if one goes past the end (very common problem in a for loop ranging over enum values using ++ or the Pascal equivalent SUCC). Finally, an enum declaration introduces a new type, the elements of which are the enum values. These values have names (the ones defined in the enum declaration), but those names are (in Pascal, C) in the same scope as the type. This is a bit unsatisfying: When declaring two different enums, one would hope that one could use the same enum value name for each enum type without conflict, which is not possible. Of course Go doesn't solve that either, but a constant declaration also doesn't look like it's introducing a new name space. A nicer solution is to introduce a name space with each enumeration, but then each time an enum value is used, it needs to be qualified with the enum type name, which is annoying. Swift solves this by deducing the enum type where possible and then one can use the enum value name prefixed with a dot. But that's quite a bit of machinery. And finally, sometimes (often, in public APIs), one needs to extend an enum declaration. If that's not possible (you don't own the code), there's a problem. With constants, those problems don't exist.
There's probably more to it; this is just what comes to mind. In the end we decided that it was better to emulate enums in Go using the orthogonal tools we already had: custom integer types that make erroneous assignments less likely, and the iota mechanism (and ability leave away repeated initialization expressions) for the syntactic sugar.
Hence my questions: What are you looking to gain from specialized enum declarations that we can't adequately emulate in Go with little syntactic overhead? I can think of enumeration, and an enum type that cannot be extended outside the declaration. I can think of more abilities for enum values, as in Swift.
Enumeration could be solved easily with a go generator in Go. We already have a stringer. Restricting extension is problematic across API boundaries. More abilities for enum values (say as in Swift) seems very Go-unlike because it mixes a lot of orthogonal concepts. In Go, we would probably achieve that by using elementary building blocks.
@griesemer Thanks for your thoughtful reply. I don't disagree that they are basically glorified constant declarations. Having type safety in Go is great, and the main value that enum
would provide to me personally is value safety. The way to mimic that in Go today is to run validation functions at every entrypoint for that variable. It's verbose and makes it easy to make mistakes, but is possible with the language as is today. I already namespace by prefixing the type name in front of the enum, which while verbose, isn't a big deal.
I personally dislike most uses for iota
. While cool, most of the time my enum
-like values map to external resources like a db or external api, and I prefer to be more explicit that the value should not be changed if you happen to reorder. iota
also doesn't help for most of the places I would use an enum
because I would be using a list of string values.
At the end of the day, I don't know how much more I can clarify this proposal. I would love if they were supported in whatever fashion made sense for Go. Regardless of the exact implementation, I would still be able to use them and they would make my code safer.
I think the canonical way Go does enums today (as seen in https://github.com/golang/go/issues/19814#issuecomment-290909885) is pretty close to correct.
There are a few drawbacks:
I'm ok without #1.
go:generate + stringer can be used for #2. If that doesn't handle your use case, make the base type of your "enum" a string instead of an int, and use string constant values.
Add an explicit
keyword to a type definition. This keyword forbids conversions into this type except for conversions in const blocks in the package in which that type is defined. (Or restricted
? Or maybe enum
means explicit type
?)
Reusing the example I referenced above,
//go:generate stringer -type=SearchRequest
explicit type SearchRequest int
const (
Universal SearchRequest = iota
Web
Images
Local
News
Products
Video
)
There are conversions from int
to SearchRequest
inside the const
block. But only the package author can introduce a new SearchRequest value, and will be unlikely to introduce one accidentally (by passing an int
to a function that expects a SearchRequest
, for example).
I'm not really actively proposing this solution, but I think the don't-accidentally-construct-an-invalid-one is the salient property of enums that can't be captured in Go today (unless you go the struct with an unexported field route).
I think the interesting risk with enums is with untyped constants. People who write an explicit type conversion know what they are doing. I would be willing to consider a way for Go to forbid explicit type conversions under certain circumstances, but think that is entirely orthogonal to the notion of enum types. It is an idea applies to any kind of type.
But untyped constants can lead to accidentally and unexpectedly creating a value of the type, in a way that is not true of explicit type conversions. So I think that @randall77 's suggestion of explicit
can be simplified to simply mean that untyped constants may not be implicitly converted to the type. An explicit type conversion is always required.
I would be willing to consider a way for Go to forbid explicit type conversions under certain circumstances
@ianlancetaylor Optionally disallowing type conversions, whether explicit or implicit, would solve the issues that caused me to create this proposal in the first place. Only the originating package would then be able to create and thus satisfy any types. That is even better than the enum
solution in some ways because it not only supports const
declarations, but any type.
@randall77, @ianlancetaylor How would make the explicit
suggestion work together with the zero value of that type?
@derekperkins Disallowing type-conversions altogether will make it impossible to use these types in generic encoders/decoders, like the encoding/*
packages.
@Merovius I think @ianlancetaylor suggests the restriction only for implicit conversions (e.g., assignment of an untyped constant to a restricted type). Explicit conversion would still be possible.
@griesemer I know :) But I understood @derekperkins to suggest differently.
Doesn't allowing explicit conversions undermine the very reason we're thinking about this "explicit" qualifier? If someone can decide to convert an arbitrary value to an "explicit" type, then we have no more guarantee that a given value is one of the enumerated constants than we do now.
I guess it does help for the casual or unintended use of untyped constants, which is perhaps the most important thing.
I suppose I'm questioning whether disallowing explicit conversions is in "the spirit of Go." Disallowing explicit conversions is taking a big step toward programming based on types rather than programming based on writing code. I think Go is taking a clear position in favor of the latter.
@griesemer @Merovius I'll repost the quote from @ianlancetaylor again, since it was his suggestion, not mine.
I would be willing to consider a way for Go to forbid explicit type conversions under certain circumstances
Both @rogpeppe and @Merovius bring up good points about the ramifications. Allowing explicit conversions but not implicit conversions doesn't solve the issue of guaranteeing valid types, but losing generic encoding would be a pretty big downside.
There has been a lot of back and forth here, but I think there has been some good ideas. Here is a summary what I'd like to see (or something similar), which seems to align with what some others have said. I openly admit I am not a language designer or compiler programmer, so I don't know how well it would work.
go vet
?) a default
case, even when already exhaustive (likely an error) for future changes.nil
, like a slice does.That last one _may_ be a bit controversial. And I don't know for sure if that would work, but I think it would fit in semantically -- much like one would check for a nil
slice, one could put in checks for a nil
enum value.
As for iteration, I don't really think I would ever use it, but I don't see the harm in it.
As an example of how it could be declared:
type MetadataBlockType enum[uint] {
StreamInfo: 0
Padding: 1
Application: 2
SeekTable: 3
VorbisComment: 4
CueSheet: 5
Picture: 6
}
Also, the Swift style of inferring the type and using "dot syntax" would be _nice_, but definitely not necessary.
type EnumA int
const (
Unknown EnumA = iota
AAA
)
type EnumB int
const (
Unknown EnumB = iota
BBB
)
There 2 pieces of code can't exist in a single Go file, nor the same package, nor even one is import from another package.
Please just implement the C# way of implementing Enum:
type Days enum {Sat, Sun, Mon, Tue, Wed, Thu, Fri}
type Days enum[int] {Sat:1 , Sun, Tue, Wed, Thu, Fri}
type Days enum[string] { Sat: "Saturay" , Sun:"Sunday" etc}
@KamyarM How is that better than
type Days int
const (
Sat Days = 1+iota
Sun
...
)
and
type Days string
const (
Sat Days = "Saturday"
Sun = "Sunday"
...
)
I'd like to kindly request to restrict comments to new approaches/arguments. A lot of people are subscribed to this thread and adding noise/repitition can be perceived as disrespectful of their time and attention. There is plenty of discussion up there ↑, including detailed answers to both of the previous comments. You won't agree with everything said and none of the sides might like the outcomes of that discussion so far - but simply ignoring it won't help moving it along in a productive direction either.
It is better because it does not have naming conflict issue. Also support compiler type checking. The approach you mentioned organized it better than nothing but the compiler doesn't restrict you on what you can assign to it. You can assign an integer that is not any of the days to an object of that type:
var a Days
a =10
compiler actually does nothing about it. So There is not much point to this kind of enum. other than it is better organized in IDEs like GoLand
I would like to see something like that
type WeekDay enum string {
Monday "mon"
Tuesday "tue"
// etc...
}
Or with automatic iota
usage:
// this assumes that iota automatically assigned to the first declared enum key
// and values have unsigned integer type
// value is positional, so if you decide to rename your key
// you don't have to change everything in db
// also it is easy to grow your lists
type WeekDay enum {
Monday
Tuesday
}
This will provide simplicity and easiness in use:
func makeItWorkOn(day WeekDay) {
// your implementation
}
Also, enum should have builtin method to validate value so we can validate something from user input:
if day in WeekDay {
makeItWorkOn(day)
}
And simple things like:
if day == WeekDay.Monday {
// whatever
}
To be honest, my favorite syntax would be like this (KISS):
// type automatically inferred from values or `iota`
enum WeekDay {
Monday "mon"
Tuesday "tue"
}
@zoonman The last example doesn't follow the following principle of Go: a function declaration starts with func
, a type declaration starts with type
, a variable declaration starts with var
, ...
@md2perpe I'm not trying to follow Go "type" principles, I'm writing code everyday and the only one principle I follow - keep things simple.
Than more code you have to write _to follow principles_ then more time is wasted.
TBH I'm Go newbie but there are a lot of things I can criticize.
For example:
struct User {
Id uint
Email string
}
Is easier to write and understand than
type User struct {
Id uint
Email string
}
I can give you example where type should be used:
// this is terrible because it blows your mind off
// especially if you Go newbie
// this should not be allowed by compiler/linter:
w := map[string]interface{}{"type": 0, "connected": true}
// it has to be splitted into type definition
type WeirdJSON map[string]interface{}
// and used like
w := WeirdJSON{"type": 0, "connected": true}
I used to write code in Asm, C, C++, Pascal, Perl, PHP, Ruby, Python, JavaScript, TypeScript, now Go. I saw all of that. This experience tells me that code must be laconic, easy to read and understand.
I make some machine learning project and need to parse MIDI file.
There i need to parse SMPTE timecode. I find quite hard to use idiomatic way with iota, but it don`t stop me)
const (
SMTPE0 int8 = ((-24 - (1 + (iota - 1) * 3) % 6 * (iota - 1) / ((iota - 1) | 0x01)) - 10 * ((iota - 1) % 2) - 5 * (iota / 3 - iota / 4) ) * iota / (iota | 0x01)
SMTPE24
SMTPE25
SMTPE29
SMTPE30
)
const (
_SMTPE0 int8 = 0
_SMTPE24 int8 = -24
_SMTPE25 int8 = -25
_SMTPE29 int8 = -29
_SMTPE30 int8 = -30
)
Of course i may need some runtime check with defensive programming...
func IsSMTPE(status int8) bool {
j := 4
for i:= 0; i >= -30; i -= j % 6{
if i == int(status){
return true
}
j+=3
}
return status == 0
}
Enums make live of programmers in some case more simpler. Enums is just an instrument then you use it properly it may economy time and increse productivity. I think there is no problens to implement this in Go 2 like in c++, c# or other langs. This example is just a joke, but it cleary show the problem.
@streeter12 I don't see how your example "clearly shows the problem". How would enums make this code any better or safer?
There is C# class with implementation of same logic of enum.
public enum SMTPE : sbyte
{
SMTPE0 = 0,
SMTPE24 = -24,
SMTPE25 = -25,
SMTPE29 = -29,
SMTPE30 = -30
}
public class TestClass
{
public readonly SMTPE smtpe;
public TestClass(SMTPE smtpe)
{
this.smtpe = smtpe;
}
}
With compile time enums i can:
After all it just a tools to prevent some common human errors and save performance.
@streeter12 Thanks for clarifying what you meant. The only advantage over Go constants here is that one cannot introduce an invalid value because the type system will not accept any other value but one of the enum values. That's certainly nice to have but it also comes at a price: There's no way to extend this enum outside that code. External enum extension is one of the key reasons why in Go we decided against standard enums.
Answer just simple need to make some extensions dont use enums.
F.E need to make state machine use state pattern instead of enums.
Enums have their own scope. I complete some large projects without any enum. I think it's terrible architecture decision to extend enum outside definition code. You dont have control over that your colleague doing and it make some funny errors)
And you forgot human factor enums in many cases significhent reduce errors in large projects.
@streeter12 Unfortunately, the reality is that often enums need to be extended.
@griesemer extending an enum/sum type creates a separate and sometimes-incompatible type.
This is still true in Go even though there aren't explicit types for enums/sums. If you have an "enum type" in a package that expects values in {1, 2, 3} and you pass it a 4 from your "extended enum type" you've still violated the contract of the implicit "type".
If you need to extend an enum/sum you also need to create explicit To/From conversion functions that explicitly handle the sometimes-incompatible cases.
I think the disconnect between that argument and people for this proposal or similar proposals like #19412 is that we think it's weird that the trade off is "always write basic validation code the compiler could handle" instead of "sometimes write conversion functions that you're probably going to also have to write anyway".
That's not to say either side is right or wrong or that is the only trade off to consider, but I wanted to identify a bottleneck in communication between the sides that I've noticed.
I think the disconnect between that argument and people for this proposal or similar proposals like #19412 is that we think it's weird that the trade off is "always write basic validation code the compiler could handle" instead of "sometimes write conversion functions that you're probably going to also have to write anyway".
Very well stated
@jimmyfrasche That's not how I personally would describe the tradeoff. I would say it's "always write basic validation code the compiler could handle" vs. "add an entire new concept to the type system that everybody using Go needs to learn and understand."
Or, let me put it another way. As far as I can tell, the only significant features missing from Go's version of enumerated types is that there is no validation of assignment from untyped constants, there is no check on explicit conversions, and there is no check that all values were handled in a switch. It seems to me that those features are all independent of the notion of enumerated types. We shouldn't let the fact that other languages have enumerated types lead us to the conclusion that Go also needs enumerated types. Yes, enumerated types would give us those missing features. But is it really necessary to add an entirely new kind of type in order to get them? And is the increase in language complexity worth the benefits?
@ianlancetaylor Adding complexity to the language is certainly a valid thing to consider, and "because another language has it" is certainly not an argument. I, personally, don't think enum types are worth it on their own. (Their generalization, sum types, sure tick a lot of boxes for me, however).
A general way for a type to opt out of assignability would be nice, though I'm not sure how widely useful that would be outside of primitives.
I'm not sure how generalizable the concept of "check all values handled in a switch" is, without some way of letting the compiler know the complete list of legal values. Other than enum and sum types, the only thing I can think of is something like Ada's range types but those aren't naturally compatible with zero values unless 0 must be in the range or code's generated to handle offsets whenever they're converted or reflected upon. (Other languages have had similar families of types, some in the pascal family, but Ada's the only one that comes to mind at the moment)
Anyway, I was specifically referring to:
The only advantage over Go constants here is that one cannot introduce an invalid value because the type system will not accept any other value but one of the enum values. That's certainly nice to have but it also comes at a price: There's no way to extend this enum outside that code. External enum extension is one of the key reasons why in Go we decided against standard enums.
and
Unfortunately, the reality is that often enums need to be extended.
That argument doesn't work for me for the reasons I stated.
@jimmyfrasche Understood; it's a difficult problem. Which is why in Go we didn't try to solve it but instead only provided a mechanism to easily create sequences of constants w/o the need to repeat the constant value.
(Sent out delayed - was meant as response to https://github.com/golang/go/issues/19814#issuecomment-349158748)
@griesemer indeed and it was definitely the right call for Go 1 but some of it is worth reevaluating for Go 2.
There's enough in the language to get _almost_ everything one would want out of enum types. It requires more code than a type definition, but a generator could handle most of it and it lets you define as much or as little as fits the situation instead of just getting whatever powers come with an enum type.
This approach https://play.golang.org/p/7ud_3lrGfx gets you everything except
That approach can also be used for small, simple sum types†but it is more awkward to use, which is why I think something like https://github.com/golang/go/issues/19412#issuecomment-323208336 would add to the language and it could be used by a code generator to create enum types that avoid problems 1 and 2.
†see https://play.golang.org/p/YFffpsvx5e for a sketch of json.Token with this construction
we think it's weird that the trade off is "always write basic validation code the compiler could handle" instead of "sometimes write conversion functions that you're probably going to also have to write anyway".
To me - a representative of the camp of fierce proponents of gradual repair - this seems like the correct(ish) tradeoff. Honestly, even if we're not talking about gradual repair I'd consider that a better mental model.
For one, type-checked enum will only ever be able to check values inserted into source code anyway. If the enum travels over a network, is persisted to disk or exchanged between processes, all bets are off (and most proposed usages of enums fall into this category). So you won't get around the problem of handling incompatibilities at runtime anyway. And there is no general one-size-fits-all default behavior for when you encounter an invalid enum-value. Often you might want to error out. Sometimes you might want to coerce it into a default-value. Most of the time you want to preserve it and pass it around, so that it doesn't get lost on re-serialization.
Of course you might argue that there should still be a trust-boundary, where the validity is checked and the required behavior is implemented - and everything inside that boundary should be able to trust that behavior. And the mental model seems to be, that this trust-boundary should be a process. Because all the code in a binary will be changed atomically and stay internally consistent. But that mental model gets eroded by the idea of gradual repair; suddenly, the natural trust-boundary becomes a package (or maybe repository) as the units that you apply your atomic repairs to and the unit that you trust to be self-consistent.
And, personally, I find that a very natural and great unit of self-consistency. A package should be just large enough to keep its semantics, rules and conventions in your head. Which is also why exports work at the package-level, not the type-level and why top-level declarations are scoped at the package-level, not the program-level. Seems fine and save enough to me, to decide the correct handling of unknown enum-values at the package-level too. Have an unexported function, that checks it and maintains the internally desired behavior.
I'd be much more on board with a proposal that every switch needs a default-case, than with a proposal to have type-checked enums including exhaustiveness-checks.
@Merovius The operating system process and the package are both trust boundaries, as you put it.
Information coming from out of process has to be validated at its ingress and unmarshaled into an appropriate representation for the process and appropriate care taken when that fails. That never goes away. I don't really see anything specific to sum/enum types there. You could say the same about structs—sometimes you get extra fields or too few fields. Structs are still useful.
That being said, with enum type you are of course able to include cases specific for modelling these errors. For example
type FromTheNetwork enum {
// pretend all the "valid" values are listed here
Missing // the value was not included in the message
Unknown // the value was not in the set of the valid values
Error // there was an error attempting to read the value
}
and with sum types you can go further:
type FromTheNetwork pick {
Missing struct{} // Not included in message
Valid somepkg.TheSumBeingReceived // Include valid states with composition
Unknown []byte // Raw bytes of unknown value received
Error error // The error from attempting to read the value
}
(The former isn't as useful unless it is held in a struct with fields specific to the error cases, but then the validity of those fields depend on the value of the enum. The sum type takes care of that as it is essentially a struct that can only have one field set at a time.)
At the package level, you still need to handle high level validation, but the low level validation comes with the type. I'd say reducing the domain of the type helps keep the package small and in your head. It also makes the intent clearer to tooling so that your editor could write out all the case X:
lines and leave you to fill in the actual code or a linter could be used to make sure all code is checking all cases (you talked me out of having the exhaustiveness in the compiler earlier).
I don't really see anything specific to sum/enum types there. You could say the same about structs—sometimes you get extra fields or too few fields. Structs are still useful.
If we are talking about open enums (like the ones currently consturcted by iota), then, sure. If we are talking about closed enums (which is what people usually talk about, when they talk about enums) or enums with exhaustiveness-checks, then they certainly are special. Because they are not extensible.
The analogy with structs explains this rather perfectly: The Go 1 compatibility promise excludes unkeyed struct literals from any promise - and thus, using keyed struct literals has been a practice considered so strongly as "best", that go vet has a check for it. The reason is exactly the same: If you are using unkeyed struct literals, structs are no longer extensible.
So, yes. Structs are exactly like enums in this regard. And we have agreed as a community that it's preferable to use them in an extensible manner.
That being said, with enum type you are of course able to include cases specific for modelling these errors.
Your example only covers the process-boundary (by talking about network-errors), not the package-boundary. How will packages behave, if I add a "InvalidInternalState" (to make something up) to FromTheNetwork
? Do I have to fix their switches before they compile again? Then it's not extensible in the gradual repair model. Do they require a default-case to compile in the first place? Then there doesn't seem to be any point to enums.
Again, having open enums is a different question. I'd be on board with things like
I'd say reducing the domain of the type helps keep the package small and in your head. It also makes the intent clearer to tooling so that your editor could write out all the case X: lines and leave you to fill in the actual code or a linter could be used to make sure all code is checking all cases
But for that we don't need actual enums, as types. Such a linting tool could also heuristically check for const
-declarations using iota
, where every case is of a given type and consider that "an enum" and perform the checks you want. I'd be completely on board with a tool using these "enums by convention" to help autocompletion or linting that every switch needs to have a default or even that every (known) case must be checked. I'd even be not-opposed to adding an enum-keyword that behaves mostly like that; i.e. an enum is open (can take any integer value), gives you the extra scoping and requires to have a default in any switch (I don't think they'd add enough over iota-enums for the added costs, but at least they wouldn't harm my agenda). If that's what's proposed - fine. But it doesn't seem to be what the majority of supporters of this proposal (certainly not the initial text) mean.
We can disagree about the importance of keeping gradual repair and extensibility possible - for example, a lot of people believe that semantic versioning is a better solution to the problems it solves. But if you find them important, it is perfectly valid and reasonable to see enums as either harmful or pointless. And that was the question I was replying to: How people can reasonably make the tradeoff of requiring a check everywhere, instead of having it in the compiler. Answer: By valuing extensibility and evolution of APIs, which makes these checks necessary at the usage-site anyway.
From time to time opponents of enum said they are not expandable, we still need checks after serialization/transition, we can break back compatibility etc.
The main problem that this is not enum problems it is your develop and artitecture problems.
You try to give an example where using of enums are ridiculous, but let's consider some situations in more detail.
Example 1. I am low level developer and, i need const for some registers adress, established low level protocol values etc. At now in Go i got only one solution:is to use consts without iota, becose in many cases it would be ugly. I can get several constants block for one package and after press . i got all 20 constants and if they have same type and similar names i can make errors. If project is big you will got this error. To prevent this with defensive programming,TDD we must offen duplicate check code (duplicate code = duplicate errors/tests in any case). With the use of transfers we do not have similar problems and values will never changed in this case (try to find situation when registers adresses will cahnged in production:)). We still sometimes check is value what we get from file/net of etc. is in range, but there no probles to make this centrolized (see c# Enum.TryParse
Example 2. I developing some small module with state/errors logic. If i make enum private, no one ever know about this enum, and you can change/extense it without problems with all benefits from 1. If you based your code on some private logic, something went completely wrong on your develop.
Example 3. I developing often changed and extensible module for a wide range of applications. It be strange solution to use enums, or any other constants for determine public logic/interface. If you add new enum number on client-server arhitecture you can crash, but with constants you can get unpredictable state of model, and even save it to disk. I offen prefere crash over unpredictable state. This shows us, that back copability/extension problem is problem of our develops not enums. If your understand what enums is not sutable in this case just dont use them. I think we have enough competence to choose.
Main difference between consts and compile time enum on my opinion is that enums has two main contracts.
Enums how many other things have their disadvantages.
F.e it can`t be changed without brake copabylity. But if your know O from SOLID principles this applies not only to enum but also to development in general. Someone can say, i make my program with parrallel logic and mutable structs ugly. Let's forbid mutable structures? Instead of this we can add mutable/no mutable structs and let developers to choose.
After all that has been said, I want to note that Iota also has its disadvantages.
You try to give an example where using of enums are ridiculous
Excuse my bluntness, but after this comment I don't think you have a lot of ground to stand on here.
And I wasn't even doing what you say - that is, giing an example of where using enums is ridiculous. I took an example that was supposed to show how they are necessary, and illustrate how they hurt.
We can reasonably disagree, but we should at least all argue in good faith.
Example 1
I might give you "register names" as something that truly is not changeable, but in regards to protocol values, I am adamant that the position of having them take arbitrary values for extensibility and compatibility is reasonable. Again, proto2 -> proto3 contained exactly that change and it did so from learned experience.
And either way, I don't see why a linter wouldn't be able to catch this.
i got all 20 constants and if they have same type and similar names i can make errors. If project is big you will got this error.
If you are mistyping names, having closed enums won't help you. Only if you don't use the symbolic names and use int/string-literals instead.
Example 2
Personally, I tend to put "single package" firmly on the line of "not a large project". Thus, I consider it far less likely that you forget a case or to change a code location, when extending an enum.
And either way, I don't see why a linter wouldn't be able to catch this.
Example 3
That is the most common use-case presented for enums, though. Case in point: This specific issue uses them as a justification. Another case that is often mentioned are syscalls - a client-server architecture in disguise. The generalization of this example is "any code where two or more independently developed components exchange such values", which is incredibly broad, covers the vast majority of use-cases for them and, under the gradual-repair-model, also any exported API.
FTR, I'm still not trying to convince anyone that enums are harmful (I'm sure I won't). Just to explain how I came to the conclusion that they are and why I find the arguments in their favor unconvincing.
It always has int type,
iota
may (not necessarily, but whatever), but const
blocks don't, they can have a variety of constant types - in fact, a superset of most commonly proposed enum implementations.
You need calculate values in head.
Again, you can not use this as an argument in favor of enums; you can write out the constants just as you can in an enum-declaration.
Iota expression is code expression you also need to test this.
Not every expression has to be tested. If it is immediately obvious, testing is overkill. If it isn't, write down the constants, you'd do that in a test anyway.
iota
isn't the currently recommended way to do enums in Go - const
declarations are. iota
only serves as a more general way of saving typing when writing down consecutive or formulaic const declarations.
And yes, the open enums of Go have drawbacks, obviously. They've been mentioned above, extensively: You may forget a case in a switch, leading to bugs. They don't namespace. You might accidentally use a non-symbolic constant that ends up being an invalid value (leading to bugs).
But it seems more productive to me, to talk about these downsides and measure them against the downsides of any proposed solution, than to take a fixed solution (enum type) and argue about its specific tradeoffs solve the problems.
To me, most of the downsides can be mostly solved pragmatically in the current language, with a linter-tool detecting const-declarations of a certain kind and check their usages. Namespacing can't be solved this way, which isn't great. But there may be a different solution to that problem, than enums, too.
I might give you "register names" as something that truly is not changeable, but in regards to protocol values, I am adamant that the position of having them take arbitrary values for extensibility and compatibility is reasonable. Again, proto2 -> proto3 contained exactly that change and it did so from learned experience.
This why i said established values. F.e wav format base dont changed for many years and get great back capability. If it where new values you can stay use enums and add some values.
If you are mistyping names, having closed enums won't help you. Only if you don't use the symbolic names and use int/string-literals instead.
Yes it dont help me to make good names, but they can help to organise some values with one name. It make develop process faster in some cases. It can reduce number of variants with autotyping to one.
That is the most common use-case presented for enums, though. Case in point: This specific issue uses them as a justification. Another case that is often mentioned are syscalls - a client-server architecture in disguise. The generalization of this example is "any code where two or more independently developed components exchange such values", which is incredibly broad, covers the vast majority of use-cases for them and, under the gradual-repair-model, also any exported API.
But using/not using constants/enums dont remove the core of problem, you still need think about back copability. I want to say that the problem is not in enums/consts but in our using cases.
Personally, I tend to put "single package" firmly on the line of "not a large project". Thus, I consider it far less likely that you forget a case or to change a code location, when extending an enum.
In this case you still have benefits of name convetion and compile time check,
Not every expression has to be tested. If it is immediately obvious, testing is overkill. If it isn't, write down the constants, you'd do that in a test anyway.
Ofcouse i understand that no all line of code must be tested, but if you have precedent you must test this or rewrite. I know how to make this without iota, but my old example is just a joke.
Again, you can not use this as an argument in favor of enums; you can write out the constants just as you can in an enum-declaration.
It is not argument for enums.
@Merovius
If we are talking about closed enums (which is what people usually talk about, when they talk about enums) or enums with exhaustiveness-checks, then they certainly are special. Because they are not extensible.
Nor are they safely extensible.
If you have
package p
type Enum int
const (
A Enum = iota
B
C
)
func Make() Enum {...}
func Take(Enum) {...}
and
package q
import "p"
const D enum = p.C + 1
within q
it is safe to use D
(unless the next version of p
adds its own label for Enum(3)
) but only as long as you never ever pass it back to p
: You can take the result of p.Make
and transition its state to D
but if you call p.Take
you have to make sure it's not being passed q.D
AND it has to ensure it only gets one of A
, B
, C
or you have a bug. You can work around this by doing
package q
import "p"
type Enum int
const (
A = p.A
B = p.B
C = p.C
D = C + 1
)
// needs to return an error if passed D or an unknown state of Enum
func To(Enum) (p.Enum, error) {...}
// needs to return an error if p.Enum has a value not known to the author
// at the time this package was written.
func From(p.Enum) (Enum, error) {...}
With or without a closed type in the language you have all the issues of having a closed type but without the compiler watching out for you.
Your example only covers the process-boundary (by talking about network-errors), not the package-boundary. How will packages behave, if I add a "InvalidInternalState" (to make something up) to FromTheNetwork? Do I have to fix their switches before they compile again? Then it's not extensible in the gradual repair model. Do they require a default-case to compile in the first place? Then there doesn't seem to be any point to enums.
With just enum types you'd still have to do like the above and define your own version with the extra state and write conversion functions.
However, sum types are composable even when used as enums, so you can "extend" one quite naturally and safely in this manner. I gave an example, but to be more explicit, given
package p
type Enum pick {
A, B, C struct{}
}
Enum
can be "extended" with
package q
import "p"
type Enum pick {
P p.Enum
D struct{}
}
and this time it's completely safe for a new version of p
to add D
. The only downside is that you have to double-switch to get to the state of a p.Enum
from inside a q.Enum
but it's explicit and clear and, as I mentioned, your editor could spit the skeleton of the switches out automatically.
But for that we don't need actual enums, as types. Such a linting tool could also heuristically check for const-declarations using iota, where every case is of a given type and consider that "an enum" and perform the checks you want. I'd be completely on board with a tool using these "enums by convention" to help autocompletion or linting that every switch needs to have a default or even that every (known) case must be checked.
There are two problems with this.
If you have a defined integral type with lables given to a subset of its domain via const/iota:
One, it can represent a closed or open enum. While mostly used to simulate a closed type, it could also be used simply to give names to commonly used values. Consider an open enum for an imaginary file format:
const (
//Name is the ID of a record field
Name Record = iota
//EmpID is the ID of an employee ID field
EmpID
//Intermediate values are reserved for future versions
//Custom is the base of custom fields. Any custom field must have a unique ID greater than Custom.
Custom Record = 42
)
This is not saying that 0, 1, and 42 are the domain of the Record type. The contract is much more subtle and would require dependent types to model. (That would definitely be going too far!)
Two, we could heuristically assume that a defined integral type with constant labels means that the domain is restricted. It would get a false positive from the above, but nothing's perfect. We could use go/types to extract this pseudo-type from the definitions and then go through and find all switches over values of that type and make sure that they all contain the necessary labels. This may be helpful, but we have not shown exhaustiveness at this point. We have ensured coverage of all valid values but not proved that no invalid values have been created. Doing so is not possible. Even if we could find every source, sink, and transformation of values and abstractly interpret them to gurantee statically that no invalid value was created, we still wouldn't be able to say anything about the value at runtime as reflect is unaware of the true domain of the type since it's not encoded in the type system.
There's an alternative to enum and sum types here that gets around this, though it has its own issues.
Let's say the type literal range m n
creates an integral type that is at least m and at most n (For all v, m ≤ v ≤ n). With that we could limit the domain of the enum, like
package p
type Enum range 0 2
const (
A Enum = iota
B
C
)
Since the size of the domain = the number of labels, it is possible to lint with 100% confidence whether a switch statement exhausts all possibilities. To extend that enum externally, you would absolutely need to create type conversion functions to handle the mapping, but I still maintain that you need to do that anyway.
Of course, that's actually a surprisingly subtle family of types to implement and would not play that well with the rest of Go. It also doesn't have a lot of uses outside of this and some niche use cases.
We can disagree about the importance of keeping gradual repair and extensibility possible - for example, a lot of people believe that semantic versioning is a better solution to the problems it solves. But if you find them important, it is perfectly valid and reasonable to see enums as either harmful or pointless. And that was the question I was replying to: How people can reasonably make the tradeoff of requiring a check everywhere, instead of having it in the compiler. Answer: By valuing extensibility and evolution of APIs, which makes these checks necessary at the usage-site anyway.
For basic enum types, I agree. At the start of this discussion, I would have simply been unhappy if they were chosen over sum types, but now I understand why they would be harmful. Thanks to you and @griesemer for clarifying that for me.
For sum types, I think what you said is a valid reason to not require switches be exhaustive at compile time. I still think that closed types have a number of benefits and that of the three examined here, sum types are the most flexible without the downsides of the others. They allow a type to be closed without inhibiting extensibility or gradual repair while avoiding errors caused by illegal values, like any good type does.
The main reason I use golang over python and javascript and other common typeless languages is the type safety. I did a lot with Java and one thing I miss in golang that Java provides is safe enums.
I would disagree to be able to differentiate the types with enums. If you need ints, just stick to ints instead, like Java does. If you need safe enums, I would suggest following syntax.
type enums enum {
foo, bar, baz
}
@rudolfschmidt, I agree with you, it might look like that too:
type DaysOfTheWeek enum {
Monday
Tuesday
}
But it has a small pitfall - we have to be able to take control over enum
in cases when we have to validate data, transform them into JSON or interact with or FS.
If we blindly assume that enum is a set of unsigned integers we can ended up with a iota.
If we want to innovate we have to think about usage convenience.
For example, how easily can I validate that value inside incoming JSON is valid element of enum?
What if I tell you that software is changing?
Lets say we have list of cryptocurrencies:
type CryptoCurrency enum {
BTC
ETH
XMR
}
We exchange data with multiple thirdparty systems. Lets says you have thousands of them.
You have long history, number of data in storage. Time goes, let's say, BitCoin eventually dies. Nobody is using it.
So you decide to drop it from structure:
type CryptoCurrency enum {
ETH
XMR
}
This is causing change in data. Because all values of enum shifted. It is OK for you. You can run migration for your data. What is about your parners, some of them are not moving that quickly, some don't have resources or just can't do that for number of reasons.
But you have ingest data from them. So you will end up having 2 enums: old and new; and a data mapper using both.
That tells us to give flexibility of definition for enums and abilities to validate and marshall/unmarshall those kinds of data.
type CryptoCurrency enum {
ETH = 1, // reminds const?
XMR = 2
}
// this is real life case
v := 3
if v is CryptoCurrency {
// right?
} else {
// nope, provide default value
v = CryptoCurrency.ETH
}
We have to think about applicability of enums and use cases.
We can learn 2 new keywords if it can save us thousands of lines of boilerplate code.
The middle ground is indeed to have the ability to validate enum values without restricting them in what those values can be. Enum type pretty much stays the same - it's a bunch of named constants. Variable of enum type can be equal to any value of enum's underlying type. What you add on top of that is the ability to validate a value to see, does it contain a valid enum value or not. And there could be other bonuses like stringification.
Very often I'm in a situation where I have a protocol (protobuf or thrift) with a bunch of enums all over the place. I have to validate each one of them and, if enum value is unknown to me, throw that message away and report an error. There's no other way I can handle that kind of message. With languages where enum is just a bunch of constants I have no other way but to write huge amounts of switch statements checking all possible combinations. That's a big amount of code and mistakes are bound to be in it. With something like C# I can use built-in support for validating enums which saves a lot of time. Some protobuf implementations are actually doing that internally and throw exception if that's the case. Not to mention how easy logging becomes - you get stringification out of the box. It's nice that protobuf generates Stringer implementation for you but not everything in your code is protobuf.
But the ability to store any value is helpful in other cases where you don't want to throw messages away but do something with them even if it's invalid. Client usually can throw message away but on the server side often you need to store everything in the database. Throwing content away is not an option there.
So for me there's a real value in ability to validate enum values. It would save me thousands of lines of boilerplate code that does nothing but validation.
It seems pretty simple to me, to provide this functionality as a tool. Part of it already exists in the stringer-tool. If there's a tool that you'd call like enumer Foo
, which would generate fmt.Stringer
methods for Foo
and (say) a Known() bool
method that checks if the stored value is in the range of known values, would that alleviate your problems?
@Merovius in the absence of anything else it would be useful. But I'm generally against autogen. Only case where it's useful and just works is things like protobuf where you have pretty stable protocol that can be compiled once. Using it for enums in general feels like a crutch for a simplistic type system. And looking at how Go is all about type safety, that feels against the philosophy of the language itself. Instead of language helping, you start developing this infrastructure on top of it that's not really a part of the language ecosystem. Leave external tools for vetting, not for implementing what's missing from the language.
. Using it for enums in general feels like a crutch for a simplistic type system.
Because it is - Go's type system is famously and intentionally simplistic. But that was not the question, the question was, if it would alleviate your problems. Apart from "I don't like it" I don't really see how it doesn't (if you assume open enums anyway).
And looking at how Go is all about type safety, that feels against the philosophy of the language itself.
Go is not "all about type safety". Languages like Idris are all about type safety. Go is about large-scale engineering problems and as such it's design is driven by the problems it is trying to solve. For example, its type system does allow to catch a wide range of bugs due to API changes and enables some large-scale refactorings. But it is also intentionally kept simple, to ease learning, reduce divergence of codebases and increase readability of third-party code.
As such, if the use case you are interested in (open enums) can be solved without a language change, by a tool that generates just as easily readable code, then that seems very much in line with Go's philosophy. In particular, adding a new language feature that is a subset of the functionality of an existing one does not seem in line with Go's design.
So, to reiterate: It would be helpful if you could expand how using a tool that generates the boilerplate you are concerned with does not solve the actual problem - if nothing else, then because understanding that is necessary to inform the design of the feature anyway.
I've combined some of the ideas from the discussion, what do you think about it?
Some basic informations:
Name()
will return the name of the enum variableIndex()
will return the enum index, which is automatically increasing. It starts where an array starts.Code:
```go
package main
//Example A
type Country enum[struct] { //enums can extend other types (look example B)
Austria("AT", "Austria", false) //Will be accessible like a const, but with type as
Germany("DE", "Germany", true) //prefix (e.g. Country.Austria)
//The struct will automatically begin when it doesn't match the format EnumName(...) anymore
Code, CountryName string
HasMerkel bool //Totally awesome
}
//Enums can have methods like every other type
func (c Country) test() {}
func main() {
println(Country.Austria.CountryName) //Austria
println(Country.Germany.Code) //DE
/* Prints:
Austria
0
Germany
1
*/
for name, country := range Country {
println(name) //Austria
println(name == country.Name()) //true ; also autogenerated
println(country.Index()) //Auto generated increasing index
}
}
//Example B
type Coolness enum[int] {
VeryCool(10)
Cool(5)
NotCool(0)
}```
@sinnlosername I think enums should be something that are very easy to understand. Combining some of the ideas presented in previous discussion might not necessarily lead to the best idea for an enum.
I believe that the following would be simple to understand:
type Day enum {
Monday
Tuesday
...
Sunday
}
Stringer
interface):func (d Day) String() string {
switch d {
case Monday:
return "mon"
case Tuesday:
return "tues"
...
case Sunday:
return "sun"
}
}
It's that simple. The benefit of this is allowing stronger type safety when passing enums around.
func IsWeekday(d Day) bool {
return d != Saturday && d != Sunday
}
If I were to use string
constants here to represent a Day
, IsWeekday
would say that any string that is not "sat"
or "sun"
is a weekday (i.e., what would/should IsWeekday("abc")
return?). In contrast, the domain of the function show above is restricted, thus allowing the function to make more sense with respect to its input.
@ljeabmreosn
it should probably be
func IsWeekday(d Day) bool {
return d != Day.Saturday && d != Day.Sunday
}
I gave up waiting the golang team to improve the language in a necessary way. I can everyone recommend to take a look to rust lang, it has already all that desired features like enum and generics and more.
We are in 14. May 2018 and we still discuss about enum support. I mean what the hell? Personally I am disappointed from golang.
I can understand that it can get frustrating while waiting for a feature. But posting non-constructive comments like this does not help. Please keep your comments respectful. See https://golang.org/conduct.
Thanks.
@agnivade I have to agree with @rudolfschmidt. GoLang is definitely not my favorite language too because of this lack of features, APIs and this too much resistance to change or accepting the past mistakes by the Go creators. But I have no choice at this moment because I was not the decision maker on what language to choose for my latest project in my workplace. So I have to work with all its shortcomings. But to be honest it is like torture writing codes in GoLang;-)
I gave up waiting the golang team to improve the language in a necessary way.
Actually core features of every modern language are necessary. GoLang has some good features, but it won't survive if the project stays conservative. Features like enums or generics have no disadvantages for people who don't like them, but they have many advantages for people who want to use them.
And don't tell me "but go wants to stay simple". There is a huge difference between "simple" and "no real features". Java is very simple but it got many features go is missing. So either the java devs are wizards or this argument is just bad.
Actually core features of every modern language are necessary.
Sure. Those core features are called Turing completeness. _Everything_ else is a design choice. There's a lot of space between Turing completeness and C++ (for example) and in that space you can find a lot of languages. The distribution suggests no global optimum exists.
GoLang has some good features, but it won't survive if the project stays conservative.
Possibly. So far it's still growing. IMO, it'd not be still growing if it hadn't been conservative. Both of ours opinions are subjective and technically not worth much. It's the experience and taste of the designers what rules. It's fine to have a different opinion, but that cannot warrant the designers will share it.
BTW, if I imagine what would Go be today if 10% of the features people demand were adopted, I'd by now probably use Go no more.
Actually you just missed the most imporsnt argument of my answer. Maybe because it's already a counter to some of the things you said.
"Features like enums or generics have no disadvantages for people who don't like them, but they have many advantages for people who want to use them."
And why do you think this conservativeness is a reason for the growth of golang? I think this is more likely related to the efficiency of golang and the great set of standard libraries.
Also java experienced a kind of "crash" as they tried to change important things in java 9, which probably caused many people to search an alternative. But look at java before this crash. It was constantly growing because it got more and more features which made the lifes of developers easier.
"Features like enums or generics have no disadvantages for people who don't like them, but they have many advantages for people who want to use them."
That's very clearly not true. Every feature will eventually make it to the stdlib and/or packages I want to import. _Everyone_ will have to deal with the new features regardless of liking them or not.
So far it's still growing. IMO, it'd not be still growing if it hadn't been conservative
I don't think its slow growth (if any) is due to conservativeness but rather standard library, already existing set of language features, tooling. That's what brought me here. Adding language features would not change anything for me in that respect.
If we look at C# and Typescript or even Rust/Swift. They're adding new features like mad. C# is still in the top languages fluctuating up and down. Typescript is growing very fast. Same for Rust/Swift. Go, on the other hand, exploded in popularity in 2009 and 2016. But between that was not growing at all and actually loosing. Go has nothing to give new developers if they already knew about it and didn't choose it earlier for some reason. Exactly because Go is stagnating in it's design. Other languages adding feature not because they don't have anything else to do but because actual user base demands it. People need new features so that their code base stays relevant in ever changing problem domains. Like async/await. It was needed to solve an actual problem. Not surprising that you can see it in many languages now.
There eventually will be Go 2 and you can be absolutely sure it will bring many new developers. Not because it's new and shiny but because new features might convince someone to finally switch or try it out. If conservativeness was that important we would even have these proposals.
I don't think its slow growth (if any) is due to conservativeness but rather standard library, already existing set of language features, tooling. That's what brought me here.
And that's the result of beeing conservative. If the language breaks something/everything every [half]year or so, you would nothing of what you say you value about Go because there will be far less people bringing you that.
Adding language features would not change anything for me in that respect.
Are you sure about that? See above.
BTW, have you seen the 2017 Survey results?
If the language breaks something/everything every [half]year or so
Then don't break anything. C# added a ton of features and never did it violate backwards compatibility. That's not an option for them either. Same for C++ I think. If Go can't add feature without breaking something, then it's a problem with Go and, possibly, how it's implemented.
BTW, have you seen the 2017 Survey results?
My comment is based on 2017/2018 surveys, TIOBE index and my general observations of what's going on with various languages.
@cznic
Everyone has to deal with them, but you dont need to use them. If you prefer to write your code with iota enums and maps, then you can still do this. And if you dont like generics, then use libraries without them. Java proofs its possible to have both.
Then don't break anything.
Good idea. However, many, if not most of the proposed changes to the language, _including this very one_, are breaking changes.
C# added a ton of features and never did it violate backwards compatibility.
Please check your facts: Visual C# 2010 Breaking Changes. (first web search result, I can only guess if it is or is not the only example.)
My comment is based on 2017/2018 surveys, TIOBE index and my general observations of what's going on with various languages.
Well, how can you then see the language not growing while the survey results show a 70% year to year growth of the number of respondents?
How do you define "breaking changes"? Every line of go code would still work after adding enums or generics.
Everyone has to deal with them, but you dont need to use them. If you prefer to write your code with iota enums and maps, then you can still do this. And if you dont like generics, then use libraries without them. Java proofs its possible to have both.
I cannot agree. Once the language gets, for example generics, then even while I may not use them, they'll be used all around. Even when internally w/o changing the API. The result is that I'm very much affected by them, because there's for sure no way to add generics to the language without slowing down building of any programs using them. Aka "No free lunch".
How do you define "breaking changes"? Every line of go code would still work after adding enums or generics.
Of course not. This code would no more compile with this proposal:
package foo
var enum = 42
The word necessary does not mean "what I want".
sure, it doesn't mean, and I have never meant it. Of course you can answer that such features are not necessary, but then I can answer what is necessary in general. Nothing is necessary and we can go back to pen and paper.
Golang claims to be a language for large teams. I am not sure if you can use golang to develop large code bases. For that you need static compilation and type checking to avoid runtime errors as much as possible. How can you do so without enums and generics? Those features are not even fancy or nice to have but absolute essential for serious development. If you do not have them, you end up using interfaces{} everywhere. What is the point of having datatypes if you are forced to use interfaces{} in your code?
Sure if you do not have choice you will do also, but why should you if you have alternatives like rust that offer all that stuff already and is even faster in execution than golang can ever be? I am really wondering if go has a future with that mindset of:
The word necessary does not mean "what I want".
I respect all contribution to open source and if golang is a hobby project its fine how it is, but golang wants to be take seriously and at the moment its more a toy for some bored developers and I do not see a will to change that.
The api doesn't need to be changed, only new api parts might use generics, but there are probably always alternatives without generics in the internet.
And both, a bit slower compiling and variables called "enum" are minimal effects. Actually 99% of people won't even notice it and the other 1% will just need to add some small changes which are tolerable. This is not comparable to e.g. java's jigsaw which fu... up everything.
And both, a bit slower compiling and variables called "enum" are minimal effects. Actually 99% of people won't even notice it and the other 1% will just need to add some small changes which are tolerable.
Everyone would be happy if someone can come with a design and implementation having such wonderful performance. Please contribute to #15292.
However, if this is a game named "pulling any numbers in my favor w/o any backing data whatsoever" than sorry, but I do not participate.
Have you got any numbers to the speed difference with generics?
And yes, those numbers are not backed by any data, because they shall just tell thst the probability of having variables called "enum" is no very high.
I'd like to remind everyone that there are lots of people subscribed to this issue for the specific question of whether and how enums might be added to Go. The general questions of "is Go a good language?" and "should Go focus more on delivering features?" are probably better discussed in a different forum.
Have you got any numbers to the speed difference with generics?
No, that's why I posted none. I only posted, that the cost cannot be zero.
And yes, those numbers are not backed by any data, because they shall just tell thst the probability of having variables called "enum" is no very high.
That's mixed up. Slowdown was about generics. "enum" was about backward compatibility and your false "_Every_ line of go code would still work after adding enums or generics." claim. (emphasizes mine)
@Merovius You're rigth, I'm now shutting up.
Bringing this back to enum types, which is what this issue is about, I completely understand the argument for why Go needs generics, but I am a lot more shaky on the argument for why Go needs enum types. In fact, I asked this above in https://github.com/golang/go/issues/19814#issuecomment-290878151 and I'm still shaky on it. If there was a good answer to that, I missed it. Could someone repeat it or point to it? Thanks.
@ianlancetaylor I don't think the use case is complicated, wanting a type safe way to ensure that a value belongs to predefined set of values, which is not possible today in Go. The only workaround is to manually validate at every possible entrypoint into your code, including RPCs and function calls, which is inherently unreliable. The other syntactic niceties for iterating make many common use cases easier. Whether or not you find that valuable is subjective, and obviously none of the arguments above have been compelling for the powers that be, so I've essentially given up on this ever being addressed at a language level.
@ianlancetaylor: everything has to do with type safety. you use types to minimize the risk of runtime errors because of a typo or using incompatible types. At the moment you can write in go
if enumReference == 1
because at the moment enums are just numbers or other primitive datatypes.
That code should not be possible at all and should be avoided. The same discussion you had in the Java community years ago, thats the reason they have introduced enums because they understood the importance.
You should be only able to write
if enumReference == enumType
you do not need too much fantasy to imagine in which scenarios if enumReference == 1
can happen in a more hidden way and lead to additional problems that you will see only in runtime.
I just want to mention: Go has its potential, but is strange that things and concepts that are proven and understood since years are discussed here like you discuss new concepts or paradigms of programming. If you have an alternative way to ensure type safety maybe there is something better than enums but I do not see it.
Go has its potential, but is strange that things and concepts that are proven and understood since years are discussed here like you discuss new concepts or paradigms of programming.
Afais, especially while following the other discussions about generics, sum types etc... it's not so much about whether to have it, but how to implement it. Javas type system is extremely extensible and well spec'ed. That is a huge difference.
In Go people are trying to come up with ways of adding features to the language, while not increasing the compilers complexity. That usually doesn't work too well and makes them abandon those initial ideas.
While I too think that those priorities are rather nonsensical in their current form and quality, your best shot is to come up with the simplest possible and least disruptive implementation. Anything else will not get you further, imo.
@derekperkins @rudolfschmidt Thanks. I want to be clear that although C++ has enum types, the features you are suggesting are not in C++. So there is nothing obvious about this.,
In general if a variable of an enum type can only accept values of that enum, that would be useless. In particular there must be a conversion from an arbitrary integer to the enum type; otherwise you can't send enums over a network connection. Well, you can, but you have to write a switch with a case for each enum value, which seems really tedious. So when doing a conversion, does the compiler generate a check that the value is a valid enum value during the type conversion? And does it panic if the value is invalid?
Are enum values required to be sequential, or may they take on any value as in C++?
In Go, constants are untyped, so if we permit conversions from integers to an enum type it would be strange to forbid if enumVal == 1
. But I guess we could.
One of the general design principles of Go is that people writing Go write code, not types. I don't yet see any advantage coming from enum types that helps us when writing code. They seem to add a set of type constraints of a kind we generally don't have in Go. For better or for worse, Go doesn't provide mechanisms to control the value of types. So I have to say that to me the argument in favor of adding enums to Go does not yet seem compelling.
I will repeat myself but I'm in favor of keeping enums as they're today and adding features on top of them:
int
should be convertible to any integer enum type.What it provides on top of that is:
-1
is not a valid enum value of some enum type Foo
, stringification should just return -1
.My justification is all about writing code and avoiding bugs. All these tasks are tedious and unnecessary for developer to do by hand or even introduce external tooling complicating code and building scripts. These features cover everything I need from enums without overly complicating and restricting them. I don't think Go needs anything like enums in Swift or even Java.
There were discussions about validating at compile time that switch statement covers every possible enum value. With my proposal it will be useless. Doing exhaustion checks will not cover invalid enum values, so you still have to have default case to handle those. That's necessary to support gradual code repair. Only thing we can do here, I think, is produce a warning if switch statement doesn't have a default case. But that can be done even without changing the language.
@ianlancetaylor I think your argument has some flaws.
In general if a variable of an enum type can only accept values of that enum, that would be useless. In particular there must be a conversion from an arbitrary integer to the enum type; otherwise you can't send enums over a network connection.
Abstraction for the programmer is fine; Go
provides many abstractions. For example, the following code does not compile:
package main
import "fmt"
const NULL = 0x0
func main() {
str := "hello"
if &str == NULL {
fmt.Println("str is null")
}
}
but in C
, a program of this style would compile. This is because Go
is strongly typed and C
is not.
The indices of enums might be internally stored but be hidden to the user as an abstraction, akin to the addresses of variables.
@zerkms Yes, that is one possibility, but given the type of d
, type inference should be possible; however, the qualified use of enums (as in your example) is a bit easier to read.
@ianlancetaylor that's a very C version of enums you're talking about. I'm sure a lot of people but would like that but, imo:
Enum values should not have any numeric properties. The values of each enum type should be their own finite universe of discrete labels, enforced at compile time, unrelated to any numbers or other enum types. The only thing you can do with a pair of such values is ==
or !=
. Other operations can be defined as methods or with functions.
The implementation is going to compile those values down to integers but that's not a fundamental thing with any legitimate reason to be exposed directly to the programmer, except by unsafe or reflection. For the same reason that you can't do bool(0)
to get false
.
If you want to convert an enum to or from a number or any other type, you write out all the cases and include error handling appropriate to the situation. If that's tedious, you use a code generator like stringer or at least something to fill out the cases in the switch statement.
If you're sending the value out of process, an int is good if you're following a well-defined standard or you know you're talking to another instance of your program that was compiled from the source or you need to make things fit in the smallest space possible even if that might cause problems, but generally none of these hold and it's better to use a string representation so the value is unaffected by the source order of the type definition. You don't want process A's Green to become process B's Blue because someone else decided Blue should be added before Green to keep things alphabetical in the definition: you want unrecognized color "Blue"
.
It's a good, safe way to represent a number of states abstractly. It leaves the program to define what those states mean.
(Of course, often you want to associate data with those states and the type of that data varies from state to state . . .)
@ljeabmreosn My point was that if Go permits converting from integers to enum types, then it would be natural for an untyped constant to automatically convert to an enum type. Your counter-example is different, since Go does not permit converting from integers to pointer types.
@jimmyfrasche If you have to write out a switch to convert between integers and enum types, then I agree that would work cleanly in Go, but frankly it doesn't seem sufficiently useful to add to the language by itself. It becomes a special case of sum types, for which see #19412.
There are lots of proposals here.
A general comment: For any proposal that doesn’t expose an underlying value (e.g. an int) that you can convert to and from for an enum, here are some questions to answer.
What is the zero value of an enum type?
How do you get from one enum to another? I suspect that for many people, days of the week is a canonical example of an enum, but one might reasonably want to “increment” from Wednesday to Thursday. I wouldn’t want to have to write a big switch statement for that.
(Also, regarding “stringification”, the correct string for a day of the week is language- and locale-dependent.)
@josharian stringification usually means converting names of enum values to strings automatically by the compiler. No localization or anything. If you want to build something on top of that, like localization, then you do it by other means and other languages provide rich language and framework tools to do that.
For example, some C# types have ToString
override that also takes culture info. Or you can use DateTime
object itself and use its ToString
method that accepts both format and culture info. But these overrides are not standard, object
class that everyone inherits from has only ToString()
. Pretty much like stringer interface in Go.
So I think localization should be outside of this proposal and enums in general. If you want to implement it, then do it some other way. Like custom stringer interface, for example.
@josharian Since, implementation-wise, it would still be an int and zero values are all-bits zero, the zero value would be the first value in source order. That's kind of leaking the int-iness but actually quite nice because you can choose the zero value, deciding whether a week starts on Monday or Sunday, for example. Of course, it's less nice that the order of the remaining terms don't have such an impact and that reordering the values can have non-trivial impacts if you change the first element. This isn't really any different than const/iota, though.
Re stringification what @creker said. To expand, though, I would expect
var e enum {
Sunday
Monday
//etc.
}
fmt.Println(reflect.ValueOf(e))
to print Sunday not 0. The label is the value, not its representation.
To be clear I'm not saying it should have an implicit String method—just that the labels be stored as part of the type and accessible by reflection. (Maybe Println calls Label() on a reflect.Value from an enum or something like that? Haven't looked deeply into how fmt does its voodoo.)
How do you get from one enum to another? I suspect that for many people, days of the week is a canonical example of an enum, but one might reasonably want to “increment” from Wednesday to Thursday. I wouldn’t want to have to write a big switch statement for that.
I think reflection or a big switch is the correct thing. Common patterns can easily be filled in with go generate to create methods on the type or factory funcs of that type (and perhaps even recognized by the compiler to lower it to arithmetic on the representation).
It doesn't make sense to me to assume that all enums have a total order or that they are cyclic. Given type failure enum { none; input; file; network }
, does it really make sense to enforce that invalid input is less than a file failure or that incrementing a file failure results in a network failure or that incrementing a network failure results in success?
Assuming that the primary use is for cyclical ordered values, another way to handle this would be to create a new class of paramaterized integer types. This is bad syntax, but, for discussion, let's say it's I%N
where I
is in an integer type and N
is an integer constant. All arithmetic with a value of this type is implicitly mod N. Then you could do
type Weekday uint%7
const (
Sunday Weekday = iota
//etc.
so Saturday + 1 == Sunday and Weekday(456) == Monday. It's impossible to construct an invalid Weekday. It could be useful outside of const/iota, though.
For when you don't want it to be number-y at all, as @ianlancetaylor pointed out what I really want is sum types.
Introducing an arbitrary modular arithmetic type is an interesting suggestion. Then enums could be of this form, which gets you a trivial String method:
var Weekdays = [...]string{"Sunday", ..., "Saturday"}
type Weekday = uint % len(Weekdays)
Combined with arbitrary sized ints, this also gets you int128, int256, etc.
You could also define some built-ins:
type uint8 = uint%(1<<8)
// etc
The compiler can prove more bounds than before. And APIs can provide more precise assertions via types, e.g. function Len64
in math/bits
can now return uint % 64
.
When working on a RISC-V port, I wanted a uint12
type, since my instruction encoding components are 12 bits; that could have been uint % (1<<12)
. Lots of bit-manipulation, particularly protocols, could benefit from this.
The downsides are significant, of course. Go tends to favor code over types, and this is type-heavy. Operations like + and - can suddenly become as expensive as %. Without type parametricity of some kind, you'll probably have to convert to the canonical uint8
, uint16
, etc. in order to interoperate with almost any library function, and the conversion back can hide bounds failures (unless we have a way to do panic-on-out-of-range conversion, which introduces its own complexity). And I can see it being overused, e.g. using uint % 1000
for HTTP status codes.
An interesting idea nevertheless, though. :)
Other minor replies:
That's kind of leaking the int-iness
This makes me think they really are ints. :)
Common patterns can easily be filled in with go generate
If you have to generate code with enums anyway, then it seems to me you may as well generate String functions and bounds checks and the like and do enums with code generation instead of the weight of language support.
It doesn't make sense to me to assume that all enums have a total order or that they are cyclic.
Fair enough. This makes me think that having a handful of concrete use cases would help bring clarity to exactly what we want out of enums. I kind of suspect that there won't be a clear set of requirements, and that emulating enums using other language constructions (i.e. the status quo) will end up making the most sense. But that's only a hypothesis.
Re stringification what @creker said.
Fair enough. But I do wonder how many cases end up being like days of the week. Anything user-facing, for sure. And stringification seems to be one of the primary requests for enums.
@josharian Enums that are really ints would probably need a similar mechanism though. Otherwise, what's enum { A; B; C}(42)
?
You can say it's a compiler error but that doesn't work in more complicated code as you can convert to and from ints at runtime.
It's either A or a runtime panic. In either case you're adding an integral type with a limited domain. If it's a runtime panic you're adding an integral type that panics on overflow when the others wrap around. If it's A, you've added uint%N with some ceremony.
The other option is to let it be none of A, B, or C, but that's what we have today with const/iota so there are no gains.
All the reasons you say int%N won't make it into the language seem to apply equally to enums that are kinda ints. (Though I would be in no way angry if something like them were included).
Taking the int-iness away removes that conundrum. It requires code generation for cases when want to add back some of that int-iness but it also gives you the choice to not do that, which lets you control how much int-iness to introduce and of what kind: you can add no "next" method, a cyclic next method, or a next method that returns an error if you fall off the edge. (You also don't end up with stuff like Monday*Sunday - Thursday
being legal). The extra rigidity makes it a more malleable building material. A discriminated union nicely models the non-int-y variety: pick { A, B, C struct{} }
, among other things.
The major benefits of having information like this in the language are that
The major benefits of having information like this in the language are that: Illegal values are illegal.
I think it's important to emphasize that not everyone sees this as a benefit. I certainly don't. It often makes it easier when consuming values, it often makes it harder when producing them. Which you weigh heavier seems, so far, up to personal preference. Thus, so is the question of whether it's a net benefit overall.
I also don't see the point in disallowing illegal values. If you already have means to check for validity yourself (like in my proposal above), what benefit does that limitation give? For me, it only complicates things. In my applications enums for the majority of cases could contain invalid/unknown values and you had to work around that depending on the application - throw away completely, downgrade to some default or save as it is.
I imagine that strict enums that disallow invalid values could be useful in very limited cases where your app is isolated from the outer world and have no way of receiving an invalid input. Like internal enums that only you can see and use.
const with iota is not safe in compile time, checking would be delayed to runtime, and the safe checking is not on type level. So I think iota can not replace enum literally, I prefer enum cause it's more powerful.
Illegal values are illegal.
I think it's important to emphasize that not everyone sees this as a benefit.
I don't understand this logic. Types are sets of values. You can't assign a type to a variable whose value is not in that type. Am I misunderstanding something?
PS: I agree that enums are a special case of sum types and that issue should take precedence over this one.
Let me rephrase/be more precise: Not everyone sees it as a benefit for enums to be closed.
If you want to be strict in that way, then a) "Illegal values are illegal" is a tautology and b) thus can't be counted as a benefit. With const-based enums, in your interpretation, illegal values are also illegal. The type just allows a lot more values.
If enums are ints and any int is legal (from the type system's point of view) then the only gain is that the named values of the type are in reflect.
That's basically just const/iota but you don't have to run stringer since the fmt package can get the names using reflection. (You'd still have to run stringer if you wanted the strings to be different from the names in the source).
@jimmyfrasche stringification is just a nice bonus. The main feature for me, as you can read in my proposal above, is the ability to check whether the given value is a valid value of the given enum type at runtime.
For example, given something like this
type Foo enum {
Val1 = 1
Val2 = 2
}
And reflection method like
func IsValidEnum(v {}interface) bool
We could do something like this
a := Foo.Val1
b := Foo(-1)
reflection.IsValidEnum(a) //returns true
reflection.IsValidEnum(b) //returns false
For a real world example you can look at enums in C# which, in my opinion, perfectly captured this middle ground instead of blindly following what Java did. To check for validity in C# you use Enum.IsDefined
static method.
@crecker The only difference between that and const/iota is the
information stored in reflect. That's not a lot of gain for a whole
new type of type.
Slightly crazy idea:
Store the names and values for all consts declared in the same package
as their defined type in a way that reflect can get to. It would be
weird to single out that narrow class of const usage, though.
The main feature for me, as you can read in my proposal above
IMO this illustrates one of the main things dragging out this discussion: A lack of clarity of what the set of "main features" are. Everyone seems to have slightly different ideas about that.
Personally, I still like the format of experience reports to discover that set. There even is one in the list (though, personally, I'd still remark on the fact that the section "What Went Wrong" only mentions what could go wrong, not what actually did). Maybe adding a couple, illustrating where the lack of type-checking lead to outages/bugs or e.g. failure to do large-scale refactorings, would be helpful.
@jimmyfrasche but that solves a big problem in many applications - validating input data. Without any help from the type system you have to do it by hand and that's not something you could do in a couple of lines of code. Having some form of type assisted validation would solve that. Adding stringification on top of that would simplify logging as you would have properly formatted names and not the underlying type values.
On the other hand, making enums strict would severely limit possible use cases. Now you can't use them in protocols easily, for example. To preserve even invalid values you would have to drop enums and use plain value types, possibly converting them to enums later if you need to. In some cases you could drop the invalid value and throw an error. In others you could downgrade to some default value. Either way, you're fighting with restrictions of your type system instead of it helping you avoid errors.
Just look at what protobuf for Java have to generate in order to work around Java enums.
@Merovius regarding the validation, I think I already covered that multiple times. I don't know what more could be added apart from - without validation you have to write huge amounts of pretty much copy-paste code to validate your input. The problem is obvious, as well as how proposed solution could help with that. I don't work on some large-scale application that everyone knows about but errors in that validation code bit me enough times in multiple languages with the same concept of enums that I want to see something done about it.
On the other hand, I don't see (apologies if I missed something) any argument in favor of implementing enums that don't allow invalid values. It's nice and neat in theory, but I just don't see it helping me in real applications.
There're not that many features that people want from enums. Stringification, validation, strict/loose in terms of invalid values, enumeration - that's pretty much it from what I can see. Everyone (including me of course) just shuffles them around at this point. strict/loose seems to be the main point of contention because of their conflicting nature. I don't think everyone will agree to one or another. Maybe the solution could be to incorporate both of them in some way and let the programmer choose but I don't know of any languages that have that to see how it could work in the real world.
@crecker my suggestion to store the consts in export data in the above
circumstances would allow the kind of things you are asking for
without the introduction of a new kind of type.
I’m not sure this is the idiomatic way, and I’m also quite new to the language, but the following works and is concise
type Day struct {
value string
}
// optional, if you need string representation
func (d Day) String() string { return d.value }
var (
Monday = Day{"Monday"}
Tuesday = Day{"Tuesday"}
)
func main() {
getTask(Monday)
}
func getTask(d Day) string {
if d == Monday {
fmt.Println("today is ", d, "!”) // today is Monday !
return "running"
}
return "nothing to do"
}
does not allow you to pass something different than type Day
to func getTask
https://play.golang.org/p/4JZOIG5PbRX
reordering values does not break your code (iota
are tricky instead)
no need for code generators
can be used with maps
https://play.golang.org/p/KlTNWrJpbDi
can be iterated
https://play.golang.org/p/ld3TNtenEkD
you can still pass an anonymous struct to getTask
without the compiler complaining
https://play.golang.org/p/NaV38og7e2h
constant structs are not supported, only var
https://play.golang.org/p/X6KSpACA4N0
What's to stop someone from doing something like this:
NotADay := Day{"NotADay"}
getTask(NotADay)
The consumer of such a variable may or may not catch that with proper checking of expected values (assuming no poor fall through assumptions in switch statements, like anything that's not Saturday or Sunday is a weekday, for instance), but it wouldn't be until runtime. I think one would prefer this type of mistake to be caught at compile time, not runtime.
@bpkroth
By having Day
in its own package and exposing only selected fields & methods, I cannot create new values of type Day
outside of package day
Also, this way I cannot pass anonymous structs to getTask
./day/day.go
package day
type Day struct {
value string
}
func (d Day) String() string { return d.value }
var (
Monday = Day{"Monday"}
Tuesday = Day{"Tuesday"}
Days = []Day{Monday, Tuesday}
)
./main.go
package main
import (
"fmt"
"github.com/somePath/day"
)
func main() {
january := day.Day{"january"} // implicit assignment of unexported field 'value' in day.Day literal
var march struct {
value string
}
march.value = "march"
getTask(march) // cannot use march (type struct { value string }) as type day.Day in argument to getTask
getTask(day.Monday)
}
func getTask(d day.Day) string {
if d == day.Monday {
fmt.Println("today is ", d, "!") // today is Monday !
return "running"
}
return "nothing to do"
}
func iterateDays() {
for _, d := range day.Days {
fmt.Println(d)
}
}
I've never seen in my entire life any other language that insists on not adding the most simple and helpful features like enums, ternary operators, compilation with unused variables, sum types, generics, default parameters, etc...
Is Golang a social experiment to see how stupid devs can be?
@gh67uyyghj Someone marked your comment as off-topic! and I guess someone will do the same to my reply. but I guess the answer to your question is YES. In GoLang being featureless means being featureful so anything that GoLang does not have is actually a feature that GoLang has that other programming languages do not have!!
@L-oris This is a very interesting way to implement enums with types. But it feels awkward, and having an enum keyword (which necessarily complicates the language some more) would make it easier to:
In your example (which is great because it works today) having enums (in some form) implies the need to:
This takes longer (though not _that_ much longer) to read, write, and reason about (discern that it represents and should be used as enums).
Therefore, I think the syntax proposal strikes the right note in terms of simplicity and added value to the language.
Thanks @andradei
Yes it’s a workaround, but I feel the aim of the language is to keep it small and simple
We could also argue we miss classes, but then let's just move to Java :)
I would rather focus on Go 2 proposals, better error handling eg. would provide me way more value than these enums
Returning to your points:
package day
// Day Enum
type Day struct {
value string
}
@L-oris I see. I'm excited about Go 2 proposals as well. I'd argue that generics will increase the complexity of the language more than enums would. But to stick to your points:
I've been thinking a lot about this proposal, and I can see the great value simplicity has on productivity, and why you lean towards keeping it unless a change it clearly necessary. Enums could also change the language so drastically it isn't Go anymore, and assessing the pros/cons of that seems like it will take a long time. So I've been thinking that simple solutions like yours, where the code is still easy to read, are a good solution at least for now.
Guys, really want this feature for the future!. Pointers and the way to define "enums" in _nowadays_ doesn't get along very well. For example: https://play.golang.org/p/A7rjgAMjfCx
My proposal for enum is following. We should consider this as a new type. For example I would like to use enum type with arbitrary structure and following implementation :
package application
type Status struct {
Name string
isFinal bool
}
enum Status {
Started = &Status{"Started",false}
Stopped = &Status{"Stopped",true}
Canceled = &Status{"Canceled",true}
}
// application.Status.Start - to use
It is understandable how marshall this structure and how to work and how to change to string and so on.
And of course if I could override "Next" function would be great.
For that Go would have to support deep immutable structs first. Without immutable types I can imagine you could do this with enums to have the same thing:
type Status enum {
Started
Stopped
}
func isFinal(s Status) bool {
exhaustive switch(s) {
case Started: return false;
case Stopped: return true;
}
}
I think it should look simpler
func isFinal(s Status) bool {
return s == Status.Stopped
}
Logically enums supposed to provide type interface.
I stated earlier enums supposed to be separate.
It is explicitly named constants tied to a specific namespace.
enum Status uint8 {
Started // Status.Started == 0
Stopped // Status.Stopped == 1, etc, like we have used iota
}
// or
enum Status string {
Started // Status.Started == "Started", like it works with JSON
Stopped // Status.Stopped == "Stopped", etc
}
// unless you wanna define its values explicitly
enum Status {
Started "started" // compiler can infer underlying type
Stopped "finished"
}
// and enums are type extensions and should be used like this
type MyStatus Status
MyStatus validatedStatus // holds a nil until initialized
// for status value validation we can use map pattern
if validatedStatus, ok := MyStatus[s]; ok {
// this value is a valid status
// and we can use it later as regular read-only string
// or like this
if validatedStatus == MyStatus.Started {
fmt.Printf("Hey, my status is %s", validatedStatus)
}
}
Enums are type extensions, "constants containers".
Syntax alternatives for those who wants to see it as type
type Status uint8 enum {
Started // Status.Started == 0
Stopped // Status.Stopped == 1, etc, like we have used iota
}
But we can also avoid those explicit top level declarations
type Status enum {
Started // Status.Started == 0
Stopped // Status.Stopped == 1, etc, like we have used iota
}
Validation example remains the same.
but in case
type Status1 uint8 enum {
Started // Status1.Started == 0
Stopped // Status1.Stopped == 1, etc, like we have used iota
}
type Status2 uint8 enum {
Started // Status1.Started == 0
Stopped // Status1.Stopped == 1, etc, like we have used iota
}
How is about Status1.Started == Status2.Started ?
about Marshaling ?
If I change a position?
type Status uint8 enum {
Started // Status.Started == 0
InProcess
Stopped // Status.Stopped == 1, etc, like we have used iota
}
I agree with @Goodwine about immutable types.
Marshaling is an interesting question.
This all depends on how are we going to treat the underlying value. If we are going to use actual values, therefore Status1.Started
would be equal to Status2.Started
.
If we are going with symbolic interpretation those would be considered as different values.
Inserting something will case a change in values (exactly the same way as it goes with iota
).
To avoid this developer has to specify values alongside with declarations.
type Status uint8 enum {
Started 0
InProcess 2
Stopped 1
}
This is obvious thing.
If we want to avoid such issues we have to provide predictable compiler output based on lexical interpretation of the enum values. I assume the simplest way - building a hash table or sticking to symbolic names (strings) unless custom type casting is defined.
I like how Rust is implemented Enums.
Default with no type specified
enum IpAddr {
V4,
V6,
}
Custom type
enum IpAddr {
V4(string),
V6(string),
}
home := IpAddr.V4("127.0.0.1");
loopback := IpAddr.V6("::1");
Complex types
enum Message {
Quit,
Move { x: int32, y: int32 },
Write(String),
ChangeColor(int32, int32, int32),
}
For sure even having simple enums like in C# which are stored as integral types would be great.
The above go beyond enum
s, those are _discriminated unions_, which are indeed more powerful, especially with _pattern matching_, which could be a minor extension to switch
, something like:
switch something.(type) {
case Quit:
...
case ChangeColor; r, g, b := something:
...
case Write: // Here `something` is known to be a string
...
// Ideally Go would warn here about the missing case for "Move"
}
I don't need any compile time checks of enums, as that could be dangerous as mentioned
What I needed several time would have been to iterate over all constants of a given type:
We could do the validation with iota and specifying the end of the list. However using iota for anything else than just inside the code, would be fairly dangerous because stuff will break by inserting a constant at the wrong line ( I know we need to be aware of where we put things in programming, but a bug like that is a whole lot harder to find than other things). Additionally we have no description of what the constant actually stands for when it's a number. That leads to the next point:
A nice extra would be to specify stringify names for it.
What's to stop someone from doing something like this:
NotADay := Day{"NotADay"} getTask(NotADay)
The consumer of such a variable may or may not catch that with proper checking of expected values (assuming no poor fall through assumptions in switch statements, like anything that's not Saturday or Sunday is a weekday, for instance), but it wouldn't be until runtime. I think one would prefer this type of mistake to be caught at compile time, not runtime.
@L-oris So What about this:
package main
import "yet/it/is/not/a/good/practice/in/Go/enum/example/day"
func main()
{
// var foo day.Day
foo := day.Day{}
bar(foo)
}
func bar(day day.Day)
{
// xxxxxxxxxx
}
What we want is NOT RUNTIME SILENCE & Weird BUG cause by the [return "nothing to do"] but a compile-time / coding-time ERROR REPORTING!
UNDERSTAND?
enum
is indeed new type, which is what type State string
does, there is no idiomatic need to introduce a new keyword. Go isn't about saving space in your source code, it is about readability, clarity of purpose.
Lack of type safety, confusing the new string
- or int
-based types for actual strings/ints is the key hurdle. All enum clauses are declared as const
, which creates a set of known values compiler can check against.
Stringer
interface is the idiom for representing any type as human-readable text. Without customization, type ContextKey string
enums this is the string value, and for iota
-generated enums it's the integer, much like XHR ReadyState codes (0 - unsent, 4 - done) in JavaScript.
Rather, the problem lies with the fallibility of custom func (k ContextKey) String() string
implementation, which is usually done using a switch that must contain every known enum clause constant.
In a language like Swift, there is a notion of _an exhaustive switch_. This is a good approach for both the type checking against a set of const
s and building an idiomatic way to invoke that check. The String()
function, being a common necessity, is a great case for implementation.
package main
import (
"context"
"strconv"
"fmt"
"os"
)
// State is an enum of known system states.
type DeepThoughtState int
// One of known system states.
const (
Unknown DeepThoughtState = iota
Init
Working
Paused
ShutDown
)
// String returns a human-readable description of the State.
//
// It switches over const State values and if called on
// variable of type State it will fall through to a default
// system representation of State as a string (string of integer
// will be just digits).
func (s DeepThoughtState) String() string {
// NEW: Switch only over const values for State
switch s.(const) {
case Unknown:
return fmt.Printf("%d - the state of the system is not yet known", Unknown)
case Init:
return fmt.Printf("%d - the system is initializing", Init)
} // ERR: const switch must be exhaustive; add all cases or `default` clause
// ERR: no return at the end of the function (switch is not exhaustive)
}
// RegisterState allows changing the state
func RegisterState(ctx context.Context, state string) (interface{}, error) {
next, err := strconv.ParseInt(state, 10, 32)
if err != nil {
return nil, err
}
nextState := DeepThoughtState(next)
fmt.Printf("RegisterState=%s\n", nextState) // naive logging
// NEW: Check dynamically if variable is a known constant
if st, ok := nextState.(const); ok {
// TODO: Persist new state
return st, nil
} else {
return nil, fmt.Errorf("unknown state %d, new state must be one of known integers", nextState)
}
}
func main() {
_, err := RegisterState(context.Background(), "42")
if err != nil {
fmt.Println("error", err)
os.Exit(1)
}
os.Exit(0)
return
}
P.S. Associated values in Swift enums are one of my favorite gimmicks. In Go there is no place for them. If you want to have a value next to your enum data — use a strongly typed struct
wrapping the two.
Few months ago I wrote a proof-of-concept for a linter that checks that enumerated types are properly handled. https://github.com/loov/enumcheck
Currently it uses comments to mark things as enumerations:
type Letter byte // enumcheck
const (
Alpha Letter = iota
Beta
Gamma
)
func Switch(x Letter) {
switch x { // error: "missing cases Beta and Gamma"
case Alpha:
fmt.Println("alpha")
case 4: // error: "implicit conversion of 4 to Letter"
fmt.Println("beta")
default: // error: "Letter shouldn't have a default case"
fmt.Println("default")
}
}
I got stuck in figuring out how to handle all the implicit conversions, but it works decently for basic cases.
Note, currently it's still a work-in-progress, so things may change. e.g. instead of comments it could use some stub package for annotating the types, but comments are good-enough at the moment.
The current implementation of enums in Go1 is the weirdest most inobvious enum implementation in any language ever that I am aware of. Even C implements them more nicely. The iota thing looks like a hack. And what the heck does iota mean anyway? How am I supposed to memorize that keyword? Go is supposed to be easy to learn. But that is just qiurky.
@pofl:
While I agree that Go enums are pretty awkward, iota
is actually just a normal English word:
iota
_noun_
Presumably, they were going for definition one in terms of the use in the language.
On a side note in response to an older comment in here:
While I'd really like discriminated unions in Go, too, I feel like they should be separate from actual enums. With the way generics are currently going, you may actually get something very similar to discriminated unions via type lists in interfaces. See #41716.
The use of iota
in Go is loosely based on its use in APL. Quoting https://en.wikipedia.org/wiki/Iota:
In some programming languages (e.g., A+, APL, C++[6], Go[7]), iota (either as the lowercase symbol ⍳ or the identifier iota) is used to represent and generate an array of consecutive integers. For example, in APL ⍳4 gives 1 2 3 4.
Most helpful comment
@md2perpe that isn't enums.
```go
package main
import (
"fmt"
)
func main() {
type SearchRequest int
const (
Universal SearchRequest = iota
Web
)
}
````
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.