golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
120.13k stars 17.24k forks source link

proposal: Go 2: enums as an extension to types #28987

Open deanveloper opened 5 years ago

deanveloper commented 5 years ago

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:

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.

deanveloper commented 5 years ago

Small typo (I'm assuming), but you'd want to return time.Weekday enum, not just time.Weekday. Otherwise you wouldn't be able to assign the result to a time.Weekday enum.

And this statement may be controversial, but I'd much rather have a feature that feels good to use, than one that is easy to convert legacy code to but seems half-baked.

And of course another issue is that I'd rather not need to write enum after every time I want to use one. It's the reason that typedef came about in C, because it's clumsy to write struct, enum, union, etc. every time I wanted to use one of those types. I'd much rather just use the type's name. A type is already a set of what values a variable can hold, so ideally if we're limiting what values a variable can hold, we should do that at the type level.

networkimprov commented 5 years ago

Typo, yes. I was thinking it might not be nec, but it is.

Requiring type_name enum declaration is a feature; they're not like other types. And it permits unconstrained instances of the type where you need user-defined alternatives with v := type_name(x)

You're right, enum is nec for type defs, not just declarations. Then enum (subset) isn't a burden.

type Workday time.Weekday enum (time.Monday..time.Friday) // segment of a var (...) list

var A Workday enum = time.Monday
var B Workday enum = time.Sunday // compile error
deanveloper commented 5 years ago

Since Workday is based on time.Weekday enum (...) ideally we should just use Workday rather than Workday enum.

Also, since Workday and Weekday are separate types, they aren't assignable, unfortunately. I wouldn't like to change that. A typealias would make sense in this case though, since workdays are literally weekdays (rather than just being based on them).

So then perhaps the "make my pre-existing type an enum" code would become

type Weekday int
const (
    Sunday Weekday = iota
    // ...
)

type WeekdayEnum = Weekday enum

And then you could pass around a WeekdayEnum as you would a Weekday, with enum safety.

I'll add this to the points of discussion once I'm at my computer; I'm commenting from mobile right now.

networkimprov commented 5 years ago

Right, Workday needs the type identity of Weekday, so would use alias syntax as you suggest type Workday = time.Weekday enum (time.Monday...)

These then work var w Workday = time.Friday var d time.Weekday enum = w var z = time.Weekday(8)

networkimprov commented 5 years ago

To summarize...

Requirements: a) integrate with and enhance existing code b) enable value lists using all/some existing values c) enable values of any type, and constant literals d) don't require const values (since const only applies to certain types at present) e) enable definition of enumeration types f) enable embedded value definitions

// existing code
type T struct {...}
var ( Allow1 = T{}; ... AllowN = T{} )

// enhance existing code
var a T enum                        // admit any var T in scope
var b T enum ( Allow1, Allow2 )     // admit a subset
var c T enum ( Allow1..Allow9 )     // admit a range from a var/const (...) list
var i int enum (1, 2)               // admit constants

// define enum type
type T1 = T enum                    // alias syntax
type T2 = T enum (Allow1, Allow2)

var d T2 = Allow1
var e T1 = d                        // admit-any type admits subset's values

var v = 2
type S struct {
   a int enum (1, v)                // compile error? require const for primitive types
}
s := S{a: 2}                        // or compile error? v is var and 2 is const

// embedded definition ideas

var f T enum ( Permit = T{} )       // defines var in 'T enum' scope?
var g T enum = Permit 
var v T = Permit                    // compile error; not in scope

type S struct {
   a T enum ( Permit = T{} )        // type def creates namespace for new values?
}
s := S{a: Permit}                   // namespace implicit?
s.a = S.Permit
deanveloper commented 5 years ago

I have no clue where these requirements are coming from. Also I'd personally much rather require constant types, and wait for const to be allowed on all types, than to get this proposal accepted too early, thereby requiring we comply to unwanted behavior.

I'd also like it to mostly remain unchanged, I feel like a lot of the proposed changes you have been making are more apt to a new proposal rather than an amendment to this one.

In conclusion, I'd really like this proposal to remain mostly unchanged. Maybe some syntax here and there, but the overall concept should remain untouched. This proposal values making a good implementation over sacrificing quality to make it "fit better" into old code.

viper10652 commented 5 years ago

Don't forget enumeration subtypes. e.g. you want to declare an enumeration type WEEKDAYS and a subtype WORKDAYS of type WEEKDAYS.

So the value "Monday" evaluates to True when testing to be of type WEEKDAYS and WORKDAYS.

If you have atype WEEKDAYS with values (Mon, Tue, Wed, Thu, Fri, Sat, Sun) You want to be able to define your subtype as: 1) an list of values, e.g. (Mon, Tue, Wed, Thu, Fri) 2) a range of values, e.g. (Mon ... Fri)

additional constraints: 1) a subtype can only have values from the main type, i.e. no additional values

2) the numeric value of a subtype needs to be identical as its corresponding value in the main type, e.g. if the numeric value for enumeration value Monday is 2 in WEEKDAYS, then it needs to have the numeric value in WORKDAYS as well.

You need the capability to convert between unconstrained types (e.g. int) and an enumeration. This is useful when decoding bytestreams to and from data types. E.g. decoding an array of bytes received over a serial line into a structure of fields. Often these fields can be enumeration types in communication protocols.

You also want to be able to reference the enumeration values as identifiers and test for inclusion in related types e.g. type WEEKDAYS enum (Mon, Tue, Wed, Thu, Fri, Sat, Sun) type WORKDAYS enum (Mon ... Fri) type WEEKEND enum (Sat, Sun) var wday WEEKDAYS wday := WEEKDAYS .Mon if wday in WORKDAYS {} else if wday in WEEKEND {

deanveloper commented 5 years ago

I don't know if I like using the ... syntax since it's already used for variadic arguments.

When you say "don't forget enumeration subtypes", we already have them.

type Weekday enum int (Sun = iota; Mon; Tues; Wed; Thurs; Fri; Sat)

type Workday enum Weekday (Mon = Weekday.Mon + iota; Tues; Wed; Thurs; Fri)

type Weekend enum Weekday (Sun = Weekday.Sun; Sat = Weekday.Sat)

I don't want to complicate enums and make their learning curve steeper by adding additional features that are (the way I see it) not absolutely needed.

viper10652 commented 5 years ago

I don't want to complicate enums and make their learning curve steeper by adding additional features that are (the way I see it) not absolutely needed.

That is a subjective statement. You can state that YOU don't see a need for it, which is fine, but making a global statement for the whole golang community is a bold statement at best.

I suggested the feature because I got used to it when I was programming in Ada, and I saw a definite need for it back then (and a lot of Ada programmers agreed with me), but that is just my opinion (and that of a lot of Ada programmers). Please don't discard suggestions from people that were able to use the feature in other languages. If you don't find it useful, then don't use it, but at least allow others who do find it useful to use it. Remember: "In the eyes of a hammer, a screw doesn't seem to be very useful"

deanveloper commented 5 years ago

The idea of "sub enums" also seems to be a more object-oriented concept which involves inheritance, which makes sense with Ada being object oriented. I'd definitely like to stay away from that. This integrates well with Go's current (and simple) type system, so I'm very wary of adding too many features to it. Go is great (in my eyes, of course) because it's picky about what gets added into the language.

I definitely didn't mean to discard it entirely, I'm just stating that it doesn't really match with the values of this proposal. I know I kinda come off as harsh sometimes, I promise I'm not trying to do that

beoran commented 5 years ago

A Go1 backwards compatible way to do this, and to unify this with #29649 would be to reuse the range keyword in type definitions, and use it like range(type allowed values and ranges), much like we now have chan(message type) . Furthermore the simplest thing that could work do would be to limit the allowed values to constant expressions and constant ranges. You would have to define the values of the range then outside of the range type declaration itself, but that solves a few sticky issues on name spacing as well. Something like this:

type WeekdayValue int
const ( 
 Monday WeekdayValue = iota
 Tuesday
 Wednesday
 Thursday
 Friday
 Saturday
 Sunday
)

type WeekDay range(WeekdayValue Monday...Sunday) 
type AlternativeWeekDay range(WeekdayValue Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday) 
type WorkDay range(WeekdayValue Monday...Friday)
// String constants also allowed.
type EnglishDayNames range(string "monday", "tuesday", "wednesday", "friday", "saturday", "sunday")

// Allowed
var wd WeekDay = Monday
// Also Allowed
var wd WeekDay = WeekDayValue(1)
// Compiler error
var wd WeekDay = WeekDayValue(20)

// Example of iteration and indexation
func ExampleIterateWeekDay() {
  wd := range WeekDay {
    frt.Printf("%d:%s\n", int(wd), EnglishDayNames[int(wd)])
  }
}

If I see enough people like this approach I will write out a full design document for this idea.

deanveloper commented 5 years ago

The reason I am opposed to defining the underlying type and enum is illustrated in an earlier comment, which is that now we have a "useless" type (WeekdayValue in your case) that shouldn't be used anywhere other than to define the "useful" type (Weekday). I think that needless types should be avoided (aka we should define the enum type and it's values together) which is a large part of the reason that I made this proposal.

That approach also would not work for enums of structs which was a large part of this proposal. Instead you would need to have an enum of indices which reference a slice. This isn't a bad solution, and has been discussed previously, but this proposal just doesn't really "get along" with that one very well.

It looks like your proposal would actually be the exact same as #29649 though, it's doesn't appear any different (at least immediately) besides that you can specify a comma separated list of numbers, which I personally think defeats the purpose of a range type. Please correct me if I am wrong though.

Not trying to rip on this idea or your prospective proposal though! A number range type would be another good solution to an enum proposal that feels Go-like.

networkimprov commented 5 years ago

I already wrote that design document here https://github.com/golang/go/issues/28987#issuecomment-449195971 :-)

beoran commented 5 years ago

Well, my idea is to unify ranged types with enumerations, in a way that is backwards compatible with Go1. I don't mind not getting enums of structs, enums of ConstantExpressions with matching underlying types would be very useful. Maybe later we will get some structs as ConstantExpressions as well.

I see your point about wanting to declare the values of the enums as well, as in a struct's members. It would not be too hard to extend my idea for that, I think.

I started to write out my ideas at here https://gist.github.com/beoran/83526ce0c1ff2971a9119d103822533a, still incomplete, but maybe worth a read.

caibirdme commented 5 years ago

When I find a function:

func DoSomething(t UserType)

I don't know which UserTypes can I choose, IDE can't do the intelligent completion. The only way to do that is jumping to the definition of UserType and see if there're some related const definition like:

type UserType byte
const (
  Teacher UserType = iota
  Driver
  Actor
  Musician
  Doctor
  // ...
)

But if the author doesn't define those constants near UserType, it's hard to find them.

Another thing is that, UserType is just an alias name for byte(not strictly), people can convert any byte value to UserType like UserType(100). So the consumer of UserType have to check its validation on runtime, like:

func DoSomething(t UserType) {
  switch t {
    case   Teacher,Driver,Actor,Musician,Doctor:
    default:
      panic("invalid usertype")
  }
}

This actually can be done on compile time.

griesemer commented 4 years ago

I like to go back to the original list of properties of an enum as enumerated (hah!) at the start of this proposal:

I see these as three distinct, in fact orthogonal properties. If we want to make progress on enums in a way that reflects the spirit of Go, we cannot lump all these qualities into a single unified new language feature. In Go we like to provide the elementary, underlying, language features from which more complicated features can be built. For instance, there are no classes, but there are the building blocks to get the effect of classes (data types, methods on any data types, interfaces, etc.). Providing the building blocks simultaneously makes the language simpler and more powerful.

Thus, coming from this viewpoint, all the proposals on enums I've seen so far, including this one, mix way too many things together in my mind. Think of the spec entry for this proposal, for instance. It's going to be quite long and complicated, for a relatively minor feature in the grand scheme of things (minor because we've been programming successfully w/o enums for almost 10 years in Go). Compare that to the spec entry for say an slice, which is rather short and simple, yet the slice type adds enormous power to the language.

Instead, I suggest that we try to address these (the enum) properties individually. If we had a mechanism in the language for immutable values (a big "if"), and a mechanism to concisely define new values (more on that below), than an "enum" is simply a mechanism to lump together a list of values of a given type such that the compiler can do compile-time validation.

Given that immutability is non-trivial (we have a bunch of proposals trying to address that), and given the fact that we could get pretty far even w/o immutability, I suggest ignoring this for a moment.

If we have complex enum values, such as Material{Name: "Metal", Strength: values(Hardness).Hard } we already have mechanisms in the language to create them: composite literals, or functions in the most general case. Creating all these values is a matter of declaring and initializing variables.

If we have simple enum values, such as weekdays that are numbered from 0 to 6, we have iota in constant declarations.

A third, and I believe also common enum value is one that is not trivially a constant, but that can be easily computed from an increasing index (such as iota). For instance, one might declare enum variables that cannot be of a constant type for some reason:

var (
   pascal Lang = newLang(0)
   c Lang = newLang(1)
   java Lang = newLang(2)
   python Lang = newLang(3)
   ...
)

I believe this case is trivially addressed with existing proposal #21473 which proposes to permit iota in variable declarations. With that we could write this as:

var (
   pascal Lang = newLang(iota)
   c
   java
   python
   ...
)

which is a very nice and extremely powerful way to define a list of non-constant values which might be used in an enumeration.

That is, we basically already have all the machinery we need in Go to declare individual enum values in a straight-forward way.

All that is left (and ignoring immutability) is a way to tell the compiler which set of values makes up the actual enum set. An obvious choice would be to have a new type enum which lists the respective values. For instance:

type ProgLang enum{pascal, c, java, python} // maybe we need to mention the element type

That is, the enum simply defines the permissible set of values. The syntactic disadvantage of such an approach is that one will have to repeat the values after having declared them. The advantage is the simplicity, readability, and that there are no questions regarding the scoping of enum names (do I need to say ProgLang.pascal, or can I just say pascal for instance).

I am not suggesting this as the solution to the enum problem, this needs clearly more thought. But I do believe that separating the concerns of an enum into distinct orthogonal concepts will lead to a much more powerful solution. It also allows us to think about how each of the individual properties would be implemented, separately from the other properties.

I'm hoping that this comment inspires different ways of looking at enums than what we've seen so far.

networkimprov commented 4 years ago

@griesemer this comment contains the core of another enum proposal, any thoughts? https://github.com/golang/go/issues/28987#issuecomment-449195971

The discussion prior to that comment provides more details...

griesemer commented 4 years ago

@networkimprov I deliberately did not start yet another enum proposal because what I wrote above also needs much more thought. I give credit to this (@deanveloper 's) proposal for identifying the three properties of an enum, I think that's a useful observation. What I don't like with this current proposal is that it mixes the declaration of a type with the initialization of values of that type. Sometimes, the type declaration appears to define two types (as with his Material example which declares the Material enum type, but then that type also is used as the element type for the composite literals that make up the enum values). There's clearly a lot going on. If we can disentangle the various concerns we might be better off. My post was an attempt at showing that there are ways to disentangle things.

Also, one thing that I rarely see addressed (or perhaps I missed it) is how an implementation would ensure that a variable of enum type can only hold values of that enum type. If the enum is simply a range of integers, it's easy. But what if we have complex values? (For instance, in an assignment of an enum value to an enum-typed variable, we certainly don't want a compiler have to check each value against a set of approved values.) A way to do this, and I believe @ianlancetaylor hinted at that, is to always map enum values to a consecutive range of integer values (starting at 0), and use that integer value as an index into a fixed size array which contains the actual enum values (and of course, in the trivial case where the index is the enum value, we don't need the array). In other words, referring to an enum value, say e, gets translated into enum_array[e], where enum_array is the (hidden) storage for all the actual enum values of the respective enum type. Such an implementation would also automatically imply that the zero value for an enum is the zero element of the array, which would be the first enum value. Iteration over the enum values also becomes obvious: it's simply iteration over the respective enum array elements. Thus, with this an enum type is simply syntactic sugar for something that we currently would program out by hand.

Anyway, all these are just some more thoughts on the subject. It's good to keep thinking about this, the more perspectives we have the better. It might well be that at some point we arrive at an idea that feels right for Go.

networkimprov commented 4 years ago

an enum type is simply syntactic sugar for something that we currently would program out by hand.

@griesemer, else, for, return, break, continue, etc are syntactic sugar for goto ;-)

So you don't believe an enum type is worth any effort? We should just use iota-defined values as indices, and let apps panic at runtime on index out-of-bounds instead of failing to compile?

If that's not your gist, I think you missed my question: whether the contents of this comment (and discussion preceding it) would dodge your critique of this proposal: https://github.com/golang/go/issues/28987#issuecomment-449195971

jimmyfrasche commented 4 years ago

@griesemer

All that is left (and ignoring immutability) is a way to tell the compiler which set of values makes up the actual enum set. An obvious choice would be to have a new type enum which lists the respective values. For instance:

type ProgLang enum{pascal, c, java, python}

I realize this is just an off the cuff example, but I don't think you can ignore immutability there. If you define a type by a list of mutable values, which can contain pointers, what does it mean for one of those values to then be mutated? Does the type's definition change at runtime? Is a deep copy of the value made at compile type and put in enum_array? What about pointer methods?

type t struct { n *int }
a := t{new(int)}
var e enum { a }
*a.n += 1
fmt.Println(*e.n) // Is this one or zero?
e = a // Is this legal or not?

You could forbid pointers in the base type (for lack of a better term) of an enum declaration, but that makes it equivalent to allowing arrays and structs to be constants when they consist only of types that can be constant https://github.com/golang/go/issues/6386#issuecomment-406824755, except that without the immutability you could still easily do confusing things:

foo = bar{2}
var e enum{foo} = foo
foo = bar{3}
e = foo // illegal?

And, even if you didn't forbid pointers, you'd probably have to forbid types that cannot be compared since you'd need to compare the values to see where and if they map into enum_array at runtime.

Mixing types and values like that also allows some strange things like

func f(a, b, c int) interface{} {
  return enum{a, b, c}
}

I'm not sure it could made to work, but, if it could, it would be the kind of thing where the instructions for using it are all footnotes and caveats.

I'm all for providing orthogonal primitives, but I don't think what most language's define as enums can be split apart cleanly. Much of them are inherently and purposefully, "let's do a bunch of stuff all at once so that we don't always have to do the same stuff together".

griesemer commented 4 years ago

@jimmyfrasche Fair point - defining an enum as a simple collection of values is probably not good enough if we can't guarantee that the values are immutable. Perhaps immutability needs to be a requirement for values used in enums. Still, I believe immutability is a separate, orthogonal concept. Or put another way, if enums somehow magically could be used to enforce immutability of the enum values, people would use enums to get immutability. I haven't seen such enums yet. And if enum elements are suitably restricted such that immutability is easy (e.g. they must be constants), then we also have enum instructions with "all footnotes and caveats".

Regarding the splitting apart of enum features: In older languages, enums simply provided an easy way to declare a sequence of named constants, together with a type. There was little enforcement (if any) that a value of enum type could not contain other (non-enum) values. We have split out these pieces in Go by providing type declarations and iota, which together can be used to emulate such basic enums. I think that worked pretty well so far and is about as safe (or unsafe) as historic enums.

@networkimprov I'm not saying we shouldn't have enums, but the demand in actual programs needs to be pretty high for them (for instance, we have switch statements because they are pretty common, even though we also have the more basic if that could be used instead). Alternatively, enums are so simple a mechanism that they are perhaps justifiable because of that . But all the proposals I've seen so far seem pretty complex. And few (if any) have drilled down to how one would implement them (another important aspect).

aprice2704 commented 4 years ago

I would like to use enums for iterating over fairly short lists of things known at compile time, my conception of them having come from Pascal. Unlike @deanveloper's list of reqs, I personally would prefer only the deliberately inflexible:

  1. An enum type defines a sequence of constants that may be cast to ints (specifically ints) and behave like them in many places.
  2. The constants are defined at compile time (natch) and may be iota, or a constant that can be an int.
    enum Fruit ( 
    Apple = iota
    Cherry
    Banana
    )
    enum WebErrors (
    MissingPage = 404
    OK = 200
    )

    Thus the compiler knows all the possible values at compile time and may order them.

    var f Fruit     ... initialized to Apple
    var e WebError   ... initialized to OK
    g := Cherry
    f = Fruit(3)   ... compiler error
    f = Apple + 1 ... compiler error
    f = Apple + Banana ... compiler error
    f = Fruit(0)   ... Apple
    myerr = WebError(goterror)  ... run time bounds checking invoked --> possible panic
    int(Cherry)    ... gives 1
    range Fruit   ... ranges *in order*
    g.String()   ... gives "Cherry"
    Fruit.First()  ... gives Apple
    Fruit.Last() ... gives Banana
    Banana.Pred() ... gives Cherry as does Banana--
    Cherry.Succ() ... gives Banana as does Cherry++ or g++
    j := Banana++  ... compiler error on bounds
    crop := make(map[Fruit]int)
    crop[Cherry] = 100
    for fr := range Fruit { fmt.printf("Fruit: %s, amount: %d\n", fr, crop[fr]) }  <--- nicely ordered range over map 

    A compact 'set' would also be nice, functionally almost identical to a map[enum]bool; again, I want to emphasize that I would be totally happy (in fact prefer) that the possible enum values must be known at compile time and are really part of the code, not true data items (n.b. http errors are probably a better example than fruit in practice). If they are unknown at compile time, and are thus data items, then one expects to write more code (to order, check bounds etc.), but this is annoying in very simple cases where the compiler could just deal with it easily.

    type Fruits set[Fruit]
    var weFarm Fruits
    weFarm[Banana] = true
    if weFarm[Apple] { MakeCider() }
    for f := range Fruit { if weFarm[f] { fmt.Println("Yes, we can supply %s\n", f) } <-- note, in order, not map-like. In most cases could be implemented with in-register bitset.

    Can the above be achieved with existing maps? Yes! with (minor) annoyances. However, I think this is a way for the language to express an opinion: "In these very simple cases, that are code really, this is the one true way you should do sequences and sets".

(this is very unoriginal and perhaps not directly relevant to @deanveloper 's proposal, sry :/ )

rsr-at-mindtwin commented 4 years ago

I think it would be appropriate to default to implementing behavior like -Wswitch-enum for the new enumeration type - that is, when switching on a given enumeration type, make sure all cases are covered in the absence of a default case, and warn when values are present that are not part of the defined enumeration. This is valuable for software maintenance when adding new cases.

deanveloper commented 4 years ago

Go does not have compiler warnings (only errors), however it may be a good thing to add to govet.

Miigon commented 3 years ago

It's 2021 2022 and I am still hoping we can get enum

emperor-limitless commented 2 years ago

Any news about this?

ianlancetaylor commented 2 years ago

There is no news. There isn't even agreement on what problem to solve. And this is affected by generics in 1.18, with the introduction of interface types with union type elements.

enkeyz commented 2 years ago

Would be nice to have, instead of the current hacky way(iota).

Splizard commented 2 years ago

There is no need to change the spec, Go 1.18 has everything that is required to define namespaced and marshalable 'enum types', including both exhaustive and non-exhaustive type switches, where the compiler ensures the assignment of valid values and automatic String(), MarshalText() methods. You can use these enum types in Go today.

package main
import (
    "fmt"
    "qlova.tech/sum"
)
type Status struct {
    Success,
    Failure sum.Int[Status]
}
var StatusValue = sum.Int[Status]{}.Sum()
func main() {
    var status = StatusValue.Success
    // exhaustive switch
    status.Switch(StatusValue, Status{
        status.Case(StatusValue.Success, func() {
            fmt.Println("Yay!", status) // output: Yay! Success
        }),
        status.Case(StatusValue.Failure, func() {
            fmt.Println("Nay!", status) // doesn't output
        }),
    })
}

Here's a weekday example to play around with: https://go.dev/play/p/MnbmE0B3sSE also see pkg.go.dev

nvhai245 commented 1 year ago

I think like const, enum should only be string, character or number, something like:

enum Role string (
  Admin = "admin" // default value
  User = "user"
  Guest = "guest"
)

var x Role
fmt.Print(x) // admin

y := "user"
if z, ok := Role(y); ok {
  fmt.Print(z)
  // user
}

w := "something"
v = Role(w) // compile error

s := Role.Guest // s will be of type Role
fmt.Print(s) // guest

t = string(s) // t will be of type string
melbahja commented 6 months ago

IMO just make it simple as possible:

type Status enum[int] {
    ALLOWED = iota
    BLOCKED
}
type Status enum[string] {
    ALLOWED = "y"
    BLOCKED = "n"
}

func foo(s Status) {
   println(s.(string))
}

foo(Status.ALLOWED)
chenjpu commented 6 months ago

IMO just make it simple as possible:

type Status enum[int] {
  ALLOWED = iota
  BLOCKED
}

type Status enum[string] {
  ALLOWED = "y"
  BLOCKED = "n"
}

:)

type Status[int]  enum  {
    ALLOWED = iota
    BLOCKED
}

type Status[string] enum  {
    ALLOWED = "y"
    BLOCKED = "n"
}
Malix-off commented 2 months ago

2024 is the year of Go enum (source: personal feeling)

emperor-limitless commented 2 months ago

Did something happen? Your comment makes no sense.

Hixon10 commented 1 month ago

Every time, when I need to write default statement, I feel bad. Compiler already knows, that it is unreachable code. I don't need it:

type DayOfWeek int

const (
    Sunday DayOfWeek = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

func (d DayOfWeek) String() (string, error) {
    switch d {
    case Sunday:
        return "Sunday", nil
    case Monday:
        return "Monday", nil
    case Tuesday:
        return "Tuesday", nil
    case Wednesday:
        return "Wednesday", nil
    case Thursday:
        return "Thursday", nil
    case Friday:
        return "Friday", nil
    case Saturday:
        return "Saturday", nil
    default:
        return "", errors.New("invalid day")
    }
}
doggedOwl commented 1 month ago

But it is reachable: fmt.Println(DayOfWeek(15).String()). The fact that you have defined 7 constants does not limit the DayOfWeek type to contain only those 7, and that's why this kind of simulated Enum is not enough.

Hixon10 commented 1 month ago

But it is reachable: fmt.Println(DayOfWeek(15).String()) and that's why this kind of simulated Enum is not enough.

Exactly! We need enums (or algebraic data types), and exhaustive switch.