Swift 4 KeyPaths and You

8 minute read

Swift 4 is almost upon us, and I thought I would explore one of its features that I haven’t had the opportunity to that much: KeyPaths. There’s a lot of interesting nuance in here that I previously didn’t realize existed which I’d love to share.

In short, KeyPaths are a type-safe way to separate referencing a type’s property from evaluating that property and getting a result back. You can already do that today in Swift 3 with functions, but until Swift 4 is released you can’t do so with properties without wrapping them in a closure, or with using the old unsafe #keyPath() syntax if your code is running on ios/mac/etc:

struct Person {
  let name: String

  func greet() {
    print("Hello \(name)!")
  }
}

let p = Person(name: "Samus")
let greeter = p.greet // stores the method without evaluating it.
greeter() // calls the stored method

// this is the only way in Swift 3.1 and below to defer evaluating the name property of a Person.
let getName = { (p: Person) in p.name }
print(getName(p)) // evaluate the property

With Swift 4 you can rewrite the last two lines above like so:

let getName = \Person.name
print(p[keyPath: getName])

// or just this:
print(p[keyPath: \Person.name])

\Person.name is the way to construct a KeyPath<Person, String> where the first generic parameter is the root type (what type we are querying), and the second generic parameter is the type of the value we are asking for. Using this, we can ask for the value of the name property from any instance of Person, but without the overhead of defining a closure every time we might want to ask for it. You can query more than one level as well. If you wanted to get a KeyPath for the length of a person’s name, you could write \Person.name.count.

In this example we created a KeyPath which is read-only, because the name property on Person was defined as a let. What happens if we change it to a var?

struct Person {
  var name: String

  func greet() {
    print("Hello \(name)!")
  }
}

let kp = \Person.name
let p = Person(name: "Samus")
p[keyPath: kp] = "Ridley"
p.greet() // prints "Hello Ridley!"

In this case, since name is defined as a var, \Person.name is a WritableKeyPath<Person, String> which means we can use the subscript to set values in addition to getting them. If we tried to use the subscript setter for a let property we’d actually get a compiler error since settable subscripts are not defined for the plain KeyPath type, only WritableKeyPath which is a pretty nice way to make sure you can’t do something that isn’t possible.

There are more KeyPath types than just KeyPath and WritableKeyPath as well. If you look at the Swift Evolution proposal for KeyPaths or the generated interface for the KeyPath classes in Xcode, you’ll see that the KeyPath types are actually a 5-deep linear class hierarchy:

AnyKeyPath
    |
    v
PartialKeyPath<Root>
    |
    v
KeyPath<Root, Value>
    |
    v
WritableKeyPath<Root, Value>
    |
    v
ReferenceWritableKeyPath<Root, Value>

We went over the third and fourth type which are pretty easy to grok, but the others may not be. ReferenceWritableKeyPath is a subclass of WritableKeyPath which means that it can also be used both in a KeyPath getter and setter, but that’s the type of KeyPath you get if you referenced a mutable property on a class:

class BankAccount {
  var balance: Decimal = 0
  var owner: Person
}

// Creates a ReferenceWritableKeyPath<BankAccount, Decimal>:
let kp = \BankAccount.balance

ReferenceWritableKeyPath is necessary so that the compiler can know whether it’s safe to allow you to use a KeyPath setter on a value that’s stored in a constant. If that value is a class type, then mutating the value’s properties is possible in any context, therefore this KeyPath is the only one that can be used for it. If it was a struct or other value type stored in a constant, you would not be able to set something with the keyPath: subscript since that would change the value itself, which is not allowed for let constants.

Moving up the tree we have PartialKeyPath<Root>. This KeyPath has a concrete root, but an unknown value type at compile time. It could be useful in cases where you might want to store all of a type’s KeyPaths in a generic data structure or if you want to do other things that don’t rely on a KeyPath’s evaluated value, since the resulting value is returned as the Any type:

struct Person {
  var name: String
  let birthdate: Date
}

let kp: PartialKeyPath<Person>: \Person.name
type(of: person[keyPath: kp]) // returns Any

let personPaths: [PartialKeyPath<Person>] = [
    \Person.name,
    \Person.birthdate,
] // only possible with PartialKeyPath since name and birthdate are different types

Finally there’s the base class for everything, AnyKeyPath. Here we don’t know the type of the value OR the type of the root at compile time. These can be queried at runtime though if necessary:

let kp: AnyKeyPath = \Person.birthdate

kp.rootType // evaluates to Person.self
kp.valueType // evaluates to Date.self

These properties are available to all the subclasses as well, though they’re not very interesting once you get to KeyPath since you already have the compile time generic type parameters at that point.

Combining KeyPaths

There’s another cool thing you can do with KeyPaths: combining them together! Every KeyPath type has a number of appending methods that let you stick together two KeyPaths to make one mega-KeyPath:

let accountOwnerPath = \BankAccount.owner
let namePath = \Person.name

let accountOwnerNamePath = accountOwnerPath.appending(namePath) // returns KeyPath<BankAccount, String>

