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 · 220 comments
Open

proposal: spec: add typed enum support #19814

derekperkins opened this issue Mar 31, 2017 · 220 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@derekperkins
Copy link

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
Copy link
Member

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

@derekperkins
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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.

@jimmyfrasche
Copy link
Member

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

@derekperkins
Copy link
Author

I think that could be worked around

@ianlancetaylor
Copy link
Member

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
Copy link

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
Copy link
Contributor

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
Copy link
Author

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
Copy link
Author

@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
Copy link

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

@derekperkins
Copy link
Author

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
Copy link
Contributor

@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
Copy link
Contributor

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. all: support gradual code repair while moving a type between packages #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
Copy link

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
Copy link
Member

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.

@bradfitz bradfitz added the LanguageChange Suggested changes to the Go language label Apr 2, 2017
@sprstnd
Copy link

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
Copy link

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

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
Copy link

vasiliicuhar commented Apr 3, 2017

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

@Merovius
Copy link
Contributor

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 v2 An incompatible library change label Apr 3, 2017
@derekperkins
Copy link
Author

@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
Copy link
Member

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.

@gophun
Copy link

gophun commented Jan 14, 2024

good autocomplete features

Actually, the IDE I use provides good autocomplete suggestions based on the type of the constants.

To me if feels like creating a new lib into the stdlib. As in, it's something new, if somebody doesn't use it, how would it break compatibility?

A new keyword breaks compatibility, as existing code using an identifier with the same name as the keyword would no longer compile.

@exapsy
Copy link

exapsy commented Jan 14, 2024

