[Enum] Allow to init/assign a case with another case of the same enum (for aliases)

As titled, I'd like to being able to type

enum MouseButton: Int {
    case _1, _2, _3, _4, _5, _6, _7, _8
    case last = _8, left = _1, right = _2, middle = _3
}

You can do:

enum MouseButton: Int {
    case _1, _2, _3, _4, _5, _6, _7, _8
    static let last = _8, left = _1, right = _2, middle = _3
}

And then use e.g. MouseButton.last or button: MouseButton = .last.

8 Likes

I am not sure case is the right keyword for the declaration, but it would be nice to have some form of alias that the compiler can see through. There are some things static let/var does not do well right now, such as exhaustivity checks:

typealias Direction = כוון
enum כוון {

    case ימין
    static var right: Direction { ימין }

    case שמאל
    static var left: Direction { שמאל }
}

func useEnglishAliases(_ direction: Direction) {
    switch direction { // Error! Switch must be exhaustive!
    case .right:
        print("right")
    case .left:
        print("left")
    }
}
2 Likes

I moved this topic to the Evolution category. I would very much like to see Swift provide a real solution to this in the future.

I’m fine with the idea of having aliases - they can be useful at times, for sure. Especially when using enums to model existing, real-life stuff where there often already exists aliasing whether implicitly or explicitly.

But, how would this behave re. exhaustivity checks? Do they go based on the nominal cases or their values? Logically it has to be the latter - at runtime the enum is ultimately just a value, and if you allow two named cases to have an identical value, they are indistinguishable at runtime. That smells bad.

For example, what if a coder writes:

enum MouseButton: Int {
    case _1, _2, _3, _4, _5, _6, _7, _8
    case last = _8, left = _1, right = _2, middle = _3
}

…

switch button {
    case ._1:
        // Do one thing
    case .left:
        // Do another thing
    …
}

Now what? Logically that has to be a compiler error. Solvable, but scrutiny is warranted any time the language introduces a new way to construct an invalid scenario.

The existing best approach for this - of static properties as @SDGGiesbrecht suggests - seems like the better approach. It’s only annoyance currently is that enums don’t allow you to define constants, i.e. use let, which is important both semantically to the user (it’s a constant, not a variable) but also presumably to the compiler, so that it can ultimately generate equally optimal code if you use one of those aliases vs the ‘raw’ case. (alternatively, today, you can define these aliases as constants outside the enum, in a context where let is allowed, but doing so has poor cohesion)

So I think what this is ultimately really wanting is just the relative minor addition of support for let attributes on enums…?

Since they are aliases (that is, they represent the same enum behind), I'd assume that to be an error that the compiler shall throw

I'll be still happy with let though

static let is supported on enums. This already works:

enum MouseButton: Int {
    case _1, _2, _3, _4, _5, _6, _7, _8
    static let last = _8, left = _1, right = _2, middle = _3
}

Or are you talking about something else?

I think he meant:

enum MouseButton: Int {
    case _1, _2, _3, _4, _5, _6, _7, _8
    let last = _8, left = _1, right = _2, middle = _3
}

What's the difference if it's not part of the enum?

Indeed.

The problems with the existing nearest approximation - static let, or a let outside the enum itself - are:

  1. You cannot use them in the same way as the native case values, i.e. you can’t use .foo shorthand. They are values of a specific enum case, not an enum case. (This would naively be true of a (non-static) let also, but could be special-cased.)
  2. The compiler doesn’t understand that they’re equivalent. e.g.:
    1. It’ll happily let you use both as cases in a switch statement, and will just silently drop any duplicates, arbitrarily picking the first. Which is consistent, sure, but not intelligent - it’s logically invalid to have the same raw value matched twice in a switch statement, whether it’s via aliasing or literally writing the same case twice (which the compiler currently also silently allows, erroneously). Sometimes it’s nondeterministic, when there’s variables or module boundaries involved, but if it’s clear at compile time, the compiler would do well to raise a red flag.
    2. It doesn’t recognise them for determining switch completeness.
  3. In the case of let outside the enum, that fragments the code and makes it harder for people to discover that there are aliases. Forcing aliases to be defined outside the enum has no upside nor functional necessity (unlike, say, extensions). Allowing them to be defined outside - such as in an enum extension - would be nice too, though.

So on further reflection, I think there is merit to the proposal as originally written, both in syntax and in need. Yes, it does introduce the possibility of defining two cases in a switch which actually map to the same value, but that’s already possible with the let alternatives and with just naively repeating a literal case name, and the compiler already screws up those situations anyway. If that were fixed, which it should be, it eliminates the only apparent downside to aliasing in principle.

Whether or not it’s by defining cases in terms of other cases, or by allowing (non-static) let, seems to mostly depend on whether we need to maintain some idea of a ‘primary’ name for a value (and don’t otherwise want to just pick one arbitrarily, e.g. whichever appears first in the source). case foo = .bar seems like the better option overall, IMO - less magic involved for users and the compiler in treating the aliases as completely equivalent to the canonical cases.

Addendum

The situation is more complicated once you get to overlapping but not identical cases, within a switch (e.g. when using ranges, or matching sets, etc). I think I still want the compiler to at least warn about that, if not treat it as an error, since it seems like a code smell at best, but maybe there’s scenarios where it’s less clearly erroneous (and/or difficult for the compiler to prove conclusively, even if technically it’s possible with the information available at compile time)?

1 Like

That's what I've been thinking about raising compile error for duplicated cases. I don't want to put more strain on the compiler if the current behavior is already consistent. It sounds like a better job for analyzer than for compiler.

That aside, what's the use case you have in mind for such alias. AFAIR, C usage of first/last aliases are used mainly to figure out the number of possible cases (for memory allocation), and/or to check if a value is part of a particular group. Both are already superseded by CaseIterable, and computed properties.

FWIW you can already do dot-syntax where enum is expected.

enum Foo { 
    case a, b, c 
    static let d = Foo.a 
} 
let x: Foo = .d // Foo.a
let y: Foo = .a // Foo.a

And anywhere that expect enum value would accept both .a and .d. So I don't see what's the problem here.

3 Likes

Huh! I did not know that - thanks! I wonder if that’s always worked? I didn’t sanity-check that in Playgrounds before posting because I seemed to recall running into that in practice in the past.

In any case, the status quo is significantly more ergonomic than I thought, then. Though it doesn’t address the other issues, e.g. around duplicate cases in switches etc. Maybe it makes them orthogonal, though… other than perhaps some debatable and subtle semantic differences between static let vs case.

I think in layman’s terms “analyzer” vs “compiler” is somewhat academic, which is to say, hopefully irrelevant. As long as the end result is that your code editor warns you of the issue immediately, and the code doesn’t compile if the situation is serious, then it’s all good.

This seems worth filing a bug over for static let. The compiler can't know that a static var is equivalent so I see why no one tried yet but for cases where the compiler can know… it seems reasonable.

1 Like

It’s been that way since Swift 3 at least, probably longer. When you use leading dot syntax (.last or .first(5)) Swift will look for any static member in the type, whether case, function, property or constant. You can actually think of cases as static functions that construct the enum’s value (SE-0155 calls them ”case constructors”).

1 Like

I was thinking the same. I think that making constants (such as static let, or global lets) participate in exclusiveness checking should be possible and hope it's even not too hard