Skip to content
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 · 176 comments

Comments

@derekperkins
Copy link

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

Copy link
Member

@jimmyfrasche jimmyfrasche commented Mar 31, 2017

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

@derekperkins

This comment has been minimized.

Copy link
Author

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

Copy link
Member

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

Copy link
Author

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

Copy link
Member

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

Copy link
Author

@derekperkins derekperkins commented Apr 1, 2017

I think that could be worked around

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

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

Copy link

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

Copy link
Contributor

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

Copy link
Author

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

Copy link
Author

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

Copy link

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

Copy link
Author

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

Copy link
Contributor

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

Copy link

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

@vasiliicuhar

This comment has been minimized.

Copy link

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

Copy link
Member

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

Copy link

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

Copy link

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

Copy link
Contributor

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

@vasiliicuhar

This comment has been minimized.

Copy link

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

Copy link
Contributor

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

@vasiliicuhar

This comment has been minimized.

Copy link

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

Copy link

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

Copy link
Author

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

Copy link
Contributor

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

@creker

This comment has been minimized.

Copy link

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

Copy link
Member

@jimmyfrasche jimmyfrasche commented May 25, 2018

@Merovius

This comment has been minimized.

Copy link

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

Copy link

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

Copy link

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

Copy link
Member

@jimmyfrasche jimmyfrasche commented May 25, 2018

@L-oris

This comment has been minimized.

Copy link

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

Copy link

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

Copy link

@L-oris 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)
	}
}
@gh67uyyghj

This comment was marked as off-topic.

Copy link

@gh67uyyghj gh67uyyghj commented Aug 9, 2018

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?

@KamyarM

This comment was marked as off-topic.

Copy link

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

Copy link

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

Copy link

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

Copy link

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

Copy link

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

@vladimirschuka

This comment has been minimized.

Copy link

@vladimirschuka vladimirschuka commented Nov 20, 2018

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.

@Goodwine

This comment has been minimized.

Copy link

@Goodwine Goodwine commented Nov 20, 2018

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;
  }
}
@zoonman

This comment has been minimized.

Copy link

@zoonman zoonman commented Nov 20, 2018

I think it should look simpler

func isFinal(s Status) bool {
  return s == Status.Stopped
}

Proposal for Go2

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

For Type lovers

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.

@vladimirschuka

This comment has been minimized.

Copy link

@vladimirschuka vladimirschuka commented Nov 20, 2018

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.

@zoonman

This comment has been minimized.

Copy link

@zoonman zoonman commented Nov 20, 2018

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.

@anjmao

This comment has been minimized.

Copy link

@anjmao anjmao commented Dec 27, 2018

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.

@vp2177

This comment has been minimized.

Copy link

@vp2177 vp2177 commented Jan 9, 2019

The above go beyond enums, 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"
}
@mier85

This comment has been minimized.

Copy link

@mier85 mier85 commented Jun 22, 2019

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:

  • either for validation ( if we are very sure we only want to accept this or to simply ignore unknown options )
  • or for a list of possible constants ( think of dropdowns ).

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.

@x-strong

This comment has been minimized.

Copy link

@x-strong x-strong commented Nov 6, 2019

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?

@ermik

This comment has been minimized.

Copy link

@ermik ermik commented Nov 7, 2019

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

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

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

  4. 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 consts and building an idiomatic way to invoke that check. The String() function, being a common necessity, is a great case for implementation.

Proposal

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.