-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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: spec: enum type (revisited) #28438
Comments
Would it make sense to use |
I'm sorry, I do not understand the function bit. How is that an enum? |
Bad example, I guess. |
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
}
} |
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 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. |
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
} |
There are no unknown values, enum is a type and all cases must exist at compile time. Therefore, the compile would error.
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.
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 |
As this enum creates a namespace, did you consider
Since enum values are often associated with a struct member, this feels more ergonomic to me. EDIT:
Hm, maybe this calls for a third enum proposal :-) |
This does not feel This is extremely confusing, especially the
There are three possibilities as to what would happen here:
I really don't see any good options. |
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
}
I relate enums to values and struct to types.
Maybe, give it a shot. |
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.
1- Yes, it prints "BarBarBar" because it's a code block. And you print the returned 0 too. That's meant to work. 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. |
Did it in even fewer.
Actually, it doesn't.
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 |
In C,
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.
There are some good ideas in the proposal, but I'd divide it into required features and open questions. |
Yes, constants are included.
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. |
Can you explain to me what the following would print?
|
What happens if I try to access All of these would be possible with enums.
That's not a func. It's an enum func field/case. It's matched by its signature. The type of the enum is
It's a code block, it's evaluated yes. It's no different than using a |
Fine, use
That wouldn't make sense, as
With 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) |
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 Things[1].val(24)
Things.Childhood(24) |
So if
Go's type system is supposed to be simple. If It doesn't make sense, and it's not intuitive that the following two things evaluate differently:
|
1- Not a func. A label.
This wouldn't work, 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. |
Yes, that's what I'm saying.
Is not equivalent to
Which is not intuitive and goes against the design patterns of Go. If I ever see
If you use [...]string (an array, instead of a slice) then this would be a compile-time error.
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 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. |
I don't really understand this proposal, but I agree with @deanveloper that |
You are trying to match an enum field, and 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 } |
If |
The type is 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. |
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:
|
@creker asked something similar about unknown values but I though he meant invalid enums:
The enum definition is its type. eg., 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"
} |
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
Then, someone else imports our library, and does the following:
Now, we decide to add some more functionality to our library.
Now, 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 |
You got a point. But honestly, if you are accessing enums that way then you get what you deserve :) 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.
|
does not follow the standard
syntax |
Perhaps:
similar to:
|
I was actually considering that. Another thing I was looking at:
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. |
@apkrieg that is what I like about enums with parametrized labels. that allows us to pass values to evaluate. |
We could keep the
And for parameterized labels:
Just shooting ideas at the wall |
What logical case requires ENUM to include functions noted like on the top? What I can think about is only to make enums less readable - it is a messy in top example. |
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.
} |
@changkun There are a few issues with that.
|
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)
Syntax: Not assignable
Syntax: Specific value type
Syntax: Embedding - all enum types must match
Syntax: Functions - case matches func signature, and the funcs must return values matching the enum type.
Syntax: For-loop - similar to a slice, the index match the field order.
Syntax: Switches
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.The text was updated successfully, but these errors were encountered: