Description
I think Go is missing an enum type to tie different language features together. This is not simply a "better way to use iota" but rather a new feature to simplify code logic and extend the type system.
Note: I read #19814 and it's a good start, but I think some important parts were missing.
Proposal:
This is a backward-compatible proposal to introduce a new type called enum
. With this new type will allow us to group together values and use them in a type-safe manner. It resembles a Go switch, but it defines a variable declaration (var) value. Also, there is no default enum field case. The enum-case block is a typical block, which may return the value of the field. When no return is specified, the field type defaults to int. Enum field values are not assignable, this differs from other enum implementations.
Syntax: Default (int)
// no type, defaults to int. fields with no returns will return the index ordering value.
enum Status {
case success: // value: 0
case failure: // value: 1
case something:
return -1 // value: -1
case someFunc(t time.Time): // please keep reading for func below
return int(t.Since(time.Now))
}
fmt.Printf("%T,%v", Status.success, Status.success) // output: int,0
fmt.Printf("%T,%v", Status[1], Status[1]) // output: int,1
fmt.Printf("%T", Status) // output: enum []int
Syntax: Not assignable
// this fails
Status.failure = 3
// this works
s := Status.success
fmt.Printf("%T,%v", s, s) // output: int,0
Syntax: Specific value type
enum West string {
case AZ:
return "Arizona"
case CA:
return "California"
case WA:
return "Washington"
}
fmt.Printf("%T,%v", West.AZ, West.AZ) // output: string,Arizona
fmt.Printf("%T,%v", West[2], West[2]) // output: string,Washington
fmt.Printf("%T", West) // output: enum []string
Syntax: Embedding - all enum types must match
enum Midwest string {
case IL:
return "Illinois"
}
enum USStates string {
West
case Midwest.IL: // "Illinois"
case NY:
return "New York"
}
fmt.Printf("%T,%v", USStates.AZ, USStates.West.AZ) // output: string,Arizona
fmt.Printf("%d,%d", len(USStates), len(USStates.West)) // output: 5,3
Syntax: Functions - case matches func signature, and the funcs must return values matching the enum type.
func safeDelete(user string) error {
return nil
}
enum Rest error {
case Create(user string):
if err := db.Store(user); err != nil {
return err
}
return nil
case Delete(person string):
return safeDelete(person)
}
err := Rest.Create("srfrog")
Syntax: For-loop - similar to a slice, the index match the field order.
for k, v := range USStates {
fmt.Printf("%d: %s\n", k, v)
}
for t, f := Rest {
if t == 0 { // Create
f("srfrog")
}
}
Syntax: Switches
switch USStates(0) {
case "Arizona": // match
case "New York":
}
// note: West.CA is "California" but index in West is 1. Fixed example.
party := West.CA
switch party {
case USStates.AZ:
case USStates.CA: // match
case USStates.West.CA:
case West.CA:
}
Discussion:
The goal is to have an enum syntax that feels like idiomatic Go. I borrowed ideas from Swift and C#, but there's plenty of room to improve. The enum
works like a slice of values, and it builds on interface. Using enum case blocks as regular blocks and func case matching allows us to extend its use; such as recursive enums and union-like.
Implementation:
I tried to reuse some of the existing code, like switch and slices. But there will be significant work for the func cases and enum introspection. I suppose that internally they could be treated as kind of a slice. Needs more investigation.
Impact:
I think this feature could have a large impact. Specially for simplifying existing code. And it could potentially render iota
obsolete (?). Many devs, myself included, use const/iota blocks with string types to create and manage status flags. This is a lot of boilerplate code that could basically vanish with enums. The grouping part is also beneficial to keep similar values and operations organized, which could save time during development. Finally, the type assignment reduces errors caused by value overwrites, which can be difficult to spot.