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...
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!
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
}
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 ==.
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
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.
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.
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)
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.
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