Jack Morris
Predicates from Key Paths in Swift 5.2
Feb 9, 2020
Swift

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"]

Predicates

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"]

Predicate Constructors

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 Predicates 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"]

~

Thanks for reading! I'd love to hear your feedback: I'm @AnotherJackM on Twitter, or you can contact me directly.