Is there a way to check if an enum is a specific case without switching on it?

I've seen this syntax:

enum E {
    case foo, bar
}

let f: E = .bar

if case .foo = f {
    ...
}

But this doesn't work:

XCTAssert(case .foo = f)

I'm guessing that syntax is magic and only works in an if statement. Which seems silly to me. Is there no other way to do the same check, but capture it as a Bool expression?

enum Foo: Equatable {
  case zero
  case one(Int)
}

let a: Foo = .zero
let b: Foo = .one(0)

XCTAssertEqual(a, .zero) // ok
XCTAssertEqual(b, .one(0)) // ok

That works if I own the enum, but I don't own Result for example, which doesn't conform to Equatable, and you're not supposed to add conformances to types you don't own.

Also, I'm not asking to check for absolute equality (payloads and all), I just want case equality in this instance. :/

You can assert the Result's value.

let some: Result<Int, Error> = .success(0)
XCTAssertEqual(some.value, 0)

I'm not trying to compare payloads, I just want to check if the enum is a specific case.

I want to be able to do this:

let f: E = .foo
// whether f is .bar, regardless of whether .foo or .bar have payloads
let booleanExpression = case .bar = f

If this functionality doesn't exist, it seems like a hole in the language to me.

1 Like

If you want to check for a case, that's not gonna work

let some: Swift.Result<Int, Error> = .success(0)
XCTAssertEqual(some, .success(_)) // error
XCTAssertEqual(some, Swift.Result<Int, Error>.success(_)) // error

I wrote a pitch on it a while ago: Comparing enums without their associated values

I just need to find some time to figure out the best way to do it.

Anyhow, in Result case, you can just check for value or error, which is the same as checking for success or failure cases.

Result conforms to Equatable and Hashable, when its contained Success and Failure types do. Making it more generally Equatable would not do what you want, unless you violated the requirements of Equatable and made .success(1) == .success(2). I believe there have been discussions of case equality before but they haven't gone anywhere specific. You can find other threads around enum ergonomics (I think that's what they were called).

Swift's Result type has no properties, so that isn't possible.

1 Like

That thread seems to be aimed at the problem I'm trying to solve. I want a solution that works for any enum, regardless whether it has a payload or not. Some expression that evaluates to a Bool, like f is .foo or case .foo = f without the if.

Swift already has this functionality built in: it's the logic behind a switch statement. It just doesn't expose it anywhere but if case ...

Ah, I forgot. I have an extension on it that adds these convenience properties.

I may just write a more formal pitch in hopes someone can implement it then. Thanks!

2 Likes

To this day I'm still being bitten in the ass by this. Can someone smarter than me write a proposal?

1 Like

Sadly enums are still a bit behind structs in this language. There seems to be a desire to improve the situation but there hasn't been any actual movement AFAIK yet :(

Indeed, no movement as of yet. We still need a compiler engineer that can provide an implementation to go along with the pitch for CasePaths.

Can you please describe what the specific task are you trying to solve?

I suppose what you need will generally become possible in future, when Enums will have generated Discriminator. This was discussed several times, here are some links: Comparing enum cases while ignoring associated values - #8 by Dmitriy_Ignatyev
Discriminator is a generated plain enum with the same cases. So, with result instance you can do something like result.discriminator == .success no matter what the generic Success & Failure types are.

An alternative vision for solving the discriminator problem with less compiler magic consists on leveraging a hypothetical CasePaths implementation, and with it PartialCasePaths (which are discriminators in their very essence):

enum Foo {
    case bar(String)
    case baz(Int)
}

// forming a discriminator
let barDiscriminator = \Foo.bar as PartialCasePath // PartialCasePath<Foo>

// comparing a case against this discriminator
let isBar = Foo.baz ~= barDiscriminator // false
// shorter spelling
let isBar = Foo.baz ~= \.bar // false

Taking things even further, a CasePathIterable protocol mirroring CaseIterable could also be introduced to obtain a compiler-synthesized discriminator array:

enum Foo: CasePathIterable {
    case a(String)
    case b(Int)

    // Compiler generated
    static var allCasePaths: [PartialCasePath<Foo>]
}

let allDiscriminators = Foo.allCasePaths // [\Foo.a, \Foo.b]

Of course, CasePaths along with PartialCasePaths and all other forms would be Hashable and Equatable just like their KeyPath counterparts, so this makes it very easy to identify, store and compare specific discriminators.

If CasePath were in the key path hierarchy, PartialCasePath wouldn't be necessary. We could just use PartialKeyPath:

class WritableOptionalPath<Root, Value>: KeyPath<Root, Value?> { … }
class CasePath<Root, Value>: WritableOptionalPath<Root, Value> { … }

enum Foo {
  case bar(String)
}

\Foo.bar as PartialKeyPath // ✅

To the OP's question, though, case paths provide an expressive way to solve the above:

// Library today:
XCTAssert(/E.foo ~= f)

// If included in the language:
XCTAssert(f[keyPath: \E.foo] != nil)

While having enum key paths in the language would unlock a lot of possibilities by default, the ergonomics would still trail property access if key path subscripting is required.

4 Likes

I think I'm lost in what you all are trying to accomplish now. I really just want the logic of an if case statement to be available as a stand-alone expression outside of an if statement. It's that simple :sweat_smile: like this:

let isSuccess = case result = .success(_)

I may have success and result flip-flopped here but whatever

I think there's a larger story about enum ergonomics to contend with, but if we had first-party case paths then you could do this:

let isSuccess = result[keyPath: \.success] != nil

Ideally, though, there would be more ergonomic access, something akin to struct property access. Then you could "open" any enum's associated values via something like dot-syntax:

let isSuccess = result.success? != nil
1 Like