Expression Patterns in Swift
If you’ve ever written Swift code, you’ve encountered expression patterns hundreds of times, even if you didn’t notice it.
An expression pattern is what appears after the keyword case in a switch statement. The official documentation refers to expression patterns as:
The value of an expression
When we write a switch statement like this:
switch integerValue {
case 0:
print("The value is zero")
case 1:
print("The value is one")
default:
break
}
what happens under the hood is that Swift calls a special comparison function, similar to the == function used for equality. This function is called ~=.
In fact, by default the ~= function runs the == function to determine if the value we are switching and a given case are equal. The function Swift calls for the example above would be something like this:
func ~= (pattern: Int, value: Int) -> Bool {
return pattern == value
}
where pattern is what appears after the case label, and value is the current value we are switching on.
The fun part
Now that we know how switch statements work, how could this help us? The real power of the ~= function is that it can be overloaded to suit our needs.
Let’s explore some examples:
Regex
If we define an overload of ~= as follows:
func ~=<RegexOutput>(pattern: any RegexComponent<RegexOutput>, value: String) -> Bool {
!value.matches(of: pattern).isEmpty
}
we unlock the power of Swift 5.7’s regular expressions to be used inside a switch statement like in the following example:
switch "user@email.com" {
case /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/:
logger.debug("Valid email")
default:
logger.debug("Invalid email")
}
// The code logs "Valid email"
Flexibility is the key
We can go even further than that. Let’s consider this User struct:
struct User {
enum Role {
case admin
case standard
}
let name: String
let role: Role
let lastAccess: Date? = nil
}
we can create a custom type and define an overload of ~= that uses that type. It doesn’t even need to be a new type.
Here I define just a typealias:
typealias Predicate<Root> = (Root) -> Bool
func ~=<Root>(pattern: Predicate<Root>, value: Root) -> Bool {
pattern(value)
}
we can then define our predicates based on what we’re interested in:
let recentlyLogged: Predicate<User> = { user in
guard let lastAccess = user.lastAccess else { return false }
return lastAccess > .now.addingTimeInterval(-3600)
}
let isAdmin: Predicate<User> = { $0.role == .admin }
and end up with a clear and self-explanatory switch statement:
switch user {
case recentlyLogged:
logger.debug("User logged recently")
case isAdmin:
logger.debug("User is admin")
default:
break
}
My 2 cents
I often declare my ~= overrides as fileprivate methods wherever I need them, to avoid polluting my application with unnecessary functions.