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: enum type (revisited) #28438

Open
srfrog opened this issue Oct 27, 2018 · 37 comments
Open

proposal: enum type (revisited) #28438

srfrog opened this issue Oct 27, 2018 · 37 comments
Labels
LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Milestone

Comments

@srfrog
Copy link

srfrog commented Oct 27, 2018

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.

@gopherbot gopherbot added this to the Proposal milestone Oct 27, 2018
@jimmyfrasche jimmyfrasche added LanguageChange v2 A language change or incompatible library change labels Oct 27, 2018
@ianlancetaylor
Copy link
Contributor

Would it make sense to use default instead of case unknown? Do you mean to imply that unknown is a pseudo-keyword?

@ianlancetaylor
Copy link
Contributor

I'm sorry, I do not understand the function bit. How is that an enum?

@srfrog
Copy link
Author

srfrog commented Oct 27, 2018

Would it make sense to use default instead of case unknown? Do you mean to imply that unknown is a pseudo-keyword?

Bad example, I guess. case unknown is actually one of the enum values. I have edited it for clarity.

@srfrog
Copy link
Author

srfrog commented Oct 27, 2018

I'm sorry, I do not understand the function bit. How is that an enum?

The enum func use func signatures to match values. In my proposed enum we match returned values of the funcs.

// all the enum func values must return (int, error).
enum HttpCmd (int, error) {
   case Download(url, file string):
      _, err := http.Get(url)
      if err != nil {
         // another enum value (from proposal example)
         return Status.failure, err
      }
      return Status.success, nil

   case Ping(url string):
      // recursive case
      return HttpCmd.Download(url, "")
}

// usage:
code, err := HttpCmd.Ping("https://www.example.org/ping")
if code == Status.success {
  code, err := HttpCmd.Download("https://www.example.org/file.jpg", "~/Downloads/file.jpg")
  if code == Status.failure || err != nil {
     // something to do
  }
}

@creker
Copy link

creker commented Oct 27, 2018

How would these enums deal with unknown values? Like, for example, if we use this for protobuf or thrift and receive from the remote side an unknown enum value. How could it be handled? If we look at the languages you mentioned, they all have ways to deal with that. C# allows you to convert any value to an enum as long as their underlying types match. Swift allows many different approaches one of which involves associated values that allow you to retain the unknown value and not downgrade it to some default "unknown".

And I'm also puzzled about functions. I can't think of a real problem that it solves. And, in general, it looks weird. Enum is, basically, a type. Here you declare enumeration HttpCmd that contains not only function declaration but also definition. Then you use it as some kind of namespace to call functions from. Can I declare a variable of type HttpCmd? If I can, what can I do with that variable?

It looks like you borrowed that from Swift but there functions act like simple methods allowing you to implement things like parsing or initialization. It still looks weird but kinda makes sense and allows you to neatly group enum with its helper methods that you usually write for enums but have to put in a different place. C# has something similar allowing you to write extension methods for enums.

@srfrog
Copy link
Author

srfrog commented Oct 27, 2018

This enum type is for grouping related values, so we want to have great flexibility how those values are generated and matched. I think this will help to reduce the number of loose global vars in code, and help with program design.

Something typical in Go code:

struct Person struct {
   Name string
   Department int
   Role Role // some struct
}
var defaultPerson Person
// ... code to manage and update Person values

Enum version:

// runtime or compile time configuration
enum Options string {
  case DefaultDept:
    return "Research"
  case Name(ID int):
    return "srfrog" // static value
  case Department(ID int):
    return getDeparmentName(db, ID)
  case DefaultRole:
    return "Student"
  case Role:
    rolech := make(chan string)
    go getRoleDistributed(42, rolech) // get from Dgraph cluster or something
    return <-rolech
}

@srfrog
Copy link
Author

srfrog commented Oct 28, 2018

How would these enums deal with unknown values? Like, for example, if we use this for protobuf or thrift and receive from the remote side an unknown enum value. How could it be handled? If we look at the languages you mentioned, they all have ways to deal with that. C# allows you to convert any value to an enum as long as their underlying types match. Swift allows many different approaches one of which involves associated values that allow you to retain the unknown value and not downgrade it to some default "unknown".