let account: BankAccount = ...
account[keyPath: accountOwnerNamePath] // returns the name of the account owner

You can only append two KeyPaths if the Value of the first one in the chain is of the same type as the Root of the second one in the chain. If you try to append two wholly-unrelated KeyPaths, you’ll get different behavior depending on which type of KeyPath you’re working with. If you’re working with KeyPath and its subclasses exclusively, the compiler can check your work and give you a compiler error if the types don’t match up nicely. However if you’re working with AnyKeyPath or PartialKeyPath, appending those together with any KeyPath type will result in returning an optional KeyPath, where you’ll get nil at runtime if the KeyPaths’ types don’t line up properly.

Combining KeyPaths has some other interesting behavior around how the types of the KeyPaths you combine affect the KeyPath you get back. For instance, appending a KeyPath and a WritableKeyPath gives you a read-only KeyPath since it’s not possible to mutate a property in the normal case either:

struct Person {
  let birthPlanet: Planet
}

struct Planet {
  var name: String
}

var person: Person = ...
person.birthPlanet.name = "SR388" // error: can't mutate birthPlanet.name
person[keyPath: \Person.birthPlanet.name] = "Zebes" // error for the same reason

When working with ReferenceWritableKeyPath though, it doesn’t always take the least strict version. If you append a ReferenceWritableKeyPath to the end of anything else (except AnyKeyPath), the result is a ReferenceWritableKeyPath since the KeyPath can safely mutate anything at the end of the chain if there’s reference semantics somewhere along the way. If you append a read-only KeyPath to the end of a ReferenceWritableKeyPath though, it’s still just a KeyPath since the tail end of the chain is still immutable.

The full table of ways you can combine KeyPaths and get different types back is below, for reference:

First Second Result
AnyKeyPath Anything AnyKeyPath?
PartialKeyPath AnyKeyPath or PartialKeyPath PartialKeyPath?
PartialKeyPath KeyPath or WritableKeyPath KeyPath?
PartialKeyPath ReferenceWritableKeyPath ReferenceWritableKeyPath?
KeyPath AnyKeyPath or PartialKeyPath 💥 Not possible 💥
KeyPath KeyPath or WritableKeyPath KeyPath
KeyPath ReferenceWritableKeyPath ReferenceWritableKeyPath
WritableKeyPath AnyKeyPath or PartialKeyPath 💥 Not possible 💥
WritableKeyPath KeyPath KeyPath
WritableKeyPath WritableKeyPath WritableKeyPath
WritableKeyPath ReferenceWritableKeyPath ReferenceWritableKeyPath
ReferenceWritableKeyPath AnyKeyPath or PartialKeyPath 💥 Not possible 💥
ReferenceWritableKeyPath KeyPath KeyPath
ReferenceWritableKeyPath WritableKeyPath or ReferenceWritableKeyPath ReferenceWritableKeyPath

You’ll notice that you can’t append KeyPath or its subclasses with AnyKeyPath or PartialKeyPath, unless you upcast the first KeyPath to one of those type erased variants too. I’m not sure why these weren’t included. My best guess is that the swift team didn’t want developers to accidentally move into a type-erased world when they didn’t intend to, and upcasting would force developers to consciously opt-in to that behavior.

And so much more!

Well, three things more. You can use optional chaining to model optional properties in the same way it works by directly referencing them:

struct Person {
  var address: Address?
}
struct Address {
  var fullAddress: String
}

let kp = \Person.address?.fullAddress // returns WritableKeyPath<Person, String?>

However I haven’t found a way to append KeyPaths with optionals somewhere in the chain. If a KeyPath ends in an optional Value, it’s unclear to me how you would append a KeyPath to that since you can’t (as far as I know) create a KeyPath that would lift its types up to an optional Root and Value directly. I expect explicit compiler or standard library support would need to be added to make this work.

You can also ostensibly use subscripting in KeyPaths:

struct Person {
  var previousAddresses: [Address]
}

let kp = \Person.previousAddresses[0].fullAddress // WritableKeyPath<Person, String>

However as of the swift snapshot in Xcode 9 beta 6, this isn’t working (with an actually useful error message saying that it’s not implemented yet).

There’s also ostensibly a way to use inferred types in key paths:

let p: Person = ...
p[keyPath: \.name] // \.name should evaluate to WritableKeyPath<Person, String>,
// referencing the name property on Person since we're calling it on an instance of Person

However this also appears to not be implemented yet (with a much less useful error message). I expect it will make it into Swift 4.1 if it’s too late for it to get into Swift 4.0 (as of this article’s publishing).

Conclusion

KeyPaths are a really cool feature that I haven’t heard discussed that much in the Swift developer community. I expect that more people will get excited about them once Swift 4 is released and people have more chances to play with them. There’s a lot of cool stuff in Foundation that utilizes the feature too that’s beyond the scope of this post (which I’ll probably get to in the future).

I hope this was informative, and that it inspires you to find useful or interesting ways to take advantage of this feature.