The first Xcode 11.4 beta was released on Thursday, along with a bundled version of Swift 5.2. There are a number of changes in Swift 5.2, however the ability to use key path expressions in the place of certain functions (SE-0249) allows for some interesting constructions.
So what is this change? In short, wherever you can use (T) -> V
, you can also now use the key path expression \T.value
.
struct Recipe {
var name: String
var isVegetarian: Bool
var isAttempted: Bool
var difficulty: Int
}
let recipes = [
Recipe(
name: "Omelette",
isVegetarian: true,
isAttempted: true,
difficulty: 2),
Recipe(
name: "Chicken Sandwich",
isVegetarian: false,
isAttempted: false,
difficulty: 1),
Recipe(
name: "Risotto",
isVegetarian: true,
isAttempted: false,
difficulty: 4),
]
// Prior approach.
recipes.filter { $0.isVegetarian }.map { $0.name }
// ["Omelette", "Risotto"]
// Also supported, as of Swift 5.2 ๐.
// Rather than using a closure that returns a property, we can
// pass the key path for that property directly.
recipes.filter(\.isVegetarian).map(\.name)
// ["Omelette", "Risotto"]
We've already seen how we can use this new style with filter
. In this case, the \.isAuthorized
key path acts as the predicate, signalling which elements should be retained in the filtered output.
Generalising this, we can think of predicates as functions that take an element, and return true
if the element should be matched against.
typealias Predicate<T> = (T) -> Bool
What about composition? It'd be pretty nice to be able to create a predicate that matches elements matched by both, or either, of some other collection of predicates. Likewise, negating a predicate (so that it matches all items not matched by some other predicate) sounds quite useful. We can overload the &&
, ||
and !
operators for just that purpose.
func &&<T> (
left: @escaping Predicate<T>,
right: @escaping Predicate<T>
) -> Predicate<T> {
return { left($0) && right($0) }
}
func ||<T> (
left: @escaping Predicate<T>,
right: @escaping Predicate<T>
) -> Predicate<T> {
return { left($0) || right($0) }
}
prefix func !<T> (
predicate: @escaping Predicate<T>
) -> Predicate<T> {
return { !predicate($0) }
}
Let's see this composition at work when using filter
.
recipes.filter(
{ $0.isVegetarian } && !{ $0.isAttempted }
).map(\.name)
// ["Risotto"]
And, thanks to SE-0249, we can replace these inline predicate closures directly with key path expressions, leading to readable, compact, and type-safe, predicate expressions.
recipes.filter(\.isVegetarian && !\.isAttempted).map(\.name)
// ["Risotto"]
Can we take this further? Composing predicates is nice, but there's only so far you can get with &&
-ing, ||
-ing and !
-ing boolean properties. What about constructing comparison predicates?
We can add ==
, <
, and >
operator overloads to construct Predicate
s directly from a KeyPath
, and some element to compare against.
func ==<V, T: Equatable> (
left: KeyPath<V, T>,
right: T
) -> Predicate<V> {
return { $0[keyPath: left] == right }
}
func <<V, T: Comparable> (
left: KeyPath<V, T>,
right: T
) -> Predicate<V> {
return { $0[keyPath: left] < right }
}
func ><V, T: Comparable> (
left: KeyPath<V, T>,
right: T
) -> Predicate<V> {
return { $0[keyPath: left] > right }
}
This then allows us to construct more complex predicates, without an inline closure in sight.
recipes.filter(\.isAttempted && \.difficulty < 2).map(\.name)
// ["Chicken Sandwich"]