About Enums

245 views
Skip to first unread message

Shreyas Sreenivas

unread,
Nov 12, 2021, 12:20:36 PM11/12/21
to golang-nuts
Note, I've already looked at https://golang.org/issue/28987, https://golang.org/issue/19814, and https://golang.org/issue/28438. All of them require them to make a change that breaks the compatibility promise. I want to explore another way of defining enums that is backward compatible.

Enums as types with constrained values

Say we have an enum for mathematical operations, we can define it currently with iota like so:

```go
type Operation int
const (
  Uknown Operation = iota // zero value
  Sum
  Difference
  Product
  Difference
)
```

There are a few problems here:
1. `Operation(100)` is valid even though it shouldn't be. Sure, we can use static analysis tools but it'd be a lot better if the compiler checked this for us.
2. Although I've never seen anyone do it, you can redefine the same values in a different place with different names.
3. Zero values can become complicated easily.
4. Very often we want enums with string representations. We can define a `.String()` method that uses a map, but it could be considered boilerplate. For example, we would need to define the below method for the `Operation` type

```go
func (o Operation) String() string {
  symbols := map[Operation]string{
    Sum:  "+" ,
    Product: "*",
    Difference: "-",
    Quotient: "/",
  }

  return symbols[o] // assuming we know `o` is always a valid value
}
```

I think a much better solution would be the below syntax

```go
type Operation string (
  Sum = "+"
  Product = "*"
  Difference = "-"
  Quotient = "%"
)
```

We can think of Operation as a type with an underlying type of string with "constrained values", i.e. any variable with a type of `Operation` can only have one of these predefined values. We can define constrained values similarly for any type that is comparable, but not pointers.

Usage

```go
// using one of the values
var operator = Operator.Sum

// converting a string constant to the enum
var operator2 = Operator("+") // if the value we're trying to convert is not a variable or doesn't have a component that's a variable, the validity could be checked by the compiler

// conversion of a dynamic value
var str = "foo"
var operator3 = Operator(str) // panics if invalid value?
```

We get compiler time type checking, only certain values are allowed at both compile-time and runtime (which is the point of an enum).

Downsides:
1. This could be considered syntactic sugar for `iota`, and `.String()`
2. The syntax looks out of place? This could be discussed further
3. This could potentially add complexity but IMO it's very little

Pros:
1. Better type safety 
2. Fixes the problems with the current approach to enums mentioned above
3. Doesn't violate the compatibility guidelines
4. No complex enums like the ones you'd find in other languages like Rust or Swift (specially swift)

Some more examples

```go
// the default underlying values would be A=0, B=1, and so on...
type Foo int (
  A
  B
  C
  D = 10 // custom value
)

// the underlying default values are "A", "B", and so on...
type Bar string (
  A
  B
  C
  D = "custom_value"
)

type FooS struct {
  Foo string
  Bar string
}

// no default values in this case, have to provide a value
type Foo2 FooS (
  A = FooS{"a", "b"}
  B = FooS{"c", "d"}
)
```

Zero Values

There are a few ways we handle zero values.

1. We enforce declaring a zero value
```go
type Foo struct {
  foo string
}

type Bar Foo (
  A = Foo{} // enforce this
  B = Foo{"another"}
)

type Bar2 int {
  A // zero value like the first value of iota
  B
}

type Bar3 string {
  A = "" // enforce this
  B
}
```

2. Create an `<type-name>.Zero` by default
An option, but it doesn't feel right.

3. No special naming or requirement for zero values
```go
// we can just have
type Foo string (
  A
  B
)

var foo = Foo("") // this represents a zero value and is allowed even though we don't have a value that contains a zero value
```

We shouldn't allow `nil` considering the underlying types cannot be `nil`.











Ian Lance Taylor

unread,
Nov 12, 2021, 4:55:58 PM11/12/21
to Shreyas Sreenivas, golang-nuts
On Fri, Nov 12, 2021 at 9:20 AM Shreyas Sreenivas
<shreyas.s...@gmail.com> wrote:
>
> Note, I've already looked at https://golang.org/issue/28987, https://golang.org/issue/19814, and https://golang.org/issue/28438. All of them require them to make a change that breaks the compatibility promise. I want to explore another way of defining enums that is backward compatible.
>
> Enums as types with constrained values

...

> ```go
> type Operation string (
> Sum = "+"
> Product = "*"
> Difference = "-"
> Quotient = "%"
> )
> ```

The problem I see is that people want different things from
enumeration types, and what they want in Go tends to depend on what is
provided by other languages with which they are familiar. For
example, in C there is no notion of a string associated with an enum.
But on the other hand there is a common expectation that it is
possible to write a loop over all enumeration values, which is easy in
C but is not clearly supported by your suggested syntax.

I think it's necessary to first discuss the desired set of features.
Until that is done, syntax can be a distraction.

Ian

Shreyas Sreenivas

unread,
Nov 19, 2021, 9:34:39 AM11/19/21
to golang-nuts
> But on the other hand there is a common expectation that it is
> possible to write a loop over all enumeration values, which is easy in
> C but is not clearly supported by your suggested syntax

Yeah, I had this in mind but forgot to add it. I thought we could just use `range` like below:
```
type Foo string (
  A
  B
  C
)

for val := range Foo {} 
// or
for i, val := range Foo {}
```


> The problem I see is that people want different things from
> enumeration types, and what they want in Go tends to depend on what is
> provided by other languages with which they are familiar.
> I think it's necessary to first discuss the desired set of features.
> Until that is done, syntax can be a distraction.

Yup, agreed. I suggested a syntax that would keep enums simple without doing too much like some other languages. But again, many people would like those features and there are valid use cases for them. Thanks for reviewing it nonetheless!

Reply all
Reply to author
Forward
0 new messages