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.

7 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.

1 Like

Testing the case of an enum while discarding the associated value is definitely a Swift pain point that stands out more and more as apps transition to SwiftUI.

SwiftUI can't work with protocols, so to display a typical hierarchical OutlineView that has different model objects at each level, I wrap those in an enum:

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

Some UI elements, such as context menu items, need to be enabled/disabled based on which type of object is selected in this tree. But in the .disabled() view modifier, if case let and switch are out. I need to express, "I don't care what the associated value is; just tell me if this thing is a .client, .project, .job, or .version."

I currently do that the same way people are doing it here, but it's definitely something that should just be in the language.

Case Paths is a well-explored library supporting this and more, and I'd be happy to see its features supported directly in the language, but sadly it hasn't been prioritized by anyone with the compiler knowhow just yet.

2 Likes

I'm sure there's 742 edge cases I don't know about, but it always seemed to me like this is the most elegant syntax:

let myProject = Project()
let foo: NavNode = .project(myProject)

if foo == .client(_) {
   print("We have a client case")
}

And that would translate well to SwiftUI:

Button("Blah") {
}
.disabled(selectedNavNode != .project(_))

It'd be even nicer if they just worked without the underscore at all, but that seemed like asking too much. Maybe if you decorate the enum with the equivalent of @discardableResult? Something like:

@discardableValue enum NavNode
{
...
}

// Now the concern about obfuscating the fact that this enum has associated values is gone because we've explicitly opted in:
@State var selectedNavNode: NavNode?

Button("Blah") {
}
.disabled(selectedNavNode != .project)

The use cases that use if/switch to compare to another "constant" value is not a good motivator, as you could write this today:

if case .client = foo {
}

It becomes more interesting when you need comparing two non-constant values.


This works in basic cases:

func valueBaseName(_ value: Any) -> String {
    var string = "\(value)" // string("hello")
    if let paren = string.range(of: "(") {
        string .removeSubrange(paren.lowerBound...)
        return string
    } else {
        return string
    }
}

print(valueBaseName(Foo.string("hello")))   // "string"
print(valueBaseName(Foo.number(42)))        // "number"
// used like:
valueBaseName(foo) == valueBaseName(bar)

Although it's easy to break it (e.g. by having a CustomStringConvertible implementation that does something else).


+1 to having something better built-in, ideally that's O(1) in both time and code size.

That is not an option in .disabled(). If you're lucky, the compiler will tell you 'if' may only be used as expression in return, throw, or as the source of an assignment. If you're unlucky, it will tell you it can't type check your view in reasonable time.

My primary need for this is in SwiftUI. If I'm not working in a ViewModifier, I generally do care about the associated value and switch or if case let work fine.

I've updated my examples above to stick to SwiftUI viewModifiers, since that's where the pain point really is.

1 Like

Got you. That would be less of a problem once we finally implement "Full Expressions" form of SE-0380.


Quite awkward workaround:

Button("Blah") {
}
.disabled({if case .project = selectedNavNode { true } else { false }}())

would be a bit nicer with "full expressions" implemented:

Button("Blah") {
}
.disabled(if case .project = selectedNavNode { true } else { false })

although still looks heavy and redundant.

2 Likes

Ha, stack a few of those in a modifier and you're basically writing Lisp.

How about this (new) syntax?

.disabled(case selectedNavNode != case .project)

Could be useful in the other mentioned use case of comparing two variables:

case foo == case bar

The enum itself will not have to be Equatable for this to work.

case foo or case .project would give a discriminator, e.g. Int.

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

print(case NavNode.project) // 1
let v = NavNode.project(Project(...))
print(case v) // 1