Navigation Menu

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

proposal: spec: add typed enum support #19814

derekperkins opened this issue Mar 31, 2017 · 206 comments
Labels
LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
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
Contributor

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.

@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 A language change or 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
Contributor

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.

@ysaakpr
Copy link

ysaakpr commented Jan 9, 2022

Whats the status on adding enum to golang. There are many issues and this thread also has many conversations, and still there is no progress on this direction and no final conclusion. Can someone add or mark all the complexities involved in making Enum as a fundamental construct in Go, just to understand why its a topic that need this much wait and conversation

@Merovius
Copy link
Contributor

Merovius commented Jan 9, 2022

@ysaakpr I'd say the single biggest hurdle is lack of consensus that it should happen and if so, what specific features it should have. I'm pretty sure that if we had consensus on those two, it would be fairly easy to come up with a design accommodating those features.

But the assumption of your question is that it will happen and there is just some hurdle preventing it. I would really recommend against viewing any language feature through this lens. Not all languages will have all features and that is a good thing. We certainly might get typed enums at some point, if someone on the Go team decides that we really need them (just like it happened with generics). Until then, I'd personally recommend just assuming that it won't (just like people did with generics).

@ysaakpr
Copy link

ysaakpr commented Jan 10, 2022

I would really recommend against viewing any language feature through this lens
I am not comparing features that are very complex in nature and not very much used through out. The enum are giving an ability to limit statically defined constants possible for a type. And to implement validation around this for every type that we create it always ends up writing un necessary code. Basic need of enum for me is

  1. Define static constants value possible for a type, compile time
  2. Ordinal around the possible values, manually assignable, with defaults
  3. String representation of the fields for more readability

Out of these 2 and 3 can be achieved using the current type definition, but the point 1 is not handled in the current type system, and according to me these three are if not there

@golang golang deleted a comment Jan 10, 2022
@ianlancetaylor
Copy link
Contributor

@ysaakpr As @Merovius said, there is no clear consensus on what properties enum types should have in Go.

For example, some people want the ability to loop over the valid values of the enum type, which is not on your list.

@ysaakpr
Copy link

ysaakpr commented Jan 17, 2022

@ianlancetaylor Sorry, I forgot to mention that, when the values are fixed, ability to list/iterate over all the possible values of an enum is a must and which is intrinsic with the kind of this type. I understand there are multiple features required, but i am sure this lists is still limited. At least do we have a ticket on which we tracking all the requested behaviours on enum and we can use it for tracking, prioritising and categorising the features

@Merovius
Copy link
Contributor

@ysaakpr That's this issue.

Also, to repeat: There's also the question of if we want typed enums at all. There are people (like me) who consider the idea counterproductive or their value at least overstated. It also has semantic overlap with #19412 and it's not clear if we'd need both. And some features are contradictory and need to be weighed against each other.

@henryas
Copy link

henryas commented Mar 18, 2022

I don't know if this is going to help with the discussion, but for my own use, I usually use a tool to go-generate the following functions and methods for my enums:

  1. A function that returns the defined enum values. For example: func AllMemberType() []MemberType. This is used to enumerate the enum.
  2. A conversion to and from string.
  3. IsDefined method. To check if the enum is one of the defined values.

Out of the three items above, number one is the most useful.

I do not think a new type is needed. In fact, I think Go2 is a good opportunity to review existing features and select which one to clean and merge/redesign, and prepare a clean slate for the next iteration of Go. I don't think we should be aggressively adding features to Go2.

@xiaokentrl

This comment was marked as spam.

@exapsy
Copy link

exapsy commented Jan 14, 2024

ianlancetaylor But one of the goals of the Go language is simplicity of the language.

Enums are supposed to make it simpler. I don't find this

type Country string

const (
    CountryGreatBritain Country = iota + 1 // Bloated namespace, iota', repeating "Country" over and over, colliding with other keywords that start with "Country" or even with the type itself and thus bloating more and more the package
    CountryGreece
    ...
)

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

which does not have any type-safety guarantees, any compilation features, good autocomplete features etc.
How is that more compelling and simple than this

type Country enum {
    GreatBritain = 1
    Greece
    ....
}

func NewPerson(country Country) { ... }
... { NewPerson(Country.Different) } // Doesn't compile, type-safe, not bloating auto-complete/package namespace, good for safe library usage

Im not sure, I could be entirely wrong, I'm not sure I understand the "new keyword breaks compatibility" narrative?
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?
Even if it breaks compatibility somehow, in a way I still don't understand exactly, why not just place it under Go v2 then

Enums have been supported by every language every since C. My understanding is that they have been a great type-safety tool, non-package namespace bloating technique for a long time.

Personally I don't find any good arguments for not supporting enums. It's a keyword that has made people's life easier, that helps humans not mess up, makes a language simpler by not having a) Bloated namespace b) Non-type safety for Go's pseudo-enum techniques c) Compiler making sure you don't mess up d) People use your libraries safely and many other reasons.

My understanding is that Go's goal is to be a simple language. And Enums, for the end user, seem to achieve just that goal on every aspect.


Even in more cases, an enum can be extremely nice to have feature to make the language simpler, by

  • not having to implement a ton of switch cases or functions for every enum (like for typing their string equivelant)
  • not having to maintain all the time new functions or new switch cases for new enums added

Like in this case

type Flag int

const (
    FlagRead Flag = iota + 1
    FlagWrite
)

func (f Flag) String() { // Messy hard maintenance. What if you add an enumeration and forget to add the String() equivalent? Bugs arise. Isn't Go supposed to make a language simple enough for people to not mess up simple things?
    switch f {
        case FlagRead: return "Read"
        case FlagWrite: return "Write"
        default: return "invalid"
    }
}

func main() {
    f := FlagRead
    fmt.Printf("%s", f)
}

instead of just having a syntax like

type Flag enum {
    Read
    Write
}

// No need to maintain all the time the String() function
// No need to navigate anywhere to see if there is a String() function or if it will print a number
// No need to navigate between String() and enum

func main() {
    f := Flag.Read
    fmt.Printf("%s", f) // Compiler compiling it to be printing something along the lines of "Flag.Read"
}

another compelling case

type RenewableEnergy enum {
    Solar = "Solar power" 
    // No need to navigate between String()
    // you're sure that never "invalid" or a runtime error will appear because you forgot to maintain the String() function
    Wind = "Wind turbines"
    Hydropower // Will just print Hydropower
}

...

In general, Enums seem to be helping in many cases to

  • Make the language simpler
  • Typesafety
  • Language auto-complete features
  • Making sure users of a library use the library right
  • Less complexity - you don't have to implement functions for simple use-cases that could be simple language features.
  • Package namespace bloat - You no longer have to write "Country", "CountryUSA, CountryFrance, ...", "CountryStateUSAFlorida, CountryStateUSACalifornia ..." all appearing on a user's auto-complete and them not seeing what they may be actually searching for, instead they see a bloated menu full of package-level variable names and types together.
  • Readability
  • Namespacing & Encapsulation
  • Serialization/Deserialization features
  • Error-prone operations
  • Refactoring, maintainability, extensibility
  • Future support for more advanced features if necessary and would make peoples' life easier.
  • Not messing up numbers (pseudo-integer-enums in Go are supported by iota, but if you add a new enum, you can never be sure if you messed up something, with actual enums, you are sure that the user just uses the name directly, with pseudo enums you just can't be sure) It doesn't make it foolproof, but it does make it safer to use

@gophun
Copy link

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

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests