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.