... { NewPerson(Country(15) } // should NOT be compiled, but compiles, no type-safety

Nobody would accidentally make up a meaningless number except for a deliberate intention to hurt oneself. I think this is a theoretical problem that doesn't happen in practice.

"Nobody would accidentally do ..."

The intention of a good compiler is supposed to prevent even the case of such cases in the first place. Like, cases such as #51317 are heavily debated just because of making sure the compiler and the runtime handles the case in a way that the user does not have to care or will be safe from their own actions.

Like, I see all the time arguments from both ways.

  • Go has to be simple and make sure the user is safe
  • We do not have to protect the user from making Country(5)

always depending on which side of the argument you're on.

If Go strives to be safer to use, then you should prevent a user from having such weird cases such as that. And there are cases and have seen or I'm not sure even have written myself I won't put myself in another "im the best programmer" bag, because it was "easy" or comfortable at the time to do so and because the programmer just didn't think too much about it.

Where an pseudo-enumeration was written like that in an unsafe manner.

@exapsy
Copy link

exapsy commented Jan 14, 2024

good autocomplete features

Actually, the IDE I use provides good autocomplete suggestions based on the type of the constants.

To me if feels like creating a new lib into the stdlib. As in, it's something new, if somebody doesn't use it, how would it break compatibility?

A new keyword breaks compatibility, as existing code using an identifier with the same name as the keyword would no longer compile.

I may be genuinely wrong, and just curious, how would type MyEnum enum {} break compatibility? Where you would actually use this as a golang programmer in a way that would break compatibility? It's not like you would use this format as a variable format, you cannot. Maybe some other way that can break compatibility?

I don't know, let me know what you think if it would actually break compatibility

@gophun
Copy link

gophun commented Jan 14, 2024

The intention of a good compiler is supposed to prevent even the case of such cases in the first place.

This may be the intention of a proof assistant, but not of a general-purpose programming language compiler. The latter must strike a balance between often contradictory requirements. It should aim to guard against common accidental mistakes, not against deliberate self-sabotage. Otherwise, the compiler wouldn't compile statements like os.RemoveAll("*") or similar.

@exapsy
Copy link

exapsy commented Jan 14, 2024

The intention of a good compiler is supposed to prevent even the case of such cases in the first place.

This may be the intention of a proof assistant, but not of a general-purpose programming language compiler. The latter must strike a balance between often contradictory requirements. It should aim to guard against common accidental mistakes, not against deliberate self-sabotage. Otherwise, the compiler wouldn't compile statements like os.RemoveAll("*") or similar.

I see what you mean, I think you're presenting though two completely separate cases:

os.RemoveAll("*") is code that does something, you should not prevent the programmer from executing code as a compiler at least imo.
Country(5) is syntax which can just be improved massively by just having enums


edit:

Despite that statement, I presented a ton of arguments about enums, let's not narrow it down to just Country(5) which imo should not be compilable at all and the user should have a good way of accessing enumerations with good namespace encapsulation

@gophun
Copy link

gophun commented Jan 14, 2024

I may be genuinely wrong, and just curious, how would type MyEnum enum {} break compatibility? Where you would actually use this as a golang programmer in a way that would break compatibility? It's not like you would use this format as a variable format, you cannot. Maybe some other way that can break compatibility?

This program doesn't compile, even though package can't appear within function scope:

package main

func main() {
	const package = 3
}

Go's parsing of keywords is deliberately context-free.

@Merovius
Copy link
Contributor

Merovius commented Jan 14, 2024

@exapsy In regards to backwards compatibility, you need to understand the difference between the scanner and the parser.

The scanner takes bytes and converts them into a stream of tokens which each has a kind. A keyword (like go or func) has a different kind from an identifier (like string or iota or foo).

The parser then takes the stream of tokens and converts it into an abstract syntax tree, based on the kind of the token. "A function declaration is a func token, followed by an identifier, followed by a ( token…" and so on.

If we would introduce a new keyword like enum, a variable declaration like var enum int would be made into [<var keyword> <enum keyword> <identifier>], whereas right now it would be made into [<var keyword> <identifier> <identifier>]. The compiler only recognizes the latter as a variable declaration, while the former is an invalid declaration.

To make the parser still be able to parse var enum int as a variable declaration, while enabling it to parse type x enum { as a type declaration, we would have to introduce a lot of extra cases into the grammar. Pretty much everywhere the parser currently expects an identifier, we would have to say "an identifier or an enum token and if it is the latter, it should be interpreted as an identifier, unless…" and so on. This is a general problem with introducing any new keyword.

More problematically, currently a type declaration type x enum is a perfectly valid syntactical type-declaration: It declares x to be of the same underlying type as enum. So, here is another reason why this would break backwards compatibility: It would actually cause code that currently is parsed one way to be parsed a different way.

All of that being said: Yes, today we could probably decide to do it, by guarding it behind a new language version. That is, we would make enum a keyword, if the go.mod file contains a go directive of version at least 1.30 (or whatever). But we would still be somewhat hesitant to do that, because it requires developers to manually modify their code to upgrade Go. While we intentionally build mechanisms to be able to do that when necessary, we try to do that as little as possible.

In particular, we set ourselves the limit that an existing valid program should never be re-interpreted as a different valid program - it should always either break in a new version, or the new version should not be valid today. This is to make it easier to detect these kinds of backwards-incompatible changes automatically - if your program still compiles with the new version, it should do the same thing.

A new syntactical construct should be carefully vetted to fulfill that requirement. But yes, we could probably do that today. Note that a lot of this discussion predates having these tools for introducing backwards-incompatible changes in place.

On a meta-level: In my experience, when discussions on GitHub issues get new comments with too high a frequency, that is a symptom that the discussion is not being productive. While I typed out this relatively long comment, to give a thorough explanation of all the subtleties, at least two new comments popped up above. Perhaps making parts of my thorough explanation obsolete, but probably not. But it's better to take your time to be thoughtful, in this medium. If new comments happen every few minutes (by the same two or three people) that is a clear indication that the discussion should be taken to a medium more suited for real-time communication (like Slack).

@griesemer
Copy link
Contributor

The Go team discussed the need for enums in the very early days of Go. Enums pose various of problems, some of which have been pointed out here. Extensibility of enums is a real issue. Scope of enum names is another real issue. Orthogonality of concepts is also a concern (enum vs constants vs variables). For these reasons we decided early on not to add a specific enum concept to Go and instead use the iota constant generator which is very well understood and gets us many (most?) of the benefits without the drawbacks. It doesn't give us all that "proper" enums might provide, but on the other hand it gives us the ability to write complex expressions determining enum constant values, something the typical enum mechanisms don't provide.

There's also no urgent need for enums or an inability to do things in Go that cannot be done relatively easily with existing mechanisms. In short, the bar is high for a significant change such as this, and it doesn't seem that the bar is met.

With respect to support for more complex typed "enums", rather than introducing a heavy-weight mechanism, I suggest we investigate #21473 (iota in variable declarations).

@jimwei
Copy link

jimwei commented Jan 17, 2024 via email

@jimwei
Copy link

jimwei commented Jan 17, 2024 via email

@jimwei
Copy link

jimwei commented Jan 17, 2024 via email

@jimwei
Copy link

jimwei commented Jan 17, 2024 via email

@HaraldWik
Copy link

HaraldWik commented Jun 1, 2024

"You" refers to the Go team at Google

If you are afraid of having to change thing in the standard lib thus casing problems, just dont. I'm happy if I have acces to enums.
You also said aproxomantly that you dont want to add more keyword well also just don't make it a package.
You also said that enums are complicated but enums are just a array of bools with names where only 1 can be true its not compicated, you might wounder why I don't just do it myself wich I probably chould but then I'd need to import that package every time and everyone else that want to use my packages too, so if you just add enums in the standard lib it whould be fine and people that want to use const with iota can still do it, thus not breaking any compatability.

You also said that there where no good reasons to add enums, but there is and here are some

  1. More error preventing, right now you can put anythin inside of a function.
  2. It look nicer
  3. Its esier to work with
  4. A bunch of people want it
  5. The thing with not wanting more keyword, you added iota but not enums and C has enums there are like no keyword in C and they have it

Arguments why GO shouldent have enums

  1. People having to update their project
  2. I tried to find more but I really chouldent

I still think GO is a wounderful language its a solid 8/10 but with enums is easily a 9/10 I don't even know anything else you can improve. The package manager is the best I've ever used, the code is easy to read and write and the VSCode extension is 12/10.

This is how I'd think enums should look like

package main

import "fmt"

type Fruit enum {
	Apple int = 20
	Orange
	Grape string = "Grape!"
}

func main() {
	Enum := Fruit.Apple

	if Enum == Fruit.Apple || Enum == 20 {
	    fmt.Println("Apples")
	}

	switch Enum {
	case: 20, Fruit.Apple:
    	fmt.Println("Apple")
	case: "Grape!", Fruit.Grape: 
    	fmt.Println(Fruit.Grape)   
	default:
    	fmt.Println("No fruit here!")
	}
}

@ianlancetaylor
Copy link
Member

@HaraldWik There are 207 comments on this issue (208 now, I guess) which is enough to show clearly that there is no consensus on precisely what enums should look like in Go. Different people have different concerns.

Also, there is nearly 15 years of use of Go showing that people are able to write code without enums in the language. So enums might be nice, but they don't seem to be necessary. I am not aware of any significant kinds of code that can't be written today but that could be written if we had enums.

That said I'm personally not opposed to adding language features that make it easier to write correct Go code. But we need a clear consensus on the new feature and how people would use it. We don't have that.

@HaraldWik
Copy link

@ianlancetaylor
I do understand what you mean but its just i really want enums, they whould really help me. I find my self in need for enums a lot but I do understand that the rest of the community may not need them as much because they probably use it for servers and you dont realy need enums mean while I use it for software dev.

@jorng
Copy link

jorng commented Jun 5, 2024

@HaraldWik it's not just you. Not having enums is a huge hindrance on the language. Pretty sad we still don't have them at this point.

Sure, Go has worked so far without them, but it's all a bunch of workarounds and less safe code.

@ysaakpr
Copy link

ysaakpr commented Jun 5, 2024 via email

@HaraldWik
Copy link

@ysaakpr Why anyone still uses GO even tho thers no enums is because the language is so good in generall, its just that GO whould greatly improve if there were enums in the language, and what I don't understand is why the GO team don't want to implement enums because thers no need for them, I mean C is a "simple" language with few keywords and the C developers added enums, but GO can't because of complexity or some thing else, I mean if they whould just make it into a stdlib thing it whould be fine because then they don't need to touch the compiler, and if you are woundering why I don't just make a enum package for myself is that anyone that want to use my other packages whould need to have that package too, so the point is that I whould just like there to be a standard enum that the community chould use that is supported by the GO team.

@Merovius
Copy link
Contributor

Merovius commented Jun 6, 2024

I mean C is a "simple" language with few keywords and the C developers added enums

C has neither interfaces nor iota, so the two things in Go which have major redundancy with enums and sum/union types, C does not have.

I'll also note that C enums are pretty minimalistic in what you can do with them, so in a way, they actually underscore the point Ian is making, that no one can quite agree on what to expect from them. Your comment above also demonstrates that, given that it is absolutely not clear how that is supposed to work - I don't think I've ever seen it proposed that you could mix integers, strings and completely valueless declarations in an enum. So, you seem to understand something completely different as "the bare minimum" you'd expect from enums than anyone I know.

This issue discussion is long and it contains a lot of reasonable design space and yet, there is no consensus about which of those myriads of features are critical and which are not, once you get beyond what you can do with const and iota. That's the reason why they are not added. Whether the added complexity is worth the benefit is something we can only really discuss once we have an idea of what the added complexity and the benefit is.

@HaraldWik
Copy link

HaraldWik commented Jun 6, 2024

The C enum comment was just an exampel that languages that are "simple" have enums and I'm happy that GO has sum/union types and interfaces but thats not the point, the point is that C has enums and GO doesent, even PHP has enums (sorta)

The second point you brought up I think is fair, I think the best way to resolve that is to make a vote where you can pick from diffrent enum styles.

"once you get beyond what you can do with const and iota. That's the reason why they are not added. Whether the added complexity is worth the benefit is something we can only really discuss once we have an idea of what the added complexity and the benefit is." <- As stated previosly I think they chould make it into a standard lib, that doesent add more complexity to the compiler as it seems that they don't want to make drastic changes because they don't want it to become unstable.

@wild-fruits
Copy link

If the team does not listen to the community's (suggestions/proposals), I think this is no longer an open source community

@HaraldWik

This comment was marked as off-topic.

@Merovius
Copy link
Contributor

Merovius commented Jun 6, 2024

@HaraldWik

The C enum comment was just an exampel that languages that are "simple" have enums

I think you missed my point. You can't divide the world of programming languages into "simple" and "non-simple" ones and then say "being simple is not mutually exclusive with having enums". Being simple is, primarily, a function of orthogonality. C made some choices and Go made some choices and based on those choices, enums do not fit as orthogonally into Go as they fit into C.

You can't just take a feature from one language and port it to Go and expect it to fit in well. Because the rest of the language you are taking it from is different from the rest of Go and thus fits things differently.

I think the best way to resolve that is to make a vote where you can pick from diffrent enum styles.

Go has never been developed by vote. Personally, I like that very much.

As stated previosly I think they chould make it into a standard lib

I have no idea how you imagine this working, it does not make a lot of sense to me. Feel free to write them as a 3rd party package and point at that, which should demonstrate the concept. Yes, I know you think they are only helpful if they are in the standard library, but the point is that once code exists, it's easier to discuss whether or not that code should make it into the standard library.

@wild-fruits

If the team does not listen to the community's (suggestions/proposals), I think this is no longer an open source community

This claim comes up occasionally and in my opinion, this post by @ianlancetaylor is still the definitive summary of that debate.

@HaraldWik
Copy link

  1. I don't mean that C is a "simple" language its in "" what I mean Is that C doesent have that many keywords wich I'm pretty sure the Go team stated as one of the problems with enums but I may be wrong I don't exactly remember.

  2. I do not think that you can just take a feture from one language and just add it to another one, and I do understand that the Go team made their choice but I'd like if the community had a say in it,

  3. "Go has never been developed by vote. Personally, I like that very much." Thats fine but you don't have to vote, no one is forcing that on you. And sure the Go team have not vote as of my knowledge, but they have challanged us to create a reason to add enums, and it does not need to be a vote just some type of interaction from the Go team.

  4. "It does not make a lot of sense to me", Ye me too but its just an idea!

  5. "Feel free to write them as a 3rd party package" I've brought up that to and my argument was that Its messy if every one needs to download my package just to have enums, and no one whould use them because they are not a language feture its just my package and I whouldent want to download a package just to have a basic language data type.

  6. The part where wild fruit said it does not feel like open source may have been a joke, but I think its more about the Go team not listening to community feedback wich I feel is quite frustrating!

@atdiar
Copy link

atdiar commented Jun 6, 2024

Had a few spare minutes and had a little fun:
https://go.dev/play/p/8vbDsO8USzW?v=gotip

package main

import "fmt"

// EnumValue represents a value in an enum with a generic type T and a discriminant type
type EnumValue[T comparable, discriminant any] struct {
	value T
}

func (e EnumValue[T, discriminant]) Unwrap() T {
	return e.value
}

func (e EnumValue[T, discriminant]) Value() any {
	return e.value
}

func (e EnumValue[T, discriminant]) discriminant() discriminant {
	var d discriminant
	return d
}

// Enum interface for generic enum handling
type Enum[discriminant any] interface {
	Values() []EnumMember[discriminant]
	Value(int) EnumMember[discriminant]
	Index(EnumMember[discriminant]) int
	Len() int

}



type EnumMember[T any] interface {
	discriminant() T
	Value() any
}

type enum[discriminant any] struct {
	values []EnumMember[discriminant]
}

func (e *enum[discriminant]) Values() []EnumMember[discriminant] {
	return e.values
}

func (e *enum[discriminant]) Value(i int) EnumMember[discriminant] {
	return e.values[i]
}

func (e *enum[discriminant]) Index(v EnumMember[discriminant]) int {
	for i, val := range e.values {
		if val == v {
			return i
		}
	}
	return -1
}

func (e *enum[discriminant]) Len() int {
	return len(e.values)
}

func (e *enum[discriminant]) AddValue(v EnumMember[discriminant]) int {
	l := len(e.values)
	e.values = append(e.values, v)
	return l
}

func NewEnum[discriminant any](values ...EnumMember[discriminant]) Enum[discriminant] {
	return &enum[discriminant]{values}
}

func PrintEnum[T any](e Enum[T]) {
	for _, v := range e.Values() {
		fmt.Println(v.Value())
	}
}

func main() {
	// basically the "fruits" type is a unique identifier of the enum that can be understood as being the enum name.
	type fruits struct{}

	type Fruit = EnumMember[fruits]

	var Apple = EnumValue[string, fruits]{value: "apple"}
	var Orange = EnumValue[string, fruits]{value: "orange"}
	var LeeT = EnumValue[int, fruits]{value: 1337}

	var Fruits = NewEnum[fruits](Apple, Orange, LeeT)

	PrintMember := func(ev Fruit) {
		fmt.Println("\nPrint the enum value passed as argument: ")
		fmt.Println(ev.Value())
	}

	PrintEnum(Fruits)
	PrintMember(Apple)

	fmt.Printf("\nAre %v and %v equal?  %v \n", Apple, Orange, Apple == Orange)
	fmt.Printf("Are %v and %v equal?  %v \n", LeeT, LeeT, LeeT == LeeT)
}

That doesn't handle nilability yet but that's a whole other issue.
With proper use of encapsulation, that seems tractable.

There are several other ways to achieve similar still.

My main issues would be how to deal with nil and also, how to add basic types as enum cases which requires union types implemented as interfaces. I do have some ideas but that needs some exploration.

Edit:

To be clear, this is just tinkering around enums and heterogeneous collections.
Comparison between enum values can use the Enum Index method (comparing the respective indices).

Could also only support homogeneously typed enums in which case the equal operator would be sufficient.

@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed v2 An incompatible library change NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Aug 6, 2024
@ludaplus
Copy link

I wrote a little tool to collect enumerated members defined as structures through generalizations and one-time reflections, and provide generic methods by embedding an anonymous structure.

https://github.com/ludaplus/enums

type innerPostType struct {
	enums.Element[*innerPostType]
	CommentEnabled bool
}

var PostType = enums.Of(&struct {
	enums.Enum[*innerPostType]
	Unknown,
	Post,
	Page,
	Note *innerPostType
}{
	enums.Enum[*innerPostType]{},
	&innerPostType{
		CommentEnabled: false,
	},
	&innerPostType{
		CommentEnabled: true,
	},
	&innerPostType{
		CommentEnabled: false,
	},
	&innerPostType{
		CommentEnabled: false,
	},
})

func main() {
    fmt.Println(PostType.Unknown.Name())
    
    fmt.Println(PostType.Post.CommentEnabled)
	
    fmt.Println(PostType.ValueOf("Page").Ordinal())
	
    fmt.Println(PostType.ValueOf("Note") == PostType.Note)
	
    for _, postType := range PostType.Values() {
        fmt.Println(postType.CommentEnabled())
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests