There are a number of discussions about potential enhancements to the Enum type.
For example, the ability to reopen Enum types to add additional members has been requested in Reopening Enums · Issue #7629 · crystal-lang/crystal · GitHub, but it was rejected. I believe we should perhaps revisit that decision.
The primary argument against reopening is to have a fixed, final state of all enum members, so there can be no surprises when switching over them (e.g. in exhaustive case ... in).
A flaw in that argument is that it equates enum members with values. A member is just giving a name to a value. For example, member Colorize::Mode::Bold is a name for the value returned by Colorize::Mode.new(1).
For many (most?) enum types, the set of members contains all legal values. There exists no value that’s not described by a member.
But some enum types can have more values than named members. This is typically the case when an enum is not an original concept and reads values from an external source. For example error or status codes such as Errno or HTTP::Status. These types have a large domain of possible values, but only a fraction of them is represented as a named member. The rest might not be standardized, is typically irrelevant or platform specific. But that doesn’t mean such unnamed values can ever be observed.
Enum types typically allow instantiating arbitrary values through their numeric constructor.
So, if that is allowed, we can always be surprised by an unexpected enum value.
case ... in handles that by raising a runtime error.
Adding an additional member on the other hand would result in a compile-time error.
That’s actually a better error mode! We shouldn’t refuse that with an argument that it may break things, when the alternative might break as well but in a less obvious way.
However, it’s pretty uncommon to have an enum type with open values (e.g. which might be instantiated from numerical input) used in exhaustive case.
I’ve come to think that we can describe two different flavours of enums:
- a fixed list of values, all expressed as members.
- an open list of values, with some common ones as members.
(on top of that, there is also the @[Flags] flavour for flag enums)
Perhaps we could improve the enum features in the language to separate these use cases more clearly.
For example, an enum with a fixed set of values should reject instances with any other value making it impossible to get an unnamed value. It should not be allowed to reopen and add additional members.
An enum with open values does not need to be usable in exhaustive case. Reopening and adding additional members is fine.
As a mechanism to designate an enum type with fixed values, the @[Sealed] annotation could be considered. It has been proposed for general class inheritance, but would apply to this use case as well: [RFC] Introduce a @[Sealed] annotation · Issue #9116 · crystal-lang/crystal · GitHub