It seems like enums should have a way to refer to 'the base case without associated values'

I have the following enum with an equatable conformance


enum SpeechRecognizerState {
    case ready
    case permissionDenied
    case listening
    case stopped
    case error(error:Error)
}

extension SpeechRecognizerState: Equatable {
    static func ==(lhs: SpeechRecognizerState, rhs: SpeechRecognizerState) -> Bool {
        switch (lhs, rhs) {
        case (.ready, .ready),
             (.permissionDenied, .permissionDenied),
             (.listening, .listening),
             (.stopped, .stopped),
             (.error, .error):
            return true
        default:
            return false
        }
    }
}

That feels like a lot of boilerplate in the equatable to write

static func ==(lhs: SpeechRecognizerState, rhs: SpeechRecognizerState) -> Bool {
    return lhs.caseElement == rhs.caseElement
}
3 Likes

Error in general is not Equatable, and your Equatable implementation considers 2 instances in the .error case, but with 2 different errors, to be equal which, and given that equality implies substitutability, it doesn't seem correct.

An alternative would be to define a type like this:

struct EquatableError: Error & Equatable {
  private let wrapped: any Error & Equatable

  init<SomeError>(_ error: SomeError) where SomeError: Error & Equatable {
    self.wrapped = error
  }

  static func == (lhs: Self, rhs: Self) -> Bool {
    // some sensible way to check if `lhs.wrapped` and `rhs.wrapped` are the same error
  }
}

and then define your enum as this

enum SpeechRecognizerState: Equatable {
    case ready
    case permissionDenied
    case listening
    case stopped
    case error(EquatableError)
}

which would synthesize == just fine.

An option for EquatableError.== could be:

extension EquatableError: Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool {
    func isEqual<A, B>(lhs: A, rhs: B) -> Bool where A: Error & Equatable, B: Error & Equatable {
      if A.self == B.self {
        return lhs == rhs as! A
      } else {
        return false
      }
    }

    return isEqual(lhs: lhs.wrapped, rhs: rhs.wrapped)
  }
}

A proper automatic conformance to Equatable without tricks would probably be possible with generalized algebraic data types, for example:

enum SpeechRecognizerState: Equatable {
    case ready
    case permissionDenied
    case listening
    case stopped
    case error<SomeError>(SomeError) where SomeError: Error & Equatable
}

in this case the conformance can first check if 2 instances with case error also are constructed from the same type.

I think you're missing my point.

I know error is not equatable, and I don't want to make it equatable.

However, for this specific enum, I want to be able to consider states equal if they have the same base case.

My point is that something we can express easily in language 'has the base case changed?' requires a bunch of boilerplate to express in swift.

If you wanted to be a stickler on the use of Equatable, then you could do the same with some new operator definition. My point is that Swift makes it very verbose to say something that is conceptually very simple.

Just to illustrate

This is the kind of code I'm driving from the equatable

            if vm.speechRecogniserState.isIn([.paused,.completed,.ready]){
                Button {
                    vm.startSpeechRecognition()
                } label: {
                    Color.red.opacity(0)
                        .frame(maxWidth:.infinity,maxHeight: .infinity)
                }
                .contentShape(Rectangle())
            } 

where isIn() is an extension on Equatable

public extension Equatable {
    func isIn(_ array:[Self]) -> Bool {
        return array.contains(self)
    }
}
1 Like

You're still kind of "hacking" Equatable in order make it work with the contains function, because you're conforming to Equatable just to check for the specific case. How would it work if the state was in error case, like

