I'm trying to replicate Optional's ability to "flatten" switch statements in my own type but keep getting "Switch must be exhaustive".
For example, the following works fine …
enum CowDistance {
case smallAndCloseUp, largeAndFarAway
}
let value = Optional<CowDistance>.some(.smallAndCloseUp)
switch value {
case .smallAndCloseUp:
print("small and close up")
case .largeAndFarAway:
print("large and far away")
case .none:
break
// case .some(_): // ✅ Not needed
// break
}
… but using my own type fails, even with ~=
let value = Unknownable<CowDistance, Void>.some(.smallAndCloseUp)
switch value { // ❌ Switch must be exhaustive
case .smallAndCloseUp:
print("small and close up")
case .largeAndFarAway:
print("large and far away")
case .unknown:
break
// case .some(_): // ⚠️ This is still required
// break
}
enum Unknownable<Wrapped, Unknown> {
case some(Wrapped)
case unknown(Unknown)
}
extension Unknownable where Wrapped: Equatable { // ⚠️ Removing the Equatable has no effect on the error above
static func ~=(pattern: Wrapped, predicate: Self) -> Bool {
if case .some(let value) = predicate {
return value == pattern
} else {
return false
}
}
}
I think the fact of the matter is that exhaustivity checking simply doesn't know anything about custom ~=. It doesn't (and in the general case, can't) analyze the implementation, so for all it knows, you could've said
static func ~=(pattern: Wrapped, predicate: Self) -> Bool {
if case .some(let value) = predicate {
if someCondition(value) { return false } // <- this
return value == pattern
} else {
return false
}
}
in which case there is a version of .some which wouldn't match any of the cases you've already listed.
Yes, implicit Optional pattern-matching is special-cased in the compiler, as a counterpart to the implicit conversion of values to Optional (which also cannot be implemented on arbitrary types).
It's unfortunate we don't have an underscored attribute to say "I guarantee I'll correctly handle all Wrapped values". Especially as the Swift kinda implies it should work by accepting .smallAndCloseUp and .largeAndFarAway.
Leaving out these question marks is too magical and shouldn't have ever been allowed.
switch CowDistance.smallAndCloseUp as _? {
case .smallAndCloseUp?: print("small and close up")
case .largeAndFarAway?: print("large and far away")
case nil: break
}
Not allowing other types to define operators that do the same thing as ? is too magical and shouldn't have ever been allowed.
That said, I don't think explicit enum cases are that bad until nesting is involved. (I.e. value?? vs .some(.some(let value)).)
extension Unknownable: Equatable where Wrapped: Equatable, Unknown: Equatable { }
switch Unknownable<CowDistance, Void>.some(.smallAndCloseUp) {
case .some(.smallAndCloseUp): print("small and close up")
case .some(.largeAndFarAway): print("large and far away")
case .unknown: break
}