Writing Custom Pattern Matching in Swift
Pattern matching is available everywhere in Swift, and you have likely used it tons of times to deconstruct and bind values in things like switch
cases. While regular switch
cases are the most common use for patterns, Swift has several types of patterns which can be mixed and even used outside of switches
to result in really cool and short lines of code. One thing that interests me in particular is that pattern matching can be used for a wide variety of things.
At first glance, it's easy to see pattern matching as simple equality checks:
switch 80 {
case 100:
//
case 80:
//Matches, because 80 == 80
default:
break
}
Given that, you might think that something like case "eighty"
wouldn't compile, after all, "eighty"
and 80
aren't even the same type - and it's indeed what happens if you try it right now:
if case "eighty" = 80 {
//error: expression pattern of type 'String' cannot match values of type 'Int'
}
But that's not necessarily the case. When developing your projects, you might have noticed that some types have special interactions between each other, such as Ranges
and their associated types:
switch 80 {
case 0...20:
break
case 21...50:
break
case 51...100:
//Matches, because 80 is inside 51...100
default:
break
}
The reason for this is the ~=
pattern matching operator. This operator doesn't see much use in regular projects (you might have seen it before for this exact is number inside range example), but it is used a lot internally in Swift and it's exactly what's used to confirm case statements.
For most cases, the operator is a simple wrapper for an equality check (like the Int
example), but Range
has a special implementation for ~=
when used against its own associated type, allowing it to have this custom behaviour when pattern matching:
extension RangeExpression {
@inlinable
public static func ~= (pattern: Self, value: Bound) -> Bool {
return pattern.contains(value)
}
}
And since ~=
is available globally, you can overload it in order to write your own pattern matching logic!
To make "eighty"
match 80
, for example, all you need to do is add a version of the operator that matches String
patterns with Int
values:
func ~= (pattern: String, value: Int) -> Bool {
if pattern == "eighty" {
return value == 80
} else if pattern == "not eighty" {
return value != 80
} else {
return false
}
}
switch 80 {
case "eighty":
//Compiles and matches!
case "not eighty":
//
default:
break
}
Now let's say my app recieved a deep link in the shape of a path string and I need to decide which of my tabBar's UIViewControllers
this deep link belongs too - in the form of a AppTab
type:
enum AppTab: String {
case home
case orderHistory
case profile
}
let deepLink = DeepLink(path: "home", parameters: [:])
There are several ways to do this, including adding a correspondingTab
property to the deep link itself or subclassing the DeepLink
type, but with custom pattern matching, this attribution can be one-lined without having to touch the DeepLink
type!
func ~= (pattern: AppTab, value: DeepLink) -> Bool {
return value.path.hasPrefix(pattern.rawValue)
}
switch deepLink {
case .home:
homeViewController.handle(deepLink: deepLink)
case .orderHistory:
historyViewController.handle(deepLink: deepLink)
case .profile:
profileViewController.handle(deepLink: deepLink)
default:
break
}
This allows you to bypass having to map broader types into more specific ones, such as how an Int
can be mapped to a WeekDay
enum. In this case in particular, your backend would return you the week day either as an Int
or a String
, and with custom pattern matching you can make use of this broader type while still treating it like it was mapped to the more specific enum type. This can be useful when you want to try concepts without commiting to certain approachs or types:
enum WeekDay: Int {
case sunday
case monday
case tuesday
case wednesday
case thursday
case friday
case saturday
}
func ~= (pattern: WeekDay, value: Int) -> Bool {
return pattern.rawValue == value
}
// Server returns:
// { nextHoliday: { weekDay: 5 } }
if case .friday? = nextHoliday?.weekDay {
print("Woohoo!")
}
Creation of custom patterns is a simple way to write cleaner code without too much effort, as you can leverage cases
to jump straight to the point without having to add additional properties to your types - while making sure your code doesn't become harder to understand.