Pitch: Auto-synthesize cases for enums

To go along with the recently reviewed Derived Collection of Enum Cases, I'd like to propose that we also derive a Case enum for all enums.

The problem
One of the biggest issues I have with using enums is that comparing the case of two enums is really painful. Take for example, if I decided to make a game called Orc vs Elves vs Humans, and decided to represent them as an enum

enum Race {
  case orc(weapon: Weapon)
  case elf(spell: MagicSpell)
  case human(karateMove: KarateMove)

  func attack(race: Race) {}
}

Now lets say that there's a crazy wizard, who every 30 seconds casts a spell that makes a random one of these races invincible. Unfortunately, right now in Swift there's not really a clean way to do it. How do I store which of these races is the currently invincible one?

Pitch
Can we auto-synthesize an internal Case enum inside of enums to represent the case and expose that via a case var? For example:

enum Race {
  case orc(weapon: Weapon)
  case elf(spell: MagicSpell)
  case human(karateMove: KarateMove)

  func attack(race: Race) {}

  /// AUTO-SYNTHESIZED
  enum Case {
    case orc
    case elf
    case human
  }

  // AUTO-SYNTHESIZED
  var `case`: Case {
    switch self {
    case .orc:
       return .orc
    case .elf:
        return .elf
    case human:
       return .human
    }
  }
}

That would solve our above problem because we could use:

var invincibleRace: Race.Case

and for checking if we can attack it?

func attack(race: Race) {
   guard race.case != invincibleRace else { return }
   doDamage()
}

It would also make it possible to compare the case of two enums, which is currently super painful. Say we have:

let a = Race.orc(weapon: Sword())
let b = Race.elf(spell: Fireball())

This is what we have to do now:

  switch(a, b) {
    case (.orc, .orc):
      return true
    case (.elf, .elf):
      return true
    case (.human, .human):
      return true
    default:
      return false
  }
}

This is what it would look like with the synthesized case:

return a.case == b.case

I've been using sourcery to autogenerate these already, but it's worked well enough that I think it's worth considering for Swift itself.

7 Likes

In all my projects, I auto-generate code (with Sourcery) to help dealing with enums with associated values and part of it is exactly what is proposed here. So I can say by using this firsthand that it is very useful.

The line between enum and struct gets thinner and thinner... I think properly using such powerful stricter and enums is one of the first hard steps in learning this language...

1 Like

There were a few cases where I would've really liked to have something like this... It is an issue that outside of pattern matching, which isn't available everywhere, you cannot refer to a case without it's values without actually building one.

The problem being described here is a real one, but I'm not sure synthesizing a whole new enum of just the cases is the right solution. That doesn't seem to hold its weight, because you can still check the case by itself (ignoring associated values) in a pattern match today. The real problem, IMO, is that pattern matching isn't rich enough yet to do some of these common tasks, so can we recast the problem as one of improving pattern matching?

From my experience, the biggest pain points I've hit are these:

  • You can't use a pattern match as a general Boolean expression; it has to be a clause in an if, guard, or while statement.
  • You can't write an inverted pattern match; so if you're only interested in "if a variable is any of these cases except this one", you have to write some odd code flow to make that work.
  • You can only match a value against a "compile-time-defined pattern", so you can't easily check whether two values are the same case, as already mentioned above.

While it's true that the synthesis-based solution above would help solve these, I feel like making pattern matching more capable would fit better into the language ecosystem and how enums are used.

12 Likes

The problem with this motivating example is that the enum is misnamed. The enum is not a Race, but a RacialAttack.

At best, using a value like Race.orc(.axe) to represent "orcs" is just a shortcut, and a kinda confusing one at that: either the type is confusingly named, or if it's renamed to something clear like RacialAttack, the type representing a race would be confusingly named RacialAttack.Case.

Is there a motivating example that isn't better served by a pair of separately-declared types?

It's also pretty weird what might be supposed to happen with an enum like this:

enum ABC {
  case a
  case b
  case c
}

and weirder for this:

enum ABC {
  case a (Int)
  case b
  case c (description: String)
}

[Edit: Corrected for lousy grammar.]

2 Likes

I can agree that there's a ton of solutions out there to solve this problem and creating these extra Case enums is more heavy handed than most. Fundamentally though, upgrading pattern matching doesn't fix my initial question:

Now lets say that there’s a crazy wizard, who every 30 seconds casts a spell that makes a random one of these races invincible. Unfortunately, right now in Swift there’s not really a clean way to do it. How do I store which of these races is the currently invincible one?

I'd liken this to arguing for the need for Class.Type metatypes, but for cases with enums.

In a sense, we already have those—cases with associated values are really just static functions that return instances of the enum. So the "case" of a value can be uniquely identified by its instantiation function. (Cases without associated values mix things up a bit, since those are static properties that equal the value, not parameterless functions that return the value.)

The problem is, given an enum value, there's no way to retrieve its case function. Even if you could, there's no way to compare functions for equality, and since cases could have different payloads, the concept "any instantiation function of enum Foo" doesn't necessarily have a single type.

It's an interesting design problem, to be sure!

1 Like

oh God, no. We really need a better answer for these kind of meta-programming tasks than just adding another magic auto-synthesised thing. What you're asking for is "reflection".

Besides, I would argue that you've structured your data-model awkwardly for what you are trying to do. Is a Weapon or MagicSpell really something which describes a Race? Or are they rather status modifiers which may be applied to a Character of said Race? What happens when you add cases or other modifiers? It's quickly going to become a nightmare to maintain.

Basically, you have very-tightly coupled your data and you're asking for reflection to help you un-couple it. I would rather do something like:

enum Race {
  case elf, orc
}
enum AttackStyle {
  case magicSpell(MagicSpell)
  case weapon(Weapon)
}

struct Character {
  let race: Race
  let attackStyle: AttackStyle

  // Private init so you can't mix-and-match races and styles.
  private init(race: Race, style: AttackStyle) { ... }

  // Only expose allowed pairings of races and styles.
  static func elf(spell: MagicSpell) -> Character {
    return Character(race: .elf, style: .magicSpell(spell))
  }

  func attack(other: inout Character) {
    // Some attack styles are more/less effective against certain races, or whatever.
    switch (self.attackStyle, other.race) {
      ...
    }
  }
}

let anElf = Character.elf(spell: OpenSesame())
13 Likes

How would you synthesize the cases for the following enum, because as per SE-0155 this is a valid enum (however it's not implemented yet, but it has higher priority than this pitch):

enum Foo {
  case a(x: Int)
  case a(y: Int)
}
3 Likes

Yikes. Didn't know that was going to be legal. In that case I'm not sure this is really solvable? That means that any type that these would have would be closer to a closure type which don't really have the ability to be equatable, do they?

I was hoping that the absurdity of humans using Karate and an evil wizard casting invincibility would steer people more towards the fundamental problem than the example, but c'est la vie :).

There's nothing wrong with the subject matter of your example. The issue is that it's a semantically illogical solution to the problem it purports to solve. Using two separate types (one for race, one for weapon) would be a better solution.

My question was to find out if there's a plausible example (even about wizardry) where synthesized enum cases would be a better solution than two separate types.

7 Likes

+1

I've wanted this since forever!

It's crazy to create a second enum to describe an existing enum with payload case, yet that is a situation that comes up regularly.

For example?

What we haven't seen yet is a plausible motivating example where the overall design isn't clearly better served by two separate enums.

FWIW, this proposal does in fact suggest creating a second enum to describe an existing enum. The advantage is that you don't have do it manually. The disadvantage is that it has a meaningless or misleading name.

1 Like

Ah. I misunderstood. I'd much prefer we overloaded the case name as part of the original enum. So if you switch on, or test equality, on .foo instead of .foo(bar) it "just works" for any .foo(). Also would like to creat a .foo on its own with no args. Maybe it's incorrect philosophically for Swift? I'd love it though

Would an equivalent to Rust's mem::discriminant address the parent's use case?

4 Likes

+1

This is a great way to solve this issue.

One potential issue for this being code gen and not a runtime implementation is code size - however either way you wouldn't know the difference as a developer / calling into this

Thank you for clarifying this for me. I didn't get the point of the original poster and skipped over the whole thread.

Let me expand on what this means:

Swift enums with associated types do have some internal tag (also called a discriminant) to interpret the content stored in them. In computer science an enum with associated type is known as a tagged union or discriminated union.

Currently, swift does not expose this tag, but it can be very useful to have access to this tag. I support the idea of exposing enum tags as an opaque type similar to ObjectIdentifier. It will need some special compiler support because currently there is no way to properly constrain the type that the constructor of this type should accept.

We can call this type CaseIdentifier, CaseDiscriminant or verbose variants EnumCaseIdentifier or EnumCaseDiscriminant. It needs to be Equatable and Hashable and should be generic with enum type argument for type safety.

For example, we could have something like this:

enum E {
    case a(Int)
    case b(Double)
    case c(String)
}

typealias ETag = CaseIdentifier<E>
let a = ETag(.a) // type of `a` is `CaseIdentifier<E>`
var eCases: [ETag: E] = [:]
eCases[ETag(.a)] = .a(15)
2 Likes

mem::discriminant doesn't quite meet the original request, because there are no literals of type Discriminant<T>. The function allows two values of the enum to be compared for the same case, but can't compare against a literal case except via constructing an intermediary value.

For example, this works (Swiftified syntax):

guard attack.discriminant != counterattack.discriminant else { return }

or this, though it's pretty ugly (and arbitrary):

guard attack.discriminant != .orc(weapon: Sword()).discriminant else { return }

For syntax error checking reasons, letting .orc represent its own case discriminant doesn't seem feasible. So, not this:

guard attack.discriminant != .orc else {return}

but there could be special syntax:

guard attack.discriminant != .orc.discriminant else {return}

or:

var invincibleRace: Discriminant<Race> = .orc.discriminant

and … it all starts to drown the semantic intent in noise.

It's not consistent with Swift's design to generate magic members like that. I would imagine that the Swift version of the syntax--if we go down this route--would parallel that of MemoryLayout.size(ofValue:):

guard MemoryLayout.caseIdentifier(ofValue: attack) != MemoryLayout.caseIdentifier(ofValue: counterattack) else { return }

You are right that it would be challenging to obtain such a value without an instance of it. The alternative is to use key paths for this purpose, which is a plausible extension of the language.

...or, and I'm not sure why you're not using this today, use guard case let .orc(_) = attack.

2 Likes