Switching over enums with associated types is the best. If bees had knees? Aforementioned knees would–most likely–involve switches and enums with associated types. A frustrating issue can arise, however, when you believe too completely in the power of this combination.
Motivation
I love the number 9 and want to shout to the heavens whenever it is input in whatever form. We will make an enum Number to represent numbers.
public enum Number : IntegerLiteralConvertible {
case integer(Int)
case floatingPoint(Double)
public init(integerLiteral value: Int) {
self = .integer(value)
}
}
Excellent. Let us now switch over a user provided value to look for the number 9.
let value: Number = .floatingPoint(9)
switch value {
case .integer(9):
print("hooraaaay!!!")
default:
print("I feel nothing. Nothing at all.")
}
// outputs "I feel nothing. Nothing at all."
Problem
I do not receive the output that I expect when the user provides 9 as a floating point number. This is an unending source of woe as I want to give love to the 9's equally.
Solution
You might suggest that I just write an overload for equals and convert as appropriate and that is a workable solution for certain situations but what if this enum was the payload of another enum? == isn't called then. After much public bemoaning on the internet, I remembered our oft-forgotten helper ~=. Let us implement the pattern match operator ~= and change our switch just a little.
let value: Number = .floatingPoint(9.0)
public func ~=(pattern: Int, value: Number) -> Bool {
switch value {
case let .integer(v):
return pattern == v
case let .floatingPoint(v):
return pattern == Int(v)
}
}
switch value {
case 4:
print("success!")
default:
print("try again")
}
et voila! In this way, we get union-like behavior when we want it.
Caveat Emptor
1. Overrides
The problem with overriding ~= and/or == with this casting behavior, of course, is that Double and Int are two different representations of numbers which do not always convert in the ways that you would expect. I do not, at present, have a fix for that madness.
2. Double != Int
Be careful when trying to add any of this to a protocol. I tried to make Double conform to to this protocol
public protocol NumberLiteralMatchable : FloatLiteralConvertible, IntegerLiteralConvertible {
func ~=(pattern: IntegerLiteralType, value: Self) -> Bool
func ~=(pattern: FloatLiteralType, value: Self) -> Bool
}
and was met with strife and hardship.
3. No, Really, Overrides
I thought that I could avoid mentioning the interaction with (Float|Integer)LiteralConvertible that can occur for conforming structs. I was incorrect. I was terribly incorrect. If you use this strategy for a struct, understand that == will no longer be used when you switch and provide a literal for the case.