Comparing enum cases while ignoring associated values

Multiple times I have now wanted to compare two values of an enum while ignoring their respective associated values.

enum Foo {
    case string(String)
    case number(Int)
}

let a = Foo.string("str")

a == Foo.string // Binary operator '==' cannot be applied to operands of type 'Foo' and '(String) -> Foo'

I understand why this doesn't work, but... I really want it to! Looking around the web for workarounds has led me to Erica Sadun's article, an interesting gist from the comments of said article, and even a post from Swift 1.2 era asking basically the same question.

All the solutions I've seen so far either require me to manually write type-specific code, or are plain ugly. Even if there are workarounds, though, I'd love for the following to work as well:

foos.contains(.number)

...which could only work if the cases without their associated values were somehow Equatable.

I suppose I'm posting this here out of frustration and hoping that perhaps someone from the Swift team will chime in, because this feels like it should work and just doesn't...

5 Likes

My only use of Sourcery is to auto-generate a subtype on my associated value enums so I can do simple == and != checks for their case names. I would love to see this feature exist in Swift.

It should be easy to do things like the following without resorting to weird quirks or a bunch of extra code

enum State {
  case ready
  case loading(reasons: [String])
}

let stateA = State.ready
let stateB = State.loading(reasons: ["ReasonA", "ReasonB"])

print(stateA.case == stateB.case) //false

if stateA.case != .loading { 
  // do something
}

There are a bunch of hiccups when writing if case let something = .enumCase (including the inability to use != with case let).
I too would love to see it simplified!

2 Likes

Relevant discussion regarding enum ergonomics: Automatically derive properties for enum cases - #5 by stephencelis

You can always do

if case .string = a { 
  print("found a string!") 
}
3 Likes

I think that, similar to SE-0194, the best way to provide this feature would be to add a CaseDiscriminatable (to be bike-shedded) protocol, for which, like CaseEnumerable a compiler-provided default implementation would be created for same-file extensions.

e.g:

protocol CaseDiscriminatable {
  associatedtype Discriminator: Hashable
  var case: Discriminator
}
6 Likes

Here's a simple (if slightly verbose) way to do it:

enum Foo {
    case string(String)
    case number(Int)

   func hasSameCaseAs(_ type: Self) -> Bool {
      switch self {
         case .string: if case .string = type { return true }
         case .number: if case .number = type { return true }
      }
      return false
   }
}

var a = Foo.string("hello")
var b = Foo.number(6)
var c = Foo.number(5)

a.hasSameCaseAs(b) // returns false
b.hasSameCaseAs(c) // returns true
a.hasSameCaseAs(Foo.string("")) // returns true

Doesn't use the == operator but it's not really ==, is it?
As an aside, I'm unhappy with the form: if case .string = type { return true } too since its not an assignment but a comparison and would be better served by ==.

If the enum is Equatable, this first test works but the second doesn't, maybe it should?

a == .string("hello") // true
a == .string(_) // error: '_' can only appear in a pattern or on the left side of an assignment

To me if a == .string(_) looks so much better than if case .string(_) = a.

Hello. This topic is regularly being discussed. Some of the discussions:

As current workaround, you can use this solution:

But be careful and keep in mind, it is not safe and can be broken in future.

2 Likes

this works now without brackets and Equatable requirement:

if case .string = a {

if to choose between:

if a == .string { // proposed
if .string == a { // same
vs
if case .string = a { // existing

then +1 for the former form. interestingly Equatable requirement can be optional:

enum E {
    case string(String)
    case red
    case green
    case blue
}
if a == .string { // proposed: ok
if .string == a { // same
if a == .string("hello") { // error, 'Equatable' required
if a == .red { // error, 'Equatable' required
1 Like

You can create a variable that holds their case names then compare it with each other.

enum Foo {
    case string(String)
    case number(Int)

    private var caseName: String {
        switch self {
            case .number: return "number"
            case .string: return "string"
        }
    }

    func hasSameCase(as type: Self) -> Bool {
        return self.caseName == type.caseName
    }
}

I think it's easier than using switch self then checking the equality one by one each case

1 Like

This might be an interesting use case for a macro, which could generate a nested Cases enum with one case for each case in the outer enum but without the payloads, along with a computed property that maps the payload cases in the outer enums to the corresponding no-payload cases in the inner enum.

6 Likes

@nicodioso Thank you so much for this. Someone should pitch this as a language feature so it isn’t buried or hand-rolled.

Button {...}
.disabled(status.isNot(.pending(reason: ""))

is very helpful! If I could add a feature to Swift, it would be something like...

Button {...}
.disabled(status.isNot(.pending))

so we could ignore associated values on enums for the purposes of “simple” comparison.