There are no unknown values, enum is a type and all cases must exist at compile time. Therefore, the compile would error.

And I'm also puzzled about functions. I can't think of a real problem that it solves. And, in general, it looks weird. Enum is, basically, a type. Here you declare enumeration HttpCmd that contains not only function declaration but also definition. Then you use it as some kind of namespace to call functions from. Can I declare a variable of type HttpCmd? If I can, what can I do with that variable?

The func values are matched by their signature, it sounds class-ish but it's not. They would basically work equivalent to:

// func vars now
f := func(s string) error { return nil }
// enum equivalent
enum myFuncs error {
  case Something(s string):
    return nil
}

The advantage here is that the enum has a cleaner form and has the inherit qualities of slices. e.g., a series of sequential steps can be defined in an enum and ran from a for-loop. The recursive enum aspect would make for some interesting states, such as those in parsers.

It looks like you borrowed that from Swift but there functions act like simple methods allowing you to implement things like parsing or initialization. It still looks weird but kinda makes sense and allows you to neatly group enum with its helper methods that you usually write for enums but have to put in a different place. C# has something similar allowing you to write extension methods for enums.

Yes I borrowed heavily from Swift and C#, I think Swift enums are good. But with Go we need something that works better with our type system. Also, I wanted recursion to work without keywords. Finally, the const value combined with funcs might make some people happy, since they can work like read-only or immutable values.

// boats and horses can only be named once
enum Things string {
   case setName(name string):
     if !findName(name) {
       db_insert_name(name)
     }
     return name
   case Boat(name string):
     return Things.setName(name)
   case Horse(name string):
     return Things.setName(name)
}
// assume i havent named my boat
boatName := Things.Boat("Golando") // value: Golando
boatName = Things.Boat("Golando") // value: Golando

@networkimprov
Copy link

networkimprov commented Oct 28, 2018

As this enum creates a namespace, did you consider struct as the namespace?

enum string { global: "nobody" }

type Person struct {
   name string
   status int
   enum int { follows: iota, leads: iota }
}

func (p *Person) x() { p.status = follows } // namespace implied
v := Person{status: follows, name: global}  // namespace implied
v.status = Person.leads

Since enum values are often associated with a struct member, this feels more ergonomic to me.

EDIT:
Maybe var (...) syntax is more appropriate here:

enum Name ( global = "nobody" ) // defines namespace & type; underlying type implicit

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

v := Person{ status: follows, name: global } // namespaces implied
v.status = Person.leads
v.name = Name.global

Hm, maybe this calls for a third enum proposal :-)

@deanveloper
Copy link

This does not feel
a. Like an enumeration of values
b. Like it's idiomatic to Go

This is extremely confusing, especially the case Create(user string). I also do not like the idea of a block of code executing without me typing parentheses, such as the following:

package main

enum Foo {
case Bar:
    fmt.Println("BarBarBar")
    return 0
}

func main() {
    fmt.Println(Foo.Bar)
}

There are three possibilities as to what would happen here:

  1. both BarBarBar and 0 print. This is bad, as it does not look like I have executed any code from the call site.
  2. only 0 prints. This is bad, as the case Bar states that it should print BarBarBar when it is used.
  3. it doesn't compile. This is bad, as it looks like everything has correct syntax, so it should logically compile.

I really don't see any good options.

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

As this enum creates a namespace, did you consider struct as the namespace?

True, a logical namespace is made. But it's not a struct. A struct is group of fields with different names and potentially different types.

This enum is a group of fields with different names but the same type, defined in the enum definition.

It behaves like a generic slice of type, []T, or a map[int]T. Except the matching is done with values of that slice that return T.

// slice version (Go1)
const (
  ThingBoat = iota
  ThingHorse
  ThingCar
)
var Things = []string{
  "Boat", "Horse", "Car",
}
var i int
switch i {
  case ThingBoat, ThingHorse, ThingCar:
    return Things[i]
}
// enum version (Go2 proposed)
// Please feel free to write this better than me in Go1 in less than 10 lines.
enum Things string {
  case Boat:
    return "Boat"
  case Horse:
    return "Horse"
  case Car:
    return "Car"
  case Childhood(age int):
    return getThingByAge(age) // must return string
}

Since enum values are often associated with a struct member, this feels more ergonomic to me.

I relate enums to values and struct to types.

Hm, maybe this calls for a third enum proposal :-)

Maybe, give it a shot.

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

This does not feel
a. Like an enumeration of values
b. Like it's idiomatic to Go

a) The enum behaves like an int-indexed array (Go slice), that fits the enumeration definition. You can handle them like slices; using index and iteration.
b) It's not idiomatic Go, yet. That's the goal. 🤞

This is extremely confusing, especially the case Create(user string). I also do not like the idea of a block of code executing without me typing parentheses, such as the following:

package main

enum Foo {
case Bar:
    fmt.Println("BarBarBar")
    return 0
}

func main() {
    fmt.Println(Foo.Bar)
}

There are three possibilities as to what would happen here:

  1. both BarBarBar and 0 print. This is bad, as it does not look like I have executed any code from the call site.
  2. only 0 prints. This is bad, as the case Bar states that it should print BarBarBar when it is used.
  3. it doesn't compile. This is bad, as it looks like everything has correct syntax, so it should logically compile.

1- Yes, it prints "BarBarBar" because it's a code block. And you print the returned 0 too. That's meant to work.
2- Works like explained in #1.
3- Your code looks correct, as viewed from this proposal, it should compile.

I know the func stuff might look a bit confusing if you haven't used enums in other languages. But the best way to remember enums is that they handle values; so it's not about the matching case.

@deanveloper
Copy link

deanveloper commented Oct 29, 2018

Please feel free to write this better than me in Go1 in less than 10 lines.

const Boat = "Boat"
const Horse = "Horse"
const Car = "Car"
var Things = []string{Boat, Horse, Car} // use a func here if you're worried about mutability
func Childhood(age int) string {
    return getThingByAge(age)
}

Did it in even fewer.

It behaves like a generic slice of type, []T, or a map[int]T. Except the matching is done with values of that slice that return T.

Actually, it doesn't. Thing.Childhood is of type func(int) string, not of type string. Thus Thing cannot be fully represented by a []string.

Yes, it prints "BarBarBar" because it's a code block. And you print the returned 0 too. That's meant to work.

So I'm executing foreign without doing a function call? That seems like it could lead to unreadable code and (unrelatedly) vulnerabilities that would be invisible to the caller, as they do not think they are executing any foreign code.

edit - I know the article is satire, but executing foreign code without explicitly calling a function is just plain bad

@networkimprov
Copy link

In C, enum defines a (possibly anonymous) type as well as symbolic constants. Does this not provide a type? I'd expect that in Go, with one of

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

I do not imagine Go will admit enum symbols which implicitly invoke a code block. If a symbol can be defined by expression, I'd expect the expression to be evaluated as for a struct definition.

type T enum { s: f() }  // run f() on package init
func x() {
   var v enum { s: f() } // run f() when x() invoked
}

There are some good ideas in the proposal, but I'd divide it into required features and open questions.

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

In C, enum defines a (possibly anonymous) type as well as symbolic constants. Does this not provide a type? I'd expect that in Go, with one of

Yes, constants are included.

I do not imagine Go will admit enum symbols which implicitly invoke a code block. If a symbol can be defined by expression, I'd expect the expression to be evaluated as for a struct definition.

Enums are not structs, therefore they behave differently. Enums are about values. So although we can have constant values, eval values are possible with code blocks. Also recursive values. We go a bit beyond C structs, check C# and Switch enums.

@deanveloper
Copy link

deanveloper commented Oct 29, 2018

Can you explain to me what the following would print?

package main

import "fmt"

enum Things string {
case Car:
    return "Car"
case Childhood(age int):
    return getThingByAge(age) // must return string
}

func main() {
    for _, val := range Things {
        // val should be of type string
        fmt.Println(Empty(val))
    }
}

func Empty(str string) bool {
    return str == ""
}

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

Please feel free to write this better than me in Go1 in less than 10 lines.