if vm.speechRecogniserState.isIn([.paused, .completed, .error(...what should go here?...)]

I think it would be better (in general) to use switch or if case to check if some enum value is in some particular case. In your case, for example:

switch vm.speechRecogniserState {
case .paused, .completed, .ready:
  Button...
default:
  EmptyView()
}

You're still kind of "hacking" Equatable in order make it work

fine - pretend I'm trying to write a new custom operator

~= which means 'has the same base case as the other element'

That's a good example actually. Can you write that operator in Swift for a general case?
Let's say that it would be implemented for any enum which conforms to BaseComparable

protocol BaseComparable:Enum {
 static func baseCaseIsEqual(lhs:Self,rhs:Self)
}

we can both understand what the protocol and operator would do. I think we could explain it to a five year old.
I don't think I can implement it in Swift though...

You can, just as you mentioned already it involves an unwanted boilerplate at the moment:

  1. the way you showed in the head post.
  2. the way where you have "var discriminator: Int" implemented manually where you have a switch that matches your cases to an 0, 1, 2 ...
  3. ditto, but instead of using an int you are introducing another internal enum to use as a discriminator (even more boilerplate)
  4. some hacky way to read discriminator out of enum that uses undocumented knowledge and could break

I am with you on this one.

It's definitely not easy. You might want to look at swift-case-paths, from the pointfree guys, I think they implemented some technique that makes it possible to compare enum instances only based on their case.

I can implement it for a specific enum.
(although every time I add a new case, I need to update the implementation...)

I don't think I can implement it for enums generally though?

e.g. I can't create that protocol and and automatic conformance.

At least if you don't put default in the switch, compiler will prompt your attention so you won't be able forgetting adding the new case.

var discriminator: Int {
    case .ready: return 0
    .....
    case .error: return 4
    // don't use default!
}

Don't see a way. Could macros be used here?

That's interesting, thank you. Will have a look how they did it.

1 Like

I think it would probably be useful to be able to determine if 2 enums instances are of the same base case.

But you should note that a "same case" operator or function would not help with your original example.

In this line:

vm.speechRecogniserState.isIn([.paused,.completed,.ready])

you need Equatable, and in the array you must put fully qualified enum instances, that include the associated values, so you're not asking with that "is this enum in the same cases of any of these cases", you are really asking "is this instance equal to any of these instances".

Also, as mentioned, an Equatable implementation that only considers the base case is strictly wrong, given the semantics of Equatable.

What you really want (in your example) is comparing an enum instance with a pattern, something that can be done with a switch (that is a better tool for the job). When I write:

switch x {
case .foo, .bar: ...
...

the .foo and .bar parts are not enum values, but are patterns, that can be created just with a base enum case.

2 Likes

Won't help you in your specific example but this will work in simpler cases of CaseIterable + Equatable enumerations:

extension CaseIterable where Self: Equatable {
    var descriminator: Int {
        let i = Self.allCases.firstIndex { $0 == self }!
        let s = Self.allCases.startIndex
        return Self.allCases.distance(from: s, to: i)
    }
}

I won't use this in production for enum with many cases as performance here is O(number of cases)

1 Like

"would not help" ???

I could rewrite my isIn() function to use the new 'same case' operator.

If I wanted to go wild, I could even write a new func isIn(examiningEnumBase:Enum)

Well - since you're telling me 'what I want' - perhaps you could tell me how to implement my isIn() function using patterns?

This is a very simple idea - for me it's frustrating that the language doesn't give me a decent way to express it.

After that, why don't you tell me how much you like the if case let syntax :rofl:

FWIW, I checked their implementation – it belongs to my (4) bullet point above:

1 Like

You could, but you would still need to provide to the function an array of values that are fully qualified, not just base cases.

Let's say that there's a "same case" operator, and that you use it to write an isIn function. If might be ok for simple enums in some simple cases, but consider an enum like this:

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

Say that you have this instance

let value = Foo.baz("yello")

and you want to use the isIn function (that, remember, only checks the base case) to see if your value is of case bar or baz. You would need to call the isIn like this:

value.isIn([.bar(...what should you put here?...), `.baz(...same question...)])

you can see that the .bar and .baz instances that you put in the Array can have arbitrary associated values (because isIn only checks the base case) which makes no sense and clearly signals that value-based comparison is not the right too for the job.

You can't, because you can't pass an "array of patterns". The way you do this in Swift is, again, with switch, to match a value against one or more patterns.

Maybe, but if you want to push for this concept (that, as I mentioned, seems perfectly legit to me), you'll need better examples, because your case is already solved in Swift using different features.

I'm fine with the if case syntax, it makes perfect sense to me, even if it's a little cumbersome to use (there's an ongoing discussion to improve it in simple cases).

But I see that you're trolling here, and you're not interested in discussing realistic options, the actual features language and how they work, so I'll happily leave the conversation.

1 Like

you seem to be re-describing the problem statement.

the point is that in an language which allowed me to express 'just check the base case', the answer to ...what should you put here?... is nothing. Because it isn't relevant. That's the whole point. If you're only interested in the base - then the associated value isn't important.

syntactically, I'd probably write that as .bar(_) and .baz(_) because I want to be explicit that I'm ignoring the associated value.

Frustrating isn't it. You can't pass patterns across functions, or use them as variables (as far as I know). Another related area where a simple concept can't be expressed in Swift.

I think we're agreed that you can't write a function that checks 'does the base case of this enum match'. (one that doesn't have massive boilerplate, and that doesn't need to be rewritten when a new case is added).

I think the title 'should have a way' makes it pretty clear I understand Swift doesn't currently have a way. I'm not asking 'given the current limitations, what is the least horrible approach', I'm suggesting that the actual features could change.

But hey - I'm the troll.

I see these options (bike shedding):

  • Foo.bar(anything here).case
  • Foo.bar.case // even if bar has associated values)
  • case(of: Foo.bar)

What type this case / discriminator is – is a good question. For example it could be Int and the test above could be:

[Foo.bar.case, Foo.baz.case].contains(value.case) // or
[case(of: Foo.bar), case(of: Foo.baz)].contains(case(of: value))
1 Like

I really like the .case syntax.

I just proposed it as an (imho!) cleaner solution for the is case expressions pitch

No need to create a new syntax for asking if two enums have the same base case - just give us access to a baseCase 'thing' which we can compare directly.

1 Like

I can definitively relate to that problem!

In some cases, you have absolutly no interest to know the current content of the associated value.

For a given enum

enum Life {
    case human(HumanDescription)
    case kitty(KittyDescription)
}

You currently can't make a check to know whether Life is .human or .kitty using a simple [.human, .kitty] even though it seems like a legit need.

Another example would be if you want to route some cases differently depending on their base case only. An object doing that might have no interest to check what is the associated value.

The .case.baseCase is interesting, but would it means that you have to have an array with [.human.baseCase, .kitty.baseCase] ? It's better than nothing but still looking a bit boilerplate imho :o
Having an Array<Life.BaseCase> = [.human, .kitty] would really look handy.

Also we can already check the base name of an enum using reflection, which means there already is some mechanism to get an "Equatable" representation of the base case? Or maybe I am takking some shortcuts here?

1 Like

@itMaxence do feel free to weigh in on the related pitch.

That's where there is an actual possibility of change...

Will do :+1: