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: enums as an extension to types #28987

Open
deanveloper opened this issue Nov 28, 2018 · 87 comments
Open

proposal: spec: enums as an extension to types #28987

deanveloper opened this issue Nov 28, 2018 · 87 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@deanveloper
Copy link

deanveloper commented Nov 28, 2018

Yet another enum proposal

Related: #19814, #28438

First of all, what is the issue with const? Why can't we use that instead?

Well first of all, iota of course only works with anything that works with an untyped integer. Also, the namespace for the constants are at the package level, meaning that if your package provides multiple utilities, there is no distinction between them other than their type, which may not be immediately obvious.

For instance if I had my own mat (material) package, I'd want to define mat.Metal, mat.Plastic, and mat.Wood. Then maybe classify my materials as mat.Soft, mat.Neutral, and mat.Hard. Currently, all of these would be in the same namespace. What would be good is to have something along the lines of mat.Material.Metal, mat.Material.Plastic, mat.Material.Wood, and then mat.Hardness.Soft, mat.Hardness.Neutral, and mat.Hardness.Hard.

Another issue with using constants is that they may have a lot of runtime issues. Consider the
following:

var ErrInvalidWeekday = errors.New("invalid weekday")

type Weekday byte

const (
	Sunday Weekday = iota
	Monday
	Tuesday
	// ...
)
func (f Weekday) Valid() bool {
	return f <= Saturday
}

func (d Weekday) Tomorrow() Weekday {
	if !d.Valid() {
		panic(ErrInvalidWeekday)
	}
	
	if d == Sunday {
		return Saturday
	}
	return d + 1
}

Not only is there a lot of boilerplate code where we define the "enum", but there is also a lot of boilerplate whenever we use the "enum", not to mention that it means that we need to do runtime error checking, as there are bitflags that are not valid.

I thought to myself. What even are enums? Let's take a look at some other languages:

C

typedef enum week{Sun,Mon,Tue,Wed,Thu,Fri,Sat} Weekday;

Weekday day = Sun;

This ends up being similar to Go's iota. But it suffers the same pitfalls that we have with iota, of course. But since it has a dedicated type, there is some compile-time checking to make sure that you don't mess up too easily. I had assumed there was compile-time checking to make sure that things like Weekday day = 20 were at least compile-time warnings, but at least with gcc -Wextra -Wall there are no warnings for it.

C++

This section was added in an edit, originally C and C++ were grouped together, but C++11 has added enum class and enum struct which are very similar to Java's (next section). They do have compile-time checking to make sure that you don't compare two different types, or do something like Weekday day = 20. Weeday day = static_cast<Weekday>(20) still works, however. We should not allow something like this. #28987 (comment)

Syntax:

enum class Weekday { sun, mon, tues, ... };

Weekday day = Weekday::sun;
Weekday day2 = static_cast<Weekday>(2); // tuesday

Java

An enum is a kind of class. This class has several static members, named after the enum values you define. The type of the enum value is of the class itself, so each enum value is an object.

enum Weekday {
	SUNDAY(), // parentheses optional, if we define a constructor, we can add arguments here
	MONDAY,
	TUESDAY,
	// ...
	SATURDAY;
	
	// define methods here
	
	public String toString() {
		// ...
	}
}

I personally like this implementation, although I would appreciate if the objects were immutable.

The good thing about this implementation is that you are able to define methods on your enum types, which can be extremely useful. We can do this in Go today, but with Go you need to validate the value at runtime which adds quite a bit of boilerplate and a small efficiency cost. This is not a problem in Java because there are no possible enum values other than the ones you define.

Kotlin

Kotlin, being heavily inspired by Java, has the same implementation. They are even more clearly
objects, as they are called enum class instead of simply enum.

Swift

Proposal #28438 was inspired by these. I personally don't think they're a good fit for Go, but it's a different one, so let's take a look:

enum Weekday {
	case Sunday
	case Monday
	case Tuesday
	// ...
}