const Boat = "Boat"
const Horse = "Horse"
const Car = "Car"
var Things = []string{Boat, Horse, Car} // use a func here if you're worried about mutability
func Childhood(age int) string {
    return getThingByAge(age)
}

Did it in even fewer.

What happens if I try to access Things[5] ? Also, how do I access Childhood from the Thing slice ? What if I called it childhood ? Finally, what if I want to iterate over all these values?

All of these would be possible with enums.

Actually, it doesn't. Thing.Childhood is of type func(int) string, not of type string. Thus Thing cannot be fully represented by a []string.

That's not a func. It's an enum func field/case. It's matched by its signature. The type of the enum is string so all its values are string.

So I'm executing foreign without doing a function call? That seems like it could lead to unreadable code and (unrelatedly) vulnerabilities that would be invisible to the caller, as they do not think they are executing any foreign code.

It's a code block, it's evaluated yes. It's no different than using a switch statement. The Internet is still online.

@deanveloper
Copy link

deanveloper commented Oct 29, 2018

What happens if I try to access Things[5] ?

Fine, use [...]string instead of []string, then you have compile-time safety. Any issue you'd have with [...]string, you'd have with the enum type.

Also, how do I access Childhood from the Thing slice ?

That wouldn't make sense, as Childhood is not a string. It's a function, and should be as such.

Finally, what if I want to iterate over all these values?

With range on the slice, and iterating over a function when iterating through a string slice wouldn't make sense now, would it?

Also I made a comment at the same time as you, so in case you missed it, I think it's a very important thing to consider - #28438 (comment)

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

Can you explain to me what the following would print?

package main

import "fmt"

enum Things string {
case Car:
    return "Car"
case Childhood(age int):
    return getThingByAge(age) // must return string
}

func main() {
    for _, val := range Things {
        // val should be of type string
        fmt.Println(Empty(val))
    }
}

func Empty(str string) bool {
    return str == ""
}

The output would be:

"false"
"true"

The iterator will run through the slice returning values for each of the fields, but you're not sending age, which is needed to eval Things.Childhood, so you're stuck with the empty value of the enum, which is string, hence "". If you wanted to get the real value, you'd need to use the index or the accessor. Since it's not a func, it would not yield a build error.

Things[1].val(24)
Things.Childhood(24)

@deanveloper
Copy link

deanveloper commented Oct 29, 2018

So if Things.Childhood is a string, then how are you

  1. Executing it as a function
  2. Running Things[1].val(24)

Go's type system is supposed to be simple. If Things[1] is a string, then Things[1].val(24) should be invalid. If Things.Childhood is a string, then Things.Childhood(24) should be invalid.

It doesn't make sense, and it's not intuitive that the following two things evaluate differently:

Things.Childhood(24)
// and
ch := Things.Childhood
ch(24)

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

So if Things.Childhood is a string, then how are you

  1. Executing it as a function
  2. Running Things[1].val(24)

1- Not a func. A label.
2- Indexed, but to get the value you need to supply param.

Go's type system is supposed to be simple. If Things[1] is a string, then Things[1].val(24) should be invalid. If Things.Childhood is a string, then Things.Childhood(24) should be invalid.

It doesn't make sense, and it's not intuitive that the following two things evaluate differently:

Things.Childhood(24)
// and
ch := Things.Childhood
ch(24)

This wouldn't work, ch is already "" empty string before you ch(24).
How does iterating over a channel work? for .. range chan T {} similar mechanics.
Separate types from values; the goal of enum is to operate with values in a safe manner, not to replace slices or funcs types.

a) Instead of

var things = []string{"House", "Boat", "Car"}
if v, ok := things[3]; !ok {
  // some logic
} else {
  // use v
}
// Somewhere else in the code:
thing := things[23423]

// Out of bounds? Panic
// What if the slice changes somewhere else in the code?
// You wrote the original slice, but someone amended it and didn't test - Panic.

b) Enum version:

enum things string {
  case House:
    return "House"
  case Boat:
    return "Boat"
  case Car:
    return "Car"
}
if things.Horse != "" {
   setName(things.Horse)
   // etc..
}
// No OOBs or panics. Just values.

