How Swift keypaths let us write more natural code

Swift 4.0 introduced new keypath types as part of SE-0161, but it’s fair to say a lot of folks either don’t understand them or don’t yet understand the benefits they can deliver.

Keypaths are designed to allow you to refer to properties without actually invoking them – you hold a reference to the property itself, rather than reading its value. Their use in Swift is still evolving, and in many ways they are influenced by keypath support in Objective-C, but already some clear design patterns are emerging.

I cover a selection of uses for Swift keypaths in my book Swift Design Patterns, but here I want to demonstrate how invaluable they are as a way of letting us reference different types in a natural way.

I’ve purposefully made them completely different so you can see how this pattern works.

Both Person and Book have an identifier that is unique: socialSecurityNumber and isbn respectively. If we wanted to be able to work with identifiable objects in general, we could try to write a protocol like this:

protocol Identifiable {
var id: String { get set }
}

That would work well enough for Person and Book, but it would struggle for anything that didn’t store its identifier as a string – a WebPage might use a URL, and a File might use a UUID, for example.

Worse, it locks us into using id as the unique identifier for all our data, which isn’t a descriptive property name – you need to remember that id is actually a social security number for people and an ISBN for books.

Keypaths can help us solve this problem, allowing us to use them as adapters for very different data types – i.e., allow them to be treated the same even though they aren’t the same. The Gang of Four book has a conceptually similar approach called the adapter pattern: something that allows incompatible data types to be used together by wrapping an interface around them.

What we’re going to do is create an Identifiable protocol that uses an associated type (a hole in the protocol), then use that as a keypath. What this means is that every conforming type will be asked to provide a keypath that points towards whatever property identifies it uniquely – socialSecurityNumber and isbn for our two example structs.

WritableKeyPath is one of several variants of Swift's keypath types that let us store keypaths for later. In this case we’re saying that the keypath must refer to whichever type conforms to the protocol (Self) and it will have the same value as whatever is used to fill the ID hole in our protocol.

Now let’s update both Person and Book so they conform to the protocol. This means having adding Identifiable to their list of conformances, then defining idKey to point to whichever of their properties is their unique identifier:

Swift’s type inference is extremely clever here. When it looks at Person it will:

Remember socialSecurityNumber is a String.

Store that idKey points to Person.socialSecurityNumber, which is a string.

Match idKey in Person with the same property in Identifiable.

Resolve WritableKeyPath<Self, ID> to WritableKeyPath<Self, String>.

Understand that associatedtype ID is a hole that is being filled by a string.

I know that Swift code can sometimes take a long time to compile, but you have to admit it’s pretty darn amazing.

What we’ve achieved here is that totally disparate data types – structs that are designed to store properties in whichever way works best for them rather than following some arbitrary names imposed by a protocol – are able to be used together. All we care about is that they conform to Identifiable: once we know that we also know it has an idKey keypath that points to where its identifying property is.

Putting all this together we can print the identifier of any Identifiable type like this:

Yes, that code leverages generics, keypaths, and associated types all in one, and with surprisingly little code – Swift is an extremely powerful language when you really lean on it. The end result is that we’ve been able to separate our architecture from implementation details – we’ve let types be expressed naturally, then used protocols to overlay an adapter on top to allow those types to be used together.

This was an excerpt from my book Swift Design Patterns – if you'd like to learn more about keypaths, along with delegation, protocols, associative storage, and more, you should check it out!