The idea becomes more powerful, as you can define "case functions" (syntax is case SomeCase(args...), which allow something like EnumType.number(5) being separate from EnumType.number(6). I personally think it is more fitting to just use a function instead, although it does seem like a powerful idea.

I barely have any Swift experience though, so I don't know the advantages of a lot of the features that come with Swift's implementation.

JavaScript

const Weekday = Object.freeze({
	Sunday:  Symbol("Sunday"),
	Monday:  Symbol("Monday"),
	Tuesday: Symbol("Tuesday"),
	// ...
});

This is probably the best you can do in JavaScript without a static type system. I find this to be a good implementation for JavaScript, though. It also allows the values to actually have behavior.

Okay, so enough with other languages. What about Go?

We need to ask ourselves, what would we want out of enums?

  1. Named, Immutable values.
  2. Compile-time validation. (We don't want to have to manually check at runtime to see if enum values are valid)
  3. A consise way to define the values (the only thing that iota really provides)

And what is an enum? The way that I have always seen it, enums are an exhaustive list of immutable values for a given type.

Proposal

Enums limit what values a type can hold. So really, enums are just an extension on what a type can do. "Extension" perhaps isn't the right word, but the syntax should hopefully make my point.

The enum syntax should reflect this. The proposed syntax would be type TypeName <base type> enum { <values> }

package mat // import "github.com/user/mat"

// iota can be used in enums
type Hardness int enum {
	Soft = iota
	Neutral
	Hard
}

// Enums should be able to be objects similar to Java, but
// they should be required to be immutable. A readonly types
// proposal may help this out. Until then, it may be good just to either
// have it as a special case that enum values' fields cannot be edited,
// or have a `go vet` warning if you try to assign to an enum value's field.
type Material struct {
	Name string
	Strength Hardness
} enum {
	Metal = Material{Name: "Metal", Strength: values(Hardness).Hard } // these would greatly benefit from issue #12854
	Plastic = Material{Name: "Plastic", Strength: values(Hardness).Neutral }
	Foam = Material{Name: "Foam", Strength: values(Hardness).Soft }
}

// We can define functions on `Material` like we can on any type.

// Strong returns true if this is a strong material
func (m Material) Strong() bool {
	return m.Strength >= Hardness.Neutral
}

The following would be true with enums:

  • int enum { ... } would be the type that Hardness is based on. int enum { ... } has the underlying type int, so Hardness also has the underlying type int.
  • Assigning an untyped constant to a variable with an enum type is allowed, but results in a compile error if the enum does not support the constant expression's value (That's a long winded way of saying var h Hardness = 1 is allowed, but var h Hardness = 100 is not. This is similar how it is a compile error to do var u uint = -5)
  • As with normal types, assigning a typed expression to a variable (var h Hardness = int(5)) of a different type is not allowed
  • There is a runtime validation check sometimes, although this can be ommited in most cases. The runtime check occurs when converting to the new type. For instance var h Hardness = Hardness(x) where x is an integer variable.
  • Using arithmetic operators on enums with underlying arithmetic types should probably either not be allowed, or be a runtime panic with a go vet flag. This is because h + 1 may not be a valid Hardness.

Syntax ideas for reading syntax values:

  1. Type.Name
    • It's a common syntax people are familiar with, but it makes Type look like a value.
  2. Type#Name, Type@Name, etc
    • Something like these would make the distinction that Type is not a value, but it doesn't feel familiar or intuitive.
  3. Type().Name
    • This one doesn't make too much sense to me but it popped in my head.
  4. values(Type).Name, enum(Type).Name, etc
    • values would be a builtin function that takes a type, and returns its enumeration values as a struct value. Passing a type that has no enum part would of trivially return struct{}{}. It seems extremely verbose though. It would also clash as values is a pretty common name. Many go vet errors may result from this name. A different name such as enum may be good.

I personally believe values(Type).Name (or something similar) is the best option, although I can see Type.Name being used because of it's familiarity.

I would like more critique on the enum definitions rather than reading the values, as that is mainly what the proposal mainly focuses on. Reading values from an enum is trivial once you have a syntax, so it doesn't really shouldn't need to be critiqued too much. What needs to be critiqued is what the goal of an enum is, how well this solution accomplishes that goal, and if the solution is feasible.

Points of discussion

There has been some discussion in the comments about how we can improve the design, mainly the syntax. I'll take the highlights and put them here. If new things come up and I forget to add them, please remind me.

Value list for the enum should use parentheses instead of curly braces, to match var/const declaration syntax.

  • Advantage: More consistent with the rest of the language
  • Disadvantage: Doesn't quite look as nice when declaring enums of structs

Perhaps changing the type syntax from <underlying type> enum ( values ) to enum <underlying type> ( values ).

  • Advantage: int enum ( ... ) -> enum int ( ... ) and similar become more readable and consistent with other languages.
  • Advantage: Ambiguities such as []byte enum ( ... ) get resolved to either enum []byte ( ... ) or []enum byte ( ... ).
  • Disadvantage: struct { ... } enum ( ... ) -> enum struct { ... } ( ... ) becomes less readable.
  • Disadvantage: (In my eyes) it doesn't illustrate how this enum implementation works quite as well.

Add type inference to enum declarations

  • Advantage: Definitions become more concise, especially when declaring inline types with enums.
  • Disadvantage: The concise-ness comes with a price to readability, in that the original type of the enum is not in a consistent location.
  • My Comment: Type inference in Go is typically done in places which would benefit from it often, like declaring a variable. There really should be very few enum declarations "per capita" of code, so I (personally) think the verbosity of requiring the type is justified.

Use the Type.Value syntax for reading enum values

  • I've already talked about advantages and disadvantages to this above, but it was mentioned that we already use Type.Method to reference methods, so it wouldn't be quite as bad to reference enum values as Type.Value.

Ranging over enum values is not discussed

  • I forgot about it when writing the original text, but luckily it doesn't undermine the proposal. This is an easy thing to fit in though. We can use Type.Slice which returns a []Type

Regarding zero values

  • We have two choices - either the first enum value, or the zero value of the underlying type.
  • First enum value: Makes more intuitive sense when you first look at it
  • Zero value of type: More consistent with the rest of Go, but may cause a compile error if the zero value of the type is not in the enum
  • My Comment: I think the zero value of the type should be used. The zero value of a type is always represented as all-zeros in binary, and this shouldn't change that. On top of that, the only thing the enum "attachment" to a type does is limit what values variables of the type can hold. So under this rationale, I think it makes intuitive sense that if the enum for a type doesn't include the zero-value, then declaring a variable with the zero-value should fail to compile. This may seem strange at first, but as long as the compile error message is something intuitive (ie illegal assignment to fooVar: FooType's enum does not contain value <value>) it shouldn't be much of a problem.
@gopherbot gopherbot added this to the Proposal milestone Nov 28, 2018
@ianlancetaylor ianlancetaylor added LanguageChange Suggested changes to the Go language v2 An incompatible library change labels Nov 28, 2018
@ianlancetaylor

This comment has been minimized.

@ianlancetaylor
Copy link
Member

ianlancetaylor commented Nov 28, 2018

A commonly requested feature for enums is a way to iterate through the valid enum values. That doesn't seem to be supported here.

You discuss converting an integer value to Hardness, but is it also permitted to convert a value to Material? If not, what is the essential difference?

I'm not sure but I suspect that this syntax is going to introduce parsing ambiguities. There are many places where a type can appear. You also have to consider cases like

for _, x := range []struct { f int } enum { A = /* what do I write here? */

@deanveloper
Copy link
Author

Is there a section missing after "Let's take a look at some other languages"?

Yes, that was my bad. I've updated to include other languages.

A commonly requested feature for enums is a way to iterate through the valid enum values. That doesn't seem to be supported here.

Oops... That's my bad. Either way, once there is a syntax for it, it should be easily supported. In your upcoming example I will use values(Type).slice, which evaluates to a []Type.

I'm not sure but I suspect that this syntax is going to introduce parsing ambiguities. There are many places where a type can appear. You also have to consider cases like...

That's true. It's confusing if []struct { f int } enum { ... } is an enum of []struct { f int } or a slice of struct { f int } enum { ... }, making the contents of the enum very confusing. This also isn't even really that contrived of a case either, as type TSlice []T isn't uncommon. I'd personally assume that the enum is the last thing that is "applied" to the type, since the syntax is type Type <underlying type> enum <values>, but this isn't immediately obvious.

Under that assumption, the ambiguous code would work as:

for i, x := range values([]struct { f int } enum { A = []struct{f int}{struct{f int}{5},struct{f int}{2} }).slice {
    fmt.Println(i, x)
}

which would have the same output as:

for i, x := range [][]struct{f int}{{struct{f int}{5}, struct{f int}{2}}} {
	fmt.Println(i, x)
}

https://play.golang.org/p/d3HtDbyFZVp

@networkimprov
Copy link

Non-primitive enums (i.e. struct, array) is an interesting concept!

We should also consider
a) the var/const (...) declaration syntax
b) defining a namespace from a parent type name

enum Name ( global = "Atlas" ) // defines namespace & typename; underlying type inferred
type Name enum ( global = "Atlas" ) // alternatively

var word enum ( stop = "red"; go = "green" ) // inline enum applies to one variable
                                             // no namespace or typename defined

type Person struct {
   name Name
   status enum ( follows byte = iota; leads ) // namespace is Person
}

func f() {
   v := Person{ status: follows, name: Name.global } // Person namespace is implicit
   v.status = Person.leads

   word = stop // global namespace
   word = Name.global // compile error

   for n := range Name { ... } // iterate over enum with typename
}

type Material struct {
   kind enum ( Metal = 1 ... )
   hardness enum ( Malleable = 1 ... )
}
enum Composite ( gold = Material{Metal, Malleable} )

(My up-thumb is a qualified one :-)

@deanveloper
Copy link
Author

deanveloper commented Nov 29, 2018

a) the var/const (...) declaration syntax

The reason behind picking { ... } over ( ... ) was that it just looked more visually appealing when defining enums of struct types.

example:

type Example struct {
    i int
} enum (
    A = Example{1}
    B = Example{2}
)

versus

type Example struct {
    i int
} enum {
    A = Example{1}
    B = Example{2}
}

The symmetry of } enum { looks much nicer. I would say using parentheses does make more sense, since we are declaring a list of variables. I was pretty tied on which one to use.

b) defining a namespace from a parent type name

I addressed this, I didn't like it because Type.Value makes Type look like a value, even though it isn't. It does feel much more familiar to other languages however.

Something that bothers me about the example code is that I don't like the type inference. I think that since we don't typically need to declare enums too often, the extra verbosity makes the code much more readable. For instance:

type SomeNumbers enum (
    A = 15
    B = 92
    C = 29993
    D = 29.3
    E = 1939
)

What is the underlying type of SomeNumbers? Well you'd look at the first few numbers and think it's an int type, but because the D constant is 29.3, it would instead have to be float64. This is not immediately obvious and would be an especially large problem for long enums. Just using type SomeNumbers float64 enum ( ... ) would mitigate this issue

@networkimprov
Copy link

networkimprov commented Nov 29, 2018

Type.Value makes Type look like a value, even though it isn't

Type.Method is how you reference a method.

I don't like the type inference

Leveraging the const/var (...) pattern, let the first item clarify the type:

type SomeNumbers enum ( A float64 = 15; B = 92 ... )

Which makes type T struct { ... } enum { ... } unnecessary. (It isn't that readable to my eye.)

@deanveloper
Copy link
Author

deanveloper commented Nov 29, 2018

Type.Method is how you reference a method.

Fair point, completely forgot about that. One of those features that don't get used much, haha

Leveraging the const/var (...) pattern, let the first item clarify the type:

I personally think that

type Status int enum (
    Success = iota
    TimedOut
    Interrupted
    // ...
)

is more readable than

type Status enum (
    Success int = iota
    TimedOut
    Interrupted
    // ...
)

Although that's just be a matter of opinion. I think that the extra verbosity helps the readability in this case, and since people shouldn't be declaring enums all the time, it comes at a relatively low cost.

Which makes type T struct { ... } enum { ... } unnecessary. (It isn't that readable to my eye.)

I actually thought about forcing the underlying struct and enum definition to be separate (which this would effectively do). It was actually the initial design, but as I made examples, it raised a few issues.

Now, you have defined two types (an internal one for the structure, and the type with the enum to actually be used). So now you have this useless internal type floating around, which is only used to define an enum. Not a huge deal per se, but it's pretty inconvenient and seems like a waste of typing. It would also clutter up autocompleters for those who use them.

Another issue is documentation. Presumably you wouldn't want that struct to be exported, because it's only use is to be used by the enum. The issue with not exporting the struct is that now it doesn't appear on godoc.org, so people don't know what fields your enum has. So your two choices are to either export your struct, which is bad design since it's not supposed to be used outside of the enum, or to keep the struct unexported, which makes it invisible to godoc. The type T struct { ... } enum { ... } fixes this issue, since the underlying type is defined along with the enum rather than separate.

Also, defining the enum all at once illustrates that enums are an extension on types, and not types on their own. Doing type SomeNumbers enum ( ... ) makes it look like that enum ( ... ) is the underlying type, even though it's actually an int where enum just limits which int values that SomeNumbers can hold. The proposed syntax is a bit more verbose, but I think that a syntax where the underlying type is defined along with the type illustrates how it works a bit better.

Also, if you want to define the struct and enum separately, you still can:

type internalT struct { ... }
type T internalT enum ( ... )

type SomeNumbers enum ( A float64 = 15; B = 92 ... )

Even in the const/var pattern, that doesn't work. B would still be an int since you assign an untyped integer to it: https://play.golang.org/p/NnYCrYIsENm

Either way, this is all just syntax. I'm glad that there haven't been any problems with the actual concept yet!

@jonas-schulze
Copy link

jonas-schulze commented Nov 30, 2018

I think you are using the wrong associativity: type T enum int {} should resolve the ambiguity.

I like the overall concept, but how would you cover "multi-value" enums like this one: https://play.golang.org/p/V7DAZ1HWkN4? From the top of my head one could use

but this would add yet another keyword to the language. How would one extract the bit information? Using a & b != 0 feels clumsy, but a ~= b is even more syntax to consider.

Update: I already mixed up the base values. 🙈

@deanveloper
Copy link
Author

deanveloper commented Nov 30, 2018

I think you are using the wrong associativity: type T enum int {} should resolve the ambiguity.

That's true that it would solve ambiguity with existing data structures since "extensions" on types are usually prefixes (ie slices/arrays, channels, etc).

Apply this to structs and it gets a bit more messy in my eyes, especially if we adopt the (...) syntax rather than {...}

type Example enum struct {
    i int
} (
    A = Example{1}
    B = Example{2}
)

versus

type Example struct {
    i int
} enum (
    A = Example{1}
    B = Example{2}
)

I think the second example illustrates that Example is just a struct { i int } and that the enum is simply an extension to that type, rather than enum struct { ... } being some entirely new concept.

@jonas-schulze
Copy link

But Example isn't just a struct { int }, is it? I think it really is a struct { int } wrapped as an enum. Putting enum first would also be compatible with future type additions to the language. However, that's all syntax. What do you think about the "bit flag" use case for enums?

@networkimprov
Copy link

networkimprov commented Nov 30, 2018

You only need to specify the value type if its literal form isn't unique, i.e. numeric types other than int & float64.

enum int8 (a=iota; b) is indeed a good way to achieve that, and
enum (a int8 = iota; b) should also work for consistency with var/const.

Anonymous value types have problems...

type E struct { // looks like a struct type
  ...
} enum (        // surprise, an enum type
  a = E{...}    // surprise, it works like a struct type here
)
var x = E{...}  // surprise, error

Anonymous enum types are valuable (as I described above)...

type S struct {
   v enum (a = 1)
}

@deanveloper
Copy link
Author

deanveloper commented Nov 30, 2018

But Example isn't just a struct { int }, is it? I think it really is a struct { int } wrapped as an enum. Putting enum first would also be compatible with future type additions to the language. However, that's all syntax.

Example is a struct. "Enum type" (while I have used this term) is a misnomer. The enum keyword simply limits what values type Example is able to hold.

What do you think about the "bit flag" use case for enums?

I personally think that bit-flags should not be handled by enums. They are safe to just use as a raw numeric type in my eyes, since with bitflags you can simply ignore any bits which do not have meaning. I do not see a use-case for bitflags to need a construct, what we have for them now is more than enough.

Example is not wrapped as an enum. The enum specifier is an "attribute" to a type which limits what values it can hold.

Anonymous enum types are valuable (as I described above)...

You could equally achieve that with v int enum ( a = 1 ) which could follow the same namespacing rules that you described earlier. I didn't think of this in my original design, thanks for bringing it up!

type E struct { // looks like a struct type
  ...
} enum (        // surprise, an enum type
  a = E{...}    // surprise, it works like a struct type here
)
var x = E{...}  // surprise, error

I will accept that it may be bad for enum to be after the struct since type E struct ... looks like a plain struct type, but you don't see that it's enumerated until further down. But I could not think of a better syntax for defining the new type's underlying type AND value within the same statement.

@networkimprov
Copy link

networkimprov commented Nov 30, 2018

type E enum struct {
  a, b int
} (
  x = { 1, 2 }      // type inferred, as with []struct{...} { {...}, {...} }
  y = { a:2, b:3 }
)

type E enum [2]int (
  x = { 1, 2 }
  y = { 2, 3 }
)

Voila!

@deanveloper
Copy link
Author

I really don't like the } ( on the third line, I had mentioned it before. It feels wrong, but that's probably just me being picky haha. I'd be fine with it if it were implemented that way

@networkimprov
Copy link

Maybe you could rev the proposal to incorporate the above insights and alternatives?

@deanveloper
Copy link
Author

I've added a list of points at the end which summarize what has been talked about so far.

@networkimprov
Copy link

Thanks! Also you could cover...

a) anonymous enums

var a enum (x=1; y=2)
a = x
type T struct { a enum (x=1; y=2) }
t := T{a:x}
t.a = T.x

b) type inference examples for struct and array, essential for anonymous value types:
enum struct { ... } ( x = {...}; y = {...} )

c) the default value; is it the type's zero value or the first enum value?

d) enum (x int8 = iota; y) for consistency with the Go 1 enumeration method. Granted that doesn't work well for an anonymous struct.

@UFOXD
Copy link

UFOXD commented Dec 7, 2018

good

@deanveloper
Copy link
Author

deanveloper commented Dec 7, 2018

a) anonymous enums

I haven't directly addressed them, but it is implied they are allowed because enum int ( ... ) is just as much of a type as int is.

b) type inference examples for struct and array, essential for anonymous value types:

Adding after I'm done with this reply

Actually - I'd rather not. Not because I don't like type inference or anything, but these things are mentioned in a bulletted list. The list is meant to be a TL;DR of the discussion, and I don't want to be polluting it with long examples. I personally don't think it's "essential for anonymous value types" or anything, struct and array enums are just as much enums as ints are and really doesn't take much to wrap your head around how they work.

c) the default value; is it the type's zero value or the first enum value?

Thanks for the reminder, I'll add that in

d) enum (x int8 = iota; y) for consistency with the Go 1 enumeration method. Granted that doesn't work well for an anonymous struct.

I've already used iota in enum definitions within my proposal.

@deanveloper
Copy link
Author

@networkimprov I've updated the points of discussion

@clareyy Please read https://github.com/golang/go/wiki/NoPlusOne

@networkimprov
Copy link

Re type inference, you have this example, which weakens your case

type Material struct {
   ...
} enum {
   Metal = Material{...}, // Material is an enum; the struct is anonymous

Re zero value, I often do this: const ( _ int = iota; eFirst; eSecond ) Here the zero-value is in the enum, but anonymous. That probably shouldn't produce a compile error.

@deanveloper
Copy link
Author

deanveloper commented Dec 7, 2018

Re type inference, you have this example, which weakens your case

Material is not an enum. It is a type just like any other, but the enum keyword limits what values a type may hold. Doing Material{ ... } outside of the enum section of the type is still valid as long as the value comes out to a value that is within the enum section. I'd imagine tools like golint should discourage this behavior though to make it more clear that an enum is being used.

Re zero value, I often do this: const ( _ int = iota; eFirst; eSecond ) Here the zero-value is in the enum, but anonymous. That probably shouldn't produce a compile error.

I'd argue it should. iota is always zero for the zero'th index const. If you do _ MyEnum = 0 on an enum that does not contain a 0 value, it should produce a compile error as the second bullet in the "The following would be true with enums:" part states.

A work-around would be:

type Foo enum int ( x = iota+1; y; z) // note: no zero value is allowed for Foo

const (
    _ int = iota
    a Foo = iota
    b
    c
)

@deanveloper
Copy link
Author

As a side note: C++11 introduced enum classes that entail more rigorous type checking:

Actually that's very useful, thank you. My C++ knowledge is a bit outdated since my university professor didn't like C++11 haha.

I'll separate C and C++ in the list of languages.

@networkimprov
Copy link

If type Material struct { ... } enum ( ... ) defines type Material, then the following defines type Intish, which is not an int[1], so you must convert constants to it:

type Intish int enum (
   a = Intish(1)
   b = Intish(2)
)

[1] https://golang.org/ref/spec#Type_identity

@deanveloper
Copy link
Author

1, 2, 3, etc are untyped. So you can assign them to any value that has an underlying numeric type.

@griesemer
Copy link
Contributor

griesemer commented May 29, 2019

I like to go back to the original list of properties of an enum as enumerated (hah!) at the start of this proposal:

  • Named, immutable values.
  • Compile-time validation.
  • A concise way to define the values (the only thing that iota really provides).

I see these as three distinct, in fact orthogonal properties. If we want to make progress on enums in a way that reflects the spirit of Go, we cannot lump all these qualities into a single unified new language feature. In Go we like to provide the elementary, underlying, language features from which more complicated features can be built. For instance, there are no classes, but there are the building blocks to get the effect of classes (data types, methods on any data types, interfaces, etc.). Providing the building blocks simultaneously makes the language simpler and more powerful.

Thus, coming from this viewpoint, all the proposals on enums I've seen so far, including this one, mix way too many things together in my mind. Think of the spec entry for this proposal, for instance. It's going to be quite long and complicated, for a relatively minor feature in the grand scheme of things (minor because we've been programming successfully w/o enums for almost 10 years in Go). Compare that to the spec entry for say an slice, which is rather short and simple, yet the slice type adds enormous power to the language.

Instead, I suggest that we try to address these (the enum) properties individually. If we had a mechanism in the language for immutable values (a big "if"), and a mechanism to concisely define new values (more on that below), than an "enum" is simply a mechanism to lump together a list of values of a given type such that the compiler can do compile-time validation.

Given that immutability is non-trivial (we have a bunch of proposals trying to address that), and given the fact that we could get pretty far even w/o immutability, I suggest ignoring this for a moment.

If we have complex enum values, such as Material{Name: "Metal", Strength: values(Hardness).Hard } we already have mechanisms in the language to create them: composite literals, or functions in the most general case. Creating all these values is a matter of declaring and initializing variables.

If we have simple enum values, such as weekdays that are numbered from 0 to 6, we have iota in constant declarations.

A third, and I believe also common enum value is one that is not trivially a constant, but that can be easily computed from an increasing index (such as iota). For instance, one might declare enum variables that cannot be of a constant type for some reason:

var (
   pascal Lang = newLang(0)
   c Lang = newLang(1)
   java Lang = newLang(2)
   python Lang = newLang(3)
   ...
)

I believe this case is trivially addressed with existing proposal #21473 which proposes to permit iota in variable declarations. With that we could write this as:

var (
   pascal Lang = newLang(iota)
   c
   java
   python
   ...
)

which is a very nice and extremely powerful way to define a list of non-constant values which might be used in an enumeration.

That is, we basically already have all the machinery we need in Go to declare individual enum values in a straight-forward way.

All that is left (and ignoring immutability) is a way to tell the compiler which set of values makes up the actual enum set. An obvious choice would be to have a new type enum which lists the respective values. For instance:

type ProgLang enum{pascal, c, java, python} // maybe we need to mention the element type

That is, the enum simply defines the permissible set of values. The syntactic disadvantage of such an approach is that one will have to repeat the values after having declared them. The advantage is the simplicity, readability, and that there are no questions regarding the scoping of enum names (do I need to say ProgLang.pascal, or can I just say pascal for instance).

I am not suggesting this as the solution to the enum problem, this needs clearly more thought. But I do believe that separating the concerns of an enum into distinct orthogonal concepts will lead to a much more powerful solution. It also allows us to think about how each of the individual properties would be implemented, separately from the other properties.

I'm hoping that this comment inspires different ways of looking at enums than what we've seen so far.

@networkimprov
Copy link

networkimprov commented May 29, 2019

@griesemer this comment contains the core of another enum proposal, any thoughts?
#28987 (comment)

The discussion prior to that comment provides more details...

@griesemer
Copy link
Contributor

@networkimprov I deliberately did not start yet another enum proposal because what I wrote above also needs much more thought. I give credit to this (@deanveloper 's) proposal for identifying the three properties of an enum, I think that's a useful observation. What I don't like with this current proposal is that it mixes the declaration of a type with the initialization of values of that type. Sometimes, the type declaration appears to define two types (as with his Material example which declares the Material enum type, but then that type also is used as the element type for the composite literals that make up the enum values). There's clearly a lot going on. If we can disentangle the various concerns we might be better off. My post was an attempt at showing that there are ways to disentangle things.

Also, one thing that I rarely see addressed (or perhaps I missed it) is how an implementation would ensure that a variable of enum type can only hold values of that enum type. If the enum is simply a range of integers, it's easy. But what if we have complex values? (For instance, in an assignment of an enum value to an enum-typed variable, we certainly don't want a compiler have to check each value against a set of approved values.) A way to do this, and I believe @ianlancetaylor hinted at that, is to always map enum values to a consecutive range of integer values (starting at 0), and use that integer value as an index into a fixed size array which contains the actual enum values (and of course, in the trivial case where the index is the enum value, we don't need the array). In other words, referring to an enum value, say e, gets translated into enum_array[e], where enum_array is the (hidden) storage for all the actual enum values of the respective enum type. Such an implementation would also automatically imply that the zero value for an enum is the zero element of the array, which would be the first enum value. Iteration over the enum values also becomes obvious: it's simply iteration over the respective enum array elements. Thus, with this an enum type is simply syntactic sugar for something that we currently would program out by hand.

Anyway, all these are just some more thoughts on the subject. It's good to keep thinking about this, the more perspectives we have the better. It might well be that at some point we arrive at an idea that feels right for Go.

@networkimprov
Copy link

networkimprov commented May 29, 2019

an enum type is simply syntactic sugar for something that we currently would program out by hand.

@griesemer, else, for, return, break, continue, etc are syntactic sugar for goto ;-)

So you don't believe an enum type is worth any effort? We should just use iota-defined values as indices, and let apps panic at runtime on index out-of-bounds instead of failing to compile?

If that's not your gist, I think you missed my question: whether the contents of this comment (and discussion preceding it) would dodge your critique of this proposal: #28987 (comment)

@jimmyfrasche
Copy link
Member

@griesemer

All that is left (and ignoring immutability) is a way to tell the compiler which set of values makes up the actual enum set. An obvious choice would be to have a new type enum which lists the respective values. For instance:

type ProgLang enum{pascal, c, java, python}

I realize this is just an off the cuff example, but I don't think you can ignore immutability there. If you define a type by a list of mutable values, which can contain pointers, what does it mean for one of those values to then be mutated? Does the type's definition change at runtime? Is a deep copy of the value made at compile type and put in enum_array? What about pointer methods?

type t struct { n *int }
a := t{new(int)}
var e enum { a }
*a.n += 1
fmt.Println(*e.n) // Is this one or zero?
e = a // Is this legal or not?

You could forbid pointers in the base type (for lack of a better term) of an enum declaration, but that makes it equivalent to allowing arrays and structs to be constants when they consist only of types that can be constant #6386 (comment), except that without the immutability you could still easily do confusing things:

foo = bar{2}
var e enum{foo} = foo
foo = bar{3}
e = foo // illegal?

And, even if you didn't forbid pointers, you'd probably have to forbid types that cannot be compared since you'd need to compare the values to see where and if they map into enum_array at runtime.

Mixing types and values like that also allows some strange things like

func f(a, b, c int) interface{} {
  return enum{a, b, c}
}

I'm not sure it could made to work, but, if it could, it would be the kind of thing where the instructions for using it are all footnotes and caveats.

I'm all for providing orthogonal primitives, but I don't think what most language's define as enums can be split apart cleanly. Much of them are inherently and purposefully, "let's do a bunch of stuff all at once so that we don't always have to do the same stuff together".

@griesemer
Copy link
Contributor

@jimmyfrasche Fair point - defining an enum as a simple collection of values is probably not good enough if we can't guarantee that the values are immutable. Perhaps immutability needs to be a requirement for values used in enums. Still, I believe immutability is a separate, orthogonal concept. Or put another way, if enums somehow magically could be used to enforce immutability of the enum values, people would use enums to get immutability. I haven't seen such enums yet. And if enum elements are suitably restricted such that immutability is easy (e.g. they must be constants), then we also have enum instructions with "all footnotes and caveats".

Regarding the splitting apart of enum features: In older languages, enums simply provided an easy way to declare a sequence of named constants, together with a type. There was little enforcement (if any) that a value of enum type could not contain other (non-enum) values. We have split out these pieces in Go by providing type declarations and iota, which together can be used to emulate such basic enums. I think that worked pretty well so far and is about as safe (or unsafe) as historic enums.

@networkimprov I'm not saying we shouldn't have enums, but the demand in actual programs needs to be pretty high for them (for instance, we have switch statements because they are pretty common, even though we also have the more basic if that could be used instead). Alternatively, enums are so simple a mechanism that they are perhaps justifiable because of that . But all the proposals I've seen so far seem pretty complex. And few (if any) have drilled down to how one would implement them (another important aspect).

@aprice2704
Copy link

aprice2704 commented Jun 6, 2019

I would like to use enums for iterating over fairly short lists of things known at compile time, my conception of them having come from Pascal. Unlike @deanveloper's list of reqs, I personally would prefer only the deliberately inflexible:

  1. An enum type defines a sequence of constants that may be cast to ints (specifically ints) and behave like them in many places.
  2. The constants are defined at compile time (natch) and may be iota, or a constant that can be an int.
enum Fruit ( 
   Apple = iota
   Cherry
   Banana
)
enum WebErrors (
   MissingPage = 404
   OK = 200
)

Thus the compiler knows all the possible values at compile time and may order them.

var f Fruit     ... initialized to Apple
var e WebError   ... initialized to OK
g := Cherry
f = Fruit(3)   ... compiler error
f = Apple + 1 ... compiler error
f = Apple + Banana ... compiler error
f = Fruit(0)   ... Apple
myerr = WebError(goterror)  ... run time bounds checking invoked --> possible panic
int(Cherry)    ... gives 1
range Fruit   ... ranges *in order*
g.String()   ... gives "Cherry"
Fruit.First()  ... gives Apple
Fruit.Last() ... gives Banana
Banana.Pred() ... gives Cherry as does Banana--
Cherry.Succ() ... gives Banana as does Cherry++ or g++
j := Banana++  ... compiler error on bounds
crop := make(map[Fruit]int)
crop[Cherry] = 100
for fr := range Fruit { fmt.printf("Fruit: %s, amount: %d\n", fr, crop[fr]) }  <--- nicely ordered range over map 

A compact 'set' would also be nice, functionally almost identical to a map[enum]bool; again, I want to emphasize that I would be totally happy (in fact prefer) that the possible enum values must be known at compile time and are really part of the code, not true data items (n.b. http errors are probably a better example than fruit in practice). If they are unknown at compile time, and are thus data items, then one expects to write more code (to order, check bounds etc.), but this is annoying in very simple cases where the compiler could just deal with it easily.

type Fruits set[Fruit]
var weFarm Fruits
weFarm[Banana] = true
if weFarm[Apple] { MakeCider() }
for f := range Fruit { if weFarm[f] { fmt.Println("Yes, we can supply %s\n", f) } <-- note, in order, not map-like. In most cases could be implemented with in-register bitset.

Can the above be achieved with existing maps? Yes! with (minor) annoyances. However, I think this is a way for the language to express an opinion: "In these very simple cases, that are code really, this is the one true way you should do sequences and sets".

(this is very unoriginal and perhaps not directly relevant to @deanveloper 's proposal, sry :/ )

@rsr-at-mindtwin
Copy link

I think it would be appropriate to default to implementing behavior like -Wswitch-enum for the new enumeration type - that is, when switching on a given enumeration type, make sure all cases are covered in the absence of a default case, and warn when values are present that are not part of the defined enumeration. This is valuable for software maintenance when adding new cases.

@deanveloper
Copy link
Author

Go does not have compiler warnings (only errors), however it may be a good thing to add to govet.

@Miigon
Copy link

Miigon commented Jan 24, 2021

It's 2021 2022 and I am still hoping we can get enum

@emperor-limitless
Copy link

Any news about this?

@ianlancetaylor
Copy link
Member

There is no news. There isn't even agreement on what problem to solve. And this is affected by generics in 1.18, with the introduction of interface types with union type elements.

@enkeyz
Copy link

enkeyz commented Apr 15, 2022

Would be nice to have, instead of the current hacky way(iota).

@Splizard
Copy link

Splizard commented May 5, 2022

There is no need to change the spec, Go 1.18 has everything that is required to define namespaced and marshalable 'enum types', including both exhaustive and non-exhaustive type switches, where the compiler ensures the assignment of valid values and automatic String(), MarshalText() methods. You can use these enum types in Go today.

package main
import (
	"fmt"
	"qlova.tech/sum"
)
type Status struct {
	Success,
	Failure sum.Int[Status]
}
var StatusValue = sum.Int[Status]{}.Sum()
func main() {
	var status = StatusValue.Success
	// exhaustive switch
	status.Switch(StatusValue, Status{
		status.Case(StatusValue.Success, func() {
			fmt.Println("Yay!", status) // output: Yay! Success
		}),
		status.Case(StatusValue.Failure, func() {
			fmt.Println("Nay!", status) // doesn't output
		}),
	})
}

Here's a weekday example to play around with:
https://go.dev/play/p/MnbmE0B3sSE also see pkg.go.dev

@melbahja
Copy link

melbahja commented Nov 18, 2023

IMO just make it simple as possible:

type Status enum[int] {
	ALLOWED = iota
	BLOCKED
}
type Status enum[string] {
	ALLOWED = "y"
	BLOCKED = "n"
}

func foo(s Status) {
   println(s.(string))
}

foo(Status.ALLOWED)

@chenjpu
Copy link

chenjpu commented Nov 18, 2023

IMO just make it simple as possible:

type Status enum[int] {
	ALLOWED = iota
	BLOCKED
}

type Status enum[string] {
	ALLOWED = "y"
	BLOCKED = "n"
}

:)

type Status[int]  enum  {
	ALLOWED = iota
	BLOCKED
}

type Status[string] enum  {
	ALLOWED = "y"
	BLOCKED = "n"
}

@Malix-Labs
Copy link

Malix-Labs commented Feb 22, 2024

2024 is the year of Go enum (source: personal feeling)

@emperor-limitless
Copy link

Did something happen? Your comment makes no sense.

@Hixon10
Copy link

Hixon10 commented Apr 13, 2024

Every time, when I need to write default statement, I feel bad. Compiler already knows, that it is unreachable code. I don't need it:

type DayOfWeek int

const (
    Sunday DayOfWeek = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

func (d DayOfWeek) String() (string, error) {
    switch d {
    case Sunday:
        return "Sunday", nil
    case Monday:
        return "Monday", nil
    case Tuesday:
        return "Tuesday", nil
    case Wednesday:
        return "Wednesday", nil
    case Thursday:
        return "Thursday", nil
    case Friday:
        return "Friday", nil
    case Saturday:
        return "Saturday", nil
    default:
        return "", errors.New("invalid day")
    }
}

@doggedOwl
Copy link

doggedOwl commented Apr 14, 2024

But it is reachable: fmt.Println(DayOfWeek(15).String()).
The fact that you have defined 7 constants does not limit the DayOfWeek type to contain only those 7, and that's why this kind of simulated Enum is not enough.

@Hixon10
Copy link

Hixon10 commented Apr 14, 2024

But it is reachable: fmt.Println(DayOfWeek(15).String()) and that's why this kind of simulated Enum is not enough.

Exactly! We need enums (or algebraic data types), and exhaustive switch.

@ianlancetaylor ianlancetaylor changed the title proposal: Go 2: enums as an extension to types proposal: spec: enums as an extension to types Aug 6, 2024
@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
@ConradIrwin
Copy link
Contributor

Having been recently working in rust, I've enjoyed two aspects of enums as defined there:

  • Closed types - no-one can pass a value I wasn't expecting to a method.
  • Exhaustive switches - the compiler can complain if I miss a branch

Rust obviously goes a lot further, but I think these two features would be interesting to explore.

How about "value" interfaces:

type Color interface {
  "red" | "green" | "blue"
}

A value interface can contain any number of same-typed constant expressions separated by |. This would be equivalent:

const (
  ColorRed = "red"
  ColorGreen = "green"
  ColorBlue = "blue"
)
type Color interface { 
  ColorRed | ColorGreen | ColorBlue
}

Value interfaces would participate in constant conversion so that given a method like:

func ForCSS(c Color) string {
  ...
}

I could call it:

blue := ForCSS("blue")
blue := ForCSS(ColorBlue)

Value interfaces would re-use the same syntax as non-value interfaces for fallible casting (though on non-interface types):

var s string = "blue"
if c, ok := s.(Color); ok {
  // yay!
}
var s string = "grub"
s.(Color) // panic

And if you want the underlying value you can do the opposite:

var s string = ColorRed.(string)

A switch statement handles value interfaces specially:

switch color {
  "blue": 
  "green":
// compile error: unhandled case "red"
}

(maybe?) The range keyword could be extended to take a value interface:

for c := range Color {
}

The zero value for the value interface is the first one in the list.

var c Color
c == "red"

And that's kind of it?

Two things make me think that this may not work so well in Go as they do in Rust:

  • Go cares about backward compatibility, and if you have a public value interface you cannot add (or remove) a value without breaking backward compatibility. That might be reason enough to say "enums don't fit Go's ethos".
  • Go types must have a zero value, but there's no real "zero" for many enumerations (as in this example where "red" is the default). It's kind of meh.

A final detail to untangle would be if you want your constants to be typed, it'd be nice to support:

const (
  FlagFoo = Flag(iota)
  FlagBar
  FlagBaz
)
type Flag interface { 
  FlagFoo | FlagBar | FlagBaz
}

I think the compiler could notice this pattern and use the underlying type and value of the constant if it is cast to the interface at definition time.

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