a) is error prone and has added complexity to managing and inspecting values. b) is simple and relatively safer.

@deanveloper
Copy link

This wouldn't work, ch is already "" empty string before you ch(24).

Yes, that's what I'm saying.

Things.Childhood(24)

Is not equivalent to

ch := Things.Childhood
ch(24)

Which is not intuitive and goes against the design patterns of Go.

If I ever see something(string), then I should be able to safely assume that something is a function that takes a string, and can use it, and pass it around as a variable of type func(string).

thing := things[23423]

If you use [...]string (an array, instead of a slice) then this would be a compile-time error.

How does iterating over a channel work? for .. range chan T {} similar mechanics.

I really don't see how the problems I have brought up relate to iterating over a channel.


Anyway, the main point I'm saying is that a lot of this stuff is not intuitive and goes against the design of Go.

External code (outside of initialization) should not run without explicitly calling a function.

This shares a lot of syntax with switch/case, but it is also different in several ways, such as the special "enum case func" which isn't quite a case, and not quite a func.

It has tons of implicit behavior and special rules (which again, goes against the design of Go).

Anyway, I feel like I've been a broken record here. I'm going to stop commenting since it seems like I'm just saying the same things over and over again.

@ianlancetaylor
Copy link
Contributor

I don't really understand this proposal, but I agree with @deanveloper that Things.Childhood(24) and ch := Things.Childhood; ch(24) must produce the same result. That is true everywhere else in Go and we aren't going to change it.

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

I don't really understand this proposal, but I agree with @deanveloper that Things.Childhood(24) and ch := Things.Childhood; ch(24) must produce the same result. That is true everywhere else in Go and we aren't going to change it.

You are trying to match an enum field, and Things.Childhood is not a field, but Things.Childhook(int) is. The field Things.Childhood(int) is a label not a func. The case block is not a func block.

You wouldn't expect this to work in Go1:

func Something(age int) string { return "24" }
fmt.Printf("%s", Something)
// or even
v := Something
if v == 24 { // func (int) string is not string } 

Enums would access the value:

enum This string {
  case Something(age int):
    return "24"
}
fmt.Println(This.Something) // output: "" (not matched)
fmt.Println(This.Something(123))  // output: "24"
v := This.Something // "", not matched
v = This.Something(444) // "24"
if v == "24" { // yes }

@ianlancetaylor
Copy link
Contributor

If Things.Childhood is not a field, then it shouldn't compile at all.

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

If Things.Childhood is not a field, then it shouldn't compile at all.

The type is enum Things string not Things.Childhood. That would be like saying a struct or func wouldnt compile because of the spelling of the field or parameters.

EDIT: There's an argument for this, and I can see this being useful. This is probably something that would need consensus. Would it be simpler to check them at compile time or simply return empty value for unmatched enum fields ? That's the question.

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

I feel this would be clearer with a formal proposal, but I'm going through the current process to see if there's interest. I've been looking at this for a while so it makes sense, but perhaps a detailed description will help you see it. I'm not trying to change existing functionality, just enhance Go with a value proxy, which I think it's the missing piece in Go.

Benefits:

  • Value safety, minimize panics and repetitive code to validate values.
  • Global stack reduction, complex global values can be safely accessed from enums.
  • Cleaner logic abstraction, via enum namespace and fields.
  • Simplified boilerplate code for various configurations.
  • Recursion and iterators for sequential and state-based operations.

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

@creker asked something similar about unknown values but I though he meant invalid enums:

How would these enums deal with unknown values? Like, for example, if we use this for protobuf or thrift and receive from the remote side an unknown enum value. How could it be handled? If we look at the languages you mentioned, they all have ways to deal with that. C# allows you to convert any value to an enum as long as their underlying types match. Swift allows many different approaches one of which involves associated values that allow you to retain the unknown value and not downgrade it to some default "unknown".

The enum definition is its type. eg., enum Things string. We either match one of labels/fields in the enum or not, I'm suggesting returning empty value (e.g., string) when unmatched label. But @ianlancetaylor suggests the compiler catch this an error out. If we wanted to guarantee value-safety, then catching this when compiling would be the right thing. Just like you meant to have a const value and you misspelled the name or it doesn't exist.

My original thought was to handle unknown/unmatched labels at runtime like you would with a switch-func with a default return:

func getIndexValue(idx int) string {
  switch idx {
    case 0:
      return "zero"
    case 1:
      return "one"
  } 
  return ""
}
// equivalent to this:
enum Value string {
  case Zero:
    return "zero"
  case One:
    return "one"
}

@deanveloper
Copy link

I've got something new about the proposal so I'll say it quick -

ALL additions to enums under this proposal are breaking changes.

We start off with

package us

enum Foo string {
case A:
case B:
case C:
}

Then, someone else imports our library, and does the following:

package them

enum Bar string {
us.Foo
case Z:
case Y:
case X:
}

Y can be referenced by them.Bar[4]

Now, we decide to add some more functionality to our library.

package us
enum Foo string {
case A:
case B:
case C:
case D:
case E:
case F:
}

Now, them.Bar[4] has suddenly changed from "Y" to "E". That is a huge breaking change, and can cause extremely sneaky bugs.

If there are any more things that I think need more thought, I'll let you know so in your formal proposal you can make sure that they are considered.

But Go is very expression-based. If Things.Childhood(24) is valid, then so should ch := Things.Childhood; ch(24)

@srfrog
Copy link
Author

srfrog commented Oct 29, 2018

I've got something new about the proposal so I'll say it quick -
ALL additions to enums under this proposal are breaking changes.

You got a point. But honestly, if you are accessing enums that way then you get what you deserve :)
I don't think value safety has to do with "getting the same value every time" but rather a small guarantee that if you fetch a labelled enum value that it will be what you expect.

Think about maps, you aren't guaranteed that will be iterated in the order you put them in. But you are guaranteed to get a value if the key matches, even if it's empty.

ch := Things.Childhood is different, because it's not matching a label. And this goes in hand with @ianlancetaylor remark of catching unmatched labels early.

@apkrieg
Copy link

apkrieg commented Nov 11, 2018

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))
}

