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

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

Comments

Projects
None yet
9 participants
@deanveloper
Copy link

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

@gopherbot gopherbot added the Proposal label Nov 28, 2018

@ianlancetaylor

This comment was marked as resolved.

Copy link
Contributor

commented Nov 28, 2018

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

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Author

commented Nov 28, 2018

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

This comment has been minimized.

Copy link

commented Nov 28, 2018

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link

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

  • enum (which for base type int would be numbered +=1 *=2) and exclusive enum (which for base type int would be numbered *=2 +=1) or
  • enum and bitflag (like https://github.com/mvpninjas/go-bitflag does),

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link

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. What do you think about the "bit flag" use case for enums?

@networkimprov

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Author

commented Nov 30, 2018

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

This comment has been minimized.

Copy link

commented Dec 1, 2018

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

@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 1, 2018

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

@networkimprov

This comment has been minimized.

Copy link

commented Dec 1, 2018

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.

@Allenyn

This comment has been minimized.

Copy link

commented Dec 7, 2018

good

@deanveloper

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link
Author

commented Dec 7, 2018

@networkimprov I've updated the points of discussion

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

@networkimprov

This comment has been minimized.

Copy link

commented Dec 7, 2018

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link
Author

commented Dec 7, 2018

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

This comment has been minimized.

Copy link

commented Dec 7, 2018

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

This comment has been minimized.

Copy link
Author

commented Dec 7, 2018

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

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Dec 19, 2018

Thanks, but I have to say that I am less concerned about changing the documentation and linting tools than I am about changing the language.

@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 19, 2018

There's going to be a pretty significant language change either way. Although I think my main gripe with returning an unexported type is that an unexported type logically should only be used by the package that uses it. In fact, var x pack.matValue is invalid, which is a similar issue to what this proposal encounters with the whole "what's the zero value of an enum" problem.

@networkimprov

This comment has been minimized.

Copy link

commented Dec 19, 2018

@deanveloper let's keep thinking on ways to limit a defined or anonymous type to predefined values...

type t struct {...}
var ( Allow1 = t{...}; Allow2 = t{...} ) // common today

enum t ( Allow1, Allow2 ) // plugs into existing code!

... struct {
   a int enum (1, 2) // defines anonymous type
}
@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 19, 2018

That example has the issue that you explained earlier, where we don't see that type t is an enum until much later in the code. Also seems a bit clumsy since you need to restate yourself. Ideally, you should only need to type out the list of variables once

@networkimprov

This comment has been minimized.

Copy link

commented Dec 19, 2018

The point is to enhance existing code with minimal effort. Take two:

type t struct {...}
var ( Allow1 = t{}; ... AllowN = t{} ) // existing code

var A t enum                    // any var t in scope
var B t enum ( Allow1, Allow2 ) // just these
var C t enum ( Permit = t{} )   // new one in current scope?
@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 19, 2018

It's an interesting idea to store the enum values in a variable of type t enum .... But now declaring an enum in a new project becomes clumsy, and I'd like to avoid having multiple ways to do write the same code.

Also, I'd like to point out that under this proposal, we cannot create an enum from a list of var because enum values should be required to be immutable. Take the following example:

// a person is defined by their name.
type person string
var ( Me = "Dean" )

func main() {
    var a string enum(Me) = Me
    Me = "Deanveloper"
    fmt.Printf("T:%[0]T a:%v Me:%v\n", a, a, Me) // T:string enum(Me) a:Dean Me:Deanveloper
    // problem: `Me`=="Deanveloper" but `a`=="Dean", even though `a` should only hold what `Me` does.
}
@networkimprov

This comment has been minimized.

Copy link

commented Dec 19, 2018

It's no more clumsy than in Go1; you simply add enum to a variable declaration; supplying a list of values would be uncommon. Values would be immutable when const, and there are open items proposing new const types.

My proposal actually preserves the Go1 method as the only way, and is a relatively small language change. An embedded definition was just a thought; more below.

func f() {
   var a t enum ( Permit = t{} ) // new definition only in func scope?
}
type S struct {
   a t enum ( Permit = t{} ) // new definition only in type def, which namespaces it?
}
var s S
s.a = S.Permit
@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 19, 2018

It's not just as clumsy as Go 1, because in Go 1 we don't have to redeclare our values (because we don't have enums), which is where I feel that the clumsiness comes from.

Also I really think that we should be primarily focusing on named enums which get reused in many places, not enums that are inlined (and therefore only used in one place). That's the primary goal of this proposal, other enum proposals, and enums in other languages.

@networkimprov

This comment has been minimized.

Copy link

commented Dec 20, 2018

Re "we don't have to redeclare our values" you missed the first of my three enum options above. #28987 (comment)

The Go team considered and omitted enums. If we need to limit vars to certain values, I imagine they'd prefer to address that within the current framework.

@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 20, 2018

Sorry, when I say "redeclare" I mean you declare the enum values (in the var declaration) and then a second time in the enum declaration (ie with var B ...)

The var A t enum actually works pretty okay, however.

Enums should work on the type-level instead of the var level. An ideal use case for an enum would be something like time.Weekday, where we would be using time.Weekday in multiple areas. Things like that are the primary use-case that I am going for. In these cases, enums need to be on the type level, so that when I am writing a function that takes a weekday, it doesn't look like

func Tomorrow(today time.Weekday enum(time.Sunday, time.Monday, time.Tuesday, ...)) time.Weekday enum(time.Sunday, time.Monday, time.Tuesday, ...) { ... }

That is what I mean when I say that this proposal focuses on named enums, rather than enums that only operate on a single variable.

@networkimprov

This comment has been minimized.

Copy link

commented Dec 20, 2018

You would write

func Tomorrow(day time.Weekday enum) time.Weekday { ... }
@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 20, 2018

Small typo (I'm assuming), but you'd want to return time.Weekday enum, not just time.Weekday. Otherwise you wouldn't be able to assign the result to a time.Weekday enum.

And this statement may be controversial, but I'd much rather have a feature that feels good to use, than one that is easy to convert legacy code to but seems half-baked.

And of course another issue is that I'd rather not need to write enum after every time I want to use one. It's the reason that typedef came about in C, because it's clumsy to write struct, enum, union, etc. every time I wanted to use one of those types. I'd much rather just use the type's name. A type is already a set of what values a variable can hold, so ideally if we're limiting what values a variable can hold, we should do that at the type level.

@networkimprov

This comment has been minimized.

Copy link

commented Dec 20, 2018

Typo, yes. I was thinking it might not be nec, but it is.

Requiring type_name enum declaration is a feature; they're not like other types. And it permits unconstrained instances of the type where you need user-defined alternatives with v := type_name(x)

You're right, enum is nec for type defs, not just declarations. Then enum (subset) isn't a burden.

type Workday time.Weekday enum (time.Monday..time.Friday) // segment of a var (...) list

var A Workday enum = time.Monday
var B Workday enum = time.Sunday // compile error
@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 20, 2018

Since Workday is based on time.Weekday enum (...) ideally we should just use Workday rather than Workday enum.

Also, since Workday and Weekday are separate types, they aren't assignable, unfortunately. I wouldn't like to change that. A typealias would make sense in this case though, since workdays are literally weekdays (rather than just being based on them).

So then perhaps the "make my pre-existing type an enum" code would become

type Weekday int
const (
    Sunday Weekday = iota
    // ...
)

type WeekdayEnum = Weekday enum

And then you could pass around a WeekdayEnum as you would a Weekday, with enum safety.

I'll add this to the points of discussion once I'm at my computer; I'm commenting from mobile right now.

@networkimprov

This comment has been minimized.

Copy link

commented Dec 20, 2018

Right, Workday needs the type identity of Weekday, so would use alias syntax as you suggest
type Workday = time.Weekday enum (time.Monday...)

These then work
var w Workday = time.Friday
var d time.Weekday enum = w
var z = time.Weekday(8)

@networkimprov

This comment has been minimized.

Copy link

commented Dec 21, 2018

To summarize...

Requirements:
a) integrate with and enhance existing code
b) enable value lists using all/some existing values
c) enable values of any type, and constant literals
d) don't require const values (since const only applies to certain types at present)
e) enable definition of enumeration types
f) enable embedded value definitions

// existing code
type t struct {...}
var ( Allow1 = t{}; ... AllowN = t{} )

// enhance existing code
var a t enum                        // admit any var t in scope
var b t enum ( Allow1, Allow2 )     // admit a subset
var c t enum ( Allow1..Allow9 )     // admit a range from a var/const (...) list
var i int enum (1, 2)               // admit constants

// define enum type
type T1 = t enum                    // alias syntax
type T2 = t enum (Allow1, Allow2)

var d T2 = Allow1
var e T1 = d                        // any-var type admits subset's values

var v = 2
type S struct {
   a int enum (1, v)                // compile error? require const for primitive types
}
s := S{a: 2}                        // or compile error? v is var and 2 is const

// embedded definition ideas

var f t enum ( Permit = t{} )       // defines var in 't enum' scope?
var g t enum = Permit 
var v t = Permit                    // compile error; not in scope

type S struct {
   a t enum ( Permit = t{} )        // type def creates namespace for new values?
}
s := S{a: Permit}                   // namespace implicit?
s.a = S.Permit
@deanveloper

This comment has been minimized.

Copy link
Author

commented Dec 21, 2018

I have no clue where these requirements are coming from. Also I'd personally much rather require constant types, and wait for const to be allowed on all types, than to get this proposal accepted too early, thereby requiring we comply to unwanted behavior.

I'd also like it to mostly remain unchanged, I feel like a lot of the proposed changes you have been making are more apt to a new proposal rather than an amendment to this one.

In conclusion, I'd really like this proposal to remain mostly unchanged. Maybe some syntax here and there, but the overall concept should remain untouched. This proposal values making a good implementation over sacrificing quality to make it "fit better" into old code.

@viper10652

This comment has been minimized.

Copy link

commented Jan 10, 2019

Don't forget enumeration subtypes.
e.g. you want to declare an enumeration type WEEKDAYS
and a subtype WORKDAYS of type WEEKDAYS.

So the value "Monday" evaluates to True when testing to be of type WEEKDAYS and WORKDAYS.

If you have atype WEEKDAYS with values (Mon, Tue, Wed, Thu, Fri, Sat, Sun)
You want to be able to define your subtype as:

  1. an list of values, e.g. (Mon, Tue, Wed, Thu, Fri)
  2. a range of values, e.g. (Mon ... Fri)

additional constraints:

  1. a subtype can only have values from the main type, i.e. no additional values

  2. the numeric value of a subtype needs to be identical as its corresponding value in the main type, e.g. if the numeric value for enumeration value Monday is 2 in WEEKDAYS, then it needs to have the numeric value in WORKDAYS as well.

You need the capability to convert between unconstrained types (e.g. int) and an enumeration.
This is useful when decoding bytestreams to and from data types.
E.g. decoding an array of bytes received over a serial line into a structure of fields. Often these fields can be enumeration types in communication protocols.

You also want to be able to reference the enumeration values as identifiers and test for inclusion in related types
e.g.
type WEEKDAYS enum (Mon, Tue, Wed, Thu, Fri, Sat, Sun)
type WORKDAYS enum (Mon ... Fri)
type WEEKEND enum (Sat, Sun)
var wday WEEKDAYS
wday := WEEKDAYS .Mon
if wday in WORKDAYS {} else
if wday in WEEKEND {

@deanveloper

This comment has been minimized.

Copy link
Author

commented Jan 10, 2019

I don't know if I like using the ... syntax since it's already used for variadic arguments.

When you say "don't forget enumeration subtypes", we already have them.

type Weekday enum int (Sun = iota; Mon; Tues; Wed; Thurs; Fri; Sat)

type Workday enum Weekday (Mon = Weekday.Mon + iota; Tues; Wed; Thurs; Fri)

type Weekend enum Weekday (Sun = Weekday.Sun; Sat = Weekday.Sat)

I don't want to complicate enums and make their learning curve steeper by adding additional features that are (the way I see it) not absolutely needed.

@viper10652

This comment has been minimized.

Copy link

commented Jan 11, 2019

I don't want to complicate enums and make their learning curve steeper by adding additional features that are (the way I see it) not absolutely needed.

That is a subjective statement.
You can state that YOU don't see a need for it, which is fine, but making a global statement for the whole golang community is a bold statement at best.

I suggested the feature because I got used to it when I was programming in Ada, and I saw a definite need for it back then (and a lot of Ada programmers agreed with me), but that is just my opinion (and that of a lot of Ada programmers). Please don't discard suggestions from people that were able to use the feature in other languages. If you don't find it useful, then don't use it, but at least allow others who do find it useful to use it.
Remember: "In the eyes of a hammer, a screw doesn't seem to be very useful"

@deanveloper

This comment has been minimized.

Copy link
Author

commented Jan 11, 2019

The idea of "sub enums" also seems to be a more object-oriented concept which involves inheritance, which makes sense with Ada being object oriented. I'd definitely like to stay away from that. This integrates well with Go's current (and simple) type system, so I'm very wary of adding too many features to it. Go is great (in my eyes, of course) because it's picky about what gets added into the language.

I definitely didn't mean to discard it entirely, I'm just stating that it doesn't really match with the values of this proposal. I know I kinda come off as harsh sometimes, I promise I'm not trying to do that

@beoran

This comment has been minimized.

Copy link

commented Jan 24, 2019

A Go1 backwards compatible way to do this, and to unify this with #29649 would be to reuse the range keyword in type definitions, and use it like range(type allowed values and ranges), much like we now have chan(message type) . Furthermore the simplest thing that could work do would be to limit the allowed values to constant expressions and constant ranges. You would have to define the values of the range then outside of the range type declaration itself, but that solves a few sticky issues on name spacing as well. Something like this:

type WeekdayValue int
const ( 
 Monday WeekdayValue = iota
 Tuesday
 Wednesday
 Thursday
 Friday
 Saturday
 Sunday
)

type WeekDay range(WeekdayValue Monday...Sunday) 
type AlternativeWeekDay range(WeekdayValue Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday) 
type WorkDay range(WeekdayValue Monday...Friday)
// String constants also allowed.
type EnglishDayNames range(string "monday", "tuesday", "wednesday", "friday", "saturday", "sunday")

// Allowed
var wd WeekDay = Monday
// Also Allowed
var wd WeekDay = WeekDayValue(1)
// Compiler error
var wd WeekDay = WeekDayValue(20)

// Example of iteration and indexation
func ExampleIterateWeekDay() {
  wd := range WeekDay {
    frt.Printf("%d:%s\n", int(wd), EnglishDayNames[int(wd)])
  }
}

If I see enough people like this approach I will write out a full design document for this idea.

@deanveloper

This comment has been minimized.

Copy link
Author

commented Jan 24, 2019

The reason I am opposed to defining the underlying type and enum is illustrated in an earlier comment, which is that now we have a "useless" type (WeekdayValue in your case) that shouldn't be used anywhere other than to define the "useful" type (Weekday). I think that needless types should be avoided (aka we should define the enum type and it's values together) which is a large part of the reason that I made this proposal.

That approach also would not work for enums of structs which was a large part of this proposal. Instead you would need to have an enum of indices which reference a slice. This isn't a bad solution, and has been discussed previously, but this proposal just doesn't really "get along" with that one very well.

It looks like your proposal would actually be the exact same as #29649 though, it's doesn't appear any different (at least immediately) besides that you can specify a comma separated list of numbers, which I personally think defeats the purpose of a range type. Please correct me if I am wrong though.

Not trying to rip on this idea or your prospective proposal though! A number range type would be another good solution to an enum proposal that feels Go-like.

@networkimprov

This comment has been minimized.

Copy link

commented Jan 24, 2019

I already wrote that design document here #28987 (comment) :-)

@beoran

This comment has been minimized.

Copy link

commented Jan 28, 2019

Well, my idea is to unify ranged types with enumerations, in a way that is backwards compatible with Go1. I don't mind not getting enums of structs, enums of ConstantExpressions with matching underlying types would be very useful. Maybe later we will get some structs as ConstantExpressions as well.

I see your point about wanting to declare the values of the enums as well, as in a struct's members. It would not be too hard to extend my idea for that, I think.

I started to write out my ideas at here https://gist.github.com/beoran/83526ce0c1ff2971a9119d103822533a, still incomplete, but maybe worth a read.

@caibirdme

This comment has been minimized.

Copy link

commented May 10, 2019

When I find a function:

func DoSomething(t UserType)

I don't know which UserTypes can I choose, IDE can't do the intelligent completion.
The only way to do that is jumping to the definition of UserType and see if there're some related const definition like:

type UserType byte
const (
  Teacher UserType = iota
  Driver
  Actor
  Musician
  Doctor
  // ...
)

But if the author doesn't define those constants near UserType, it's hard to find them.

Another thing is that, UserType is just an alias name for byte(not strictly), people can convert any byte value to UserType like UserType(100). So the consumer of UserType have to check its validation on runtime, like:

func DoSomething(t UserType) {
  switch t {
    case   Teacher,Driver,Actor,Musician,Doctor:
    default:
      panic("invalid usertype")
  }
}

This actually can be done on compile time.

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