Keypaths as closures

3 minute read

Swift 4.0 added support for strongly typed key paths. A KeyPath<A, B> is a type that defines a path from a root type A, through zero or more hoops, to a resulting type B through a series of named accessors. It’s a way of accessing a type’s properties indirectly.

It was always possible to store a reference to a function, and call it later, creating a distinction between the function reference and the function invocation. However, for properties this was not possible before Swift 4.

let x = foo.bar   // get a reference to the function that you can pass around
x()               // call the function

let y = Foo.bar   // get a reference to the class function
x == y(foo)       // true
y(foo)()          // invoke the function on foo

let z = foo.bar() // invoke the function directly

Now, with Swift 4, we can get a similar reference through a KeyPath, which we can evaluate later:

let x = \Foo.qux  // get a key path from a Foo to the property qux
foo[keyPath: x] == foo.qux // true

But while function references can implicitly be used as closures, we cannot do the same with key paths, even though they are both something that takes an A and produces a B.

func isEven(_ i: Int) -> Bool {
  return i % 2 == 0
}
extension Int {
  var isEven: Bool {
    return self % 2 == 0
  }
}

let numbers = [1, 4, 2, 3, 5, 7, 5, 6]
let even1 = numbers.filter(isEven)      // this works: [4, 2, 6]
let even2 = numbers.filter(\Int.isEven) // does not compile!

It is often the case that I want to filter or transform collections of values based on a single property of those values:

let names = people.map(\.name)
let valid = input.filter(\.isValid)

But we can fix this. Enter the ^ prefix operator

prefix operator ^
prefix func ^<A, B> (operand: KeyPath<A, B>) -> (A) -> B {
  return { $0[keyPath: operand] }
}

let keyPath = \Person.name // KeyPath<Person, String>
let closure = ^keyPath     // (Person) -> String

let names = people.map(\.name)  // does not compile
let names = people.map(^\.name) // ["Ben", "Jerry", "Adam"]

Tags:

Updated:

Leave a Comment