does not follow the standard

type <Identifier> <Type>

syntax

@srfrog
Copy link
Author

srfrog commented Nov 12, 2018

Perhaps:

type Status enum(int)

similar to:

type Action func(int)

@apkrieg
Copy link

apkrieg commented Nov 13, 2018

I was actually considering that. Another thing I was looking at:

type Weekday int {
    Monday
    Tuesday
    ...
}

I really like how you can pass values back with an enum const in Rust though, and I was trying to craft a solution that would allow that.

@srfrog
Copy link
Author

srfrog commented Nov 13, 2018

@apkrieg that is what I like about enums with parametrized labels. that allows us to pass values to evaluate.

@apkrieg
Copy link

apkrieg commented Nov 13, 2018

We could keep the iota thing:

type Weekday enum {
    Monday = iota, // 0
    Tuesday,       // 1
    ...
}

And for parameterized labels:

type Result enum {
    Success,
    Error {
        err string,
    },
}

Just shooting ideas at the wall

@ianlancetaylor ianlancetaylor added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Dec 4, 2018
@mateuszmmeteo
Copy link

What logical case requires ENUM to include functions noted like on the top?
Why not to use standard structs?
Do ENUM allow to use interface{}? If yes why don't you want to pass regular func body?

What I can think about is only to make enums less readable - it is a messy in top example.

@changkun
Copy link
Member

type customType int

const (
	customTypeA customType = iota
	customTypeB
	customTypeC
)

func switchfunc(tt customType) bool {

	switch tt {
	case customTypeA:
		return true
	case customTypeB:
		return false
	case customTypeC:
		return true
	}

	return false // this is unnecessary since the switch has through all possibilities.
}

@deanveloper
Copy link

@changkun There are a few issues with that.

  1. switchfunc(5) is a valid thing to be passed into that function, which when being passed in you simply return false rather than panicking or returning an error
  2. No compiler safety: it would be great if the compiler could prevent switchfunc(5) from being called
  3. The constants are not namespaced to their type, making it annoying for people with autocomplete who use your package since there may be several constants (ie in net/http)
  4. There is no way to range over enums

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

10 participants