New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: spec: add typed enum support #19814

Open
derekperkins opened this Issue Mar 31, 2017 · 165 comments

Comments

Projects
None yet
@derekperkins

derekperkins commented Mar 31, 2017

I'd like to propose that enum be added to Go as a special kind of type. The examples below are borrowed from the protobuf example.

Enums in Go today

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
}

How it might look with language support

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.

  • Safety for exported types: nothing prevents someone from doing 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.
  • Runtime safety: Just like protobuf is going to check for validity while unmarshaling, this provides language wide validation, anytime that an enum is instantiated.
  • Tooling / Documentation: many packages today put valid options into field comments, but not everyone does it and there is no guarantee that the comments aren't outdated.

Things to Consider

  • Nil: by implementing 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.
  • Default value / runtime assignments: This is one of the tougher decisions to make. What if the Go default value isn't defined as a valid enum? Static analysis can mitigate some of this at compile time, but there would need to be a way to handle outside input.

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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented Mar 31, 2017

@derekparker there's a discussion for making a Go2 proposal in #19412

@derekperkins

This comment has been minimized.

derekperkins commented Mar 31, 2017

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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented Mar 31, 2017

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

@derekperkins

This comment has been minimized.

derekperkins commented Mar 31, 2017

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.

@griesemer griesemer added the Proposal label Mar 31, 2017

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented Mar 31, 2017

They both require a new keyword which would break valid Go1 code using that as an identifier

@derekperkins

This comment has been minimized.

derekperkins commented Apr 1, 2017

I think that could be worked around

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Apr 1, 2017

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?

@gopherbot gopherbot added this to the Proposal milestone Apr 1, 2017

@md2perpe

This comment has been minimized.

md2perpe commented Apr 1, 2017

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.

@bep

This comment has been minimized.

bep commented Apr 1, 2017

@md2perpe that isn't enums.

  1. They cannot be enumerated, iterated.
  2. They have no useful string representation.
  3. They have no identity:
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.

@derekperkins

This comment has been minimized.

derekperkins commented Apr 1, 2017

@md2perpe iota is a very limited way to approach enums, which works great for a limited set of circumstances.

  1. You need an int
  2. You only need to be consistent inside your package, not representing external state

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.

@derekperkins

This comment has been minimized.

derekperkins commented Apr 1, 2017

@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 {
...
}
@mixedCase

This comment has been minimized.

mixedCase commented Apr 2, 2017

If Go were to add anything more than iota, at that point why not go for algebraic data types?

@derekperkins

This comment has been minimized.

derekperkins commented Apr 2, 2017

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.

@egonelbre

This comment has been minimized.

Contributor

egonelbre commented Apr 2, 2017

@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)
	}
}
@Merovius

This comment has been minimized.

Merovius commented Apr 2, 2017

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

  • compile-time checked enums are neither backwards nor forwards compatible for the case of additions or removals. #18130 spends significant effort to move go towards enabling gradual code repair; enums would destroy that effort; any package that ever wants to change a set of enums, would automatically and forcibly break all their importers.
  • Contrary to what the original comment claims, protobuf (for that specific reason) don't actually check the validity of enum fields. proto2 specifies that an unknown value for an enum should be treated like an unknown field and proto3 even specifies, that the generated code must have a way to represent them with the encoded value (exactly like go does currently with fake-enums)
  • In the end, it doesn't actually add a lot. You can get stringification by using the stringer tool. You can get iteration, by adding a sentinel MaxValidFoo const (but see above caveat. You shouldn't even have the requirement). You just shouldn't have the two const-decls in the first place. Just integrate a tool into your CI that checks for that.
  • I don't believe other types than ints are actually necessary. The stringer tool should already cover converting to and from strings; in the end, the generated code would be equivalent to what a compiler would generate anyway (unless you seriously suggest that any comparison on "string-enums" would iterate the bytes…)

Overall, just a huge -1 for me. Not only doesn't it add anything; it actively hurts.

@vcuhar

This comment has been minimized.

vcuhar commented Apr 2, 2017

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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented Apr 2, 2017

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.

@sprstnd

This comment has been minimized.

sprstnd commented Apr 3, 2017

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.

@alercah

This comment has been minimized.

alercah commented Apr 3, 2017

@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

This comment has been minimized.

bep commented Apr 3, 2017

@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.

@vcuhar

This comment has been minimized.

vcuhar commented Apr 3, 2017

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.

@bep

This comment has been minimized.

bep commented Apr 3, 2017

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 ...

@vcuhar

This comment has been minimized.

vcuhar commented Apr 3, 2017

//go:generate enumerator Foo,Bar
Written once, available everywhere. Note that the example is abstract.

@Merovius

This comment has been minimized.

Merovius commented Apr 3, 2017

@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 rsc added the Go2 label Apr 3, 2017

@derekperkins

This comment has been minimized.

derekperkins commented Apr 3, 2017

@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.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Apr 4, 2017

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.

@ljeabmreosn

This comment has been minimized.

ljeabmreosn commented May 15, 2018

@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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented May 15, 2018

@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 . . .)

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented May 15, 2018

@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.

@josharian

This comment has been minimized.

Contributor

josharian commented May 21, 2018

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.)

@creker

This comment has been minimized.

creker commented May 21, 2018

@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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented May 21, 2018

@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.

@josharian

This comment has been minimized.

Contributor

josharian commented May 22, 2018

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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented May 22, 2018

@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

  1. Illegal values are illegal.
  2. the information is available to reflect and go/types allowing programs to act on it without needing to make assumptions or annotations (which aren't available to reflect, currently).
@Merovius

This comment has been minimized.

Merovius commented May 22, 2018

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.

@creker

This comment has been minimized.

creker commented May 22, 2018

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.

@ethe

This comment has been minimized.

ethe commented May 25, 2018

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.

@thwd

This comment has been minimized.

thwd commented May 25, 2018

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.

@Merovius

This comment has been minimized.

Merovius commented May 25, 2018

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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented May 25, 2018

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).

@creker

This comment has been minimized.

creker commented May 25, 2018

@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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented May 25, 2018

@Merovius

This comment has been minimized.

Merovius commented May 25, 2018

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.

@creker

This comment has been minimized.

creker commented May 25, 2018

@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.

@creker

This comment has been minimized.

creker commented May 25, 2018

@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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented May 25, 2018

@L-oris

This comment has been minimized.

L-oris commented Jul 8, 2018

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"
}

Advantages:

Disadvantages:

Do we really need enums?

@bpkroth

This comment has been minimized.

bpkroth commented Jul 8, 2018

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

This comment has been minimized.

L-oris commented Jul 9, 2018

@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)
	}
}
@KamyarM

This comment was marked as off-topic.

KamyarM commented Aug 9, 2018

@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!!

@andradei

This comment has been minimized.

andradei commented Sep 5, 2018

@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:

  • write
  • read
  • reason about

In your example (which is great because it works today) having enums (in some form) implies the need to:

  • Create a struct type
  • Create a method
  • Create variables (not even constants, although a user of the library can't change those values)

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.

@L-oris

This comment has been minimized.

L-oris commented Sep 11, 2018

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:

  • it’s not that much boilerplate; at worst, we can have some generators (but, is it really that much code?)
  • how much “simplicity” are we achieving by adding a new keyword, and the whole specific set of behaviours it will probably have?
  • being a little creative with methods can also add interesting capabilities to those enums
  • for readability, it’s more about getting used to it; maybe add a comment on top of it, or prefix your variables
package day

// Day Enum
type Day struct {
    value string
}
@andradei

This comment has been minimized.

andradei commented Sep 12, 2018

@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:

  • It isn't that much boilerplate indeed
  • We'd have to check how well-known the concept of enum is to be certain, I'd say most people know what it is (but I can't prove that). The complexity of the language would be at a good "price" to pay for its benefits.
  • That's true, not having enums are problems that only arised to me when checking generetad protobuf code and when trying to make a database model that mimics an enum, for example.
  • That's also true.

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.

@Vitucho

This comment has been minimized.

Vitucho commented Oct 5, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment