Repository: go
Issue: proposal: spec: enums as an extension to types
Labels: LanguageChange,Proposal,LanguageChangeReview

# 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. https://github.com/golang/go/issues/28987#issuecomment-445296770

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.