Comparing enum cases while ignoring associated values

I like that a lot. Also fairly natural to grok: “use case let when you care about the associated value and omit the let when you don’t.”

Totally agree — I’ve run into this exact issue multiple times too. Would love a clean way to compare just the case, ignoring associated values. Hope this gets some attention!

Not built-in, but Case Paths has O(1) lookups for this and more:

Foo.allCasePaths[foo] == Foo.allCasePaths[bar]

foo.is(\.string)
bar.is(\.number)

We'd love to retire the library for built-in solutions, but it's been many years since we first released it and there's been no real public movement in the language. So if you do leverage enums in your apps, the Case Paths package may help you write the kind of code you've been wanting to write.

4 Likes

Interestingly Error / NSError has the superpowers to know enum's discriminator and by abusing it you could get that out:

protocol EnumWithDiscriminator: Error {
    var discriminator: Int { get }
}
extension EnumWithDiscriminator {
    var discriminator: Int {
        (self as NSError).code
    }
}

Test:

struct Client {}
struct Project {}
struct Job { let name: String }
struct Version {}

enum NavNode {
    case client(Client)
    case project(Project)
    case job(Job)
    case version(Version)
}

extension NavNode: EnumWithDiscriminator {}

let a = NavNode.job(Job(name: "a"))
let b = NavNode.job(Job(name: "b"))
let c = NavNode.project(Project())
precondition(a.discriminator == b.discriminator)
precondition(a.discriminator != c.discriminator)
1 Like

The implementation is probably there: _swift_stdlib_getDefaultErrorCode(), vw_getEnumTag() (i.e. not publicly available).

1 Like

The runtime metadata is stable and available, but it's not the most straightforward to access. Here's something you can drop into any project, though:

func areCasesEqual<T>(_ lhs: T, _ rhs: T) -> Bool {
  guard
    let lhsTag = enumTag(lhs),
    let rhsTag == enumTag(rhs)
  else { return false }
  return lhsTag == rhsTag
}

func enumTag<Case>(_ `case`: Case) -> UInt32? {
  let metadataPtr = unsafeBitCast(type(of: `case`), to: UnsafeRawPointer.self)
  let kind = metadataPtr.load(as: Int.self)
  let isEnumOrOptional = kind == 0x201 || kind == 0x202
  guard isEnumOrOptional else { return nil }
  let vwtPtr = (metadataPtr - MemoryLayout<UnsafeRawPointer>.size).load(as: UnsafeRawPointer.self)
  let vwt = vwtPtr.load(as: EnumValueWitnessTable.self)
  return withUnsafePointer(to: `case`) { vwt.getEnumTag($0, metadataPtr) }
}

private struct EnumValueWitnessTable {
  let f1, f2, f3, f4, f5, f6, f7, f8: UnsafeRawPointer
  let f9, f10: Int
  let f11, f12: UInt32
  let getEnumTag: @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> UInt32
  let f13, f14: UnsafeRawPointer
}
7 Likes