Pausing with Binding’s dynamic member subscript

⇐ Notes archive

(This is an entry in my technical notebook. There will likely be typos, mistakes, or wider logical leaps — the intent here is to “let others look over my shoulder while I figure things out.”)

Roughly thirteen minutes into Point-Free episode #108 (timestamped), Stephen and Brandon showed how SwiftUI’s Binding type secretly has an implementation of map under the guise of its dynamic member subscript.

I didn’t fully realize the gravity of that statement until, well, a week later.

Binding is the first real-world functor I’ve used with two components — hidden by way of its .init(get:set:) initializer — of different variances. Value is in the covariant position on the getter and in the contravariant position on the setter. The only other two-component functors I’ve used — Either and tuples — are plain old bifunctors1. And to make it even more opaque, the dynamic member subscript only accepts one argument, a WritableKeyPath<Value, Subject>, that can somehow handle the two variances.

Below is me pausing with and unpacking how this works.

First, let’s imagine implementing Binding.map without a WritableKeypath for a closer look.

(Gist permalink.)

In implementing (1), we have access to self.wrappedValue and need to conjure a Subject instance. Which means that Binding.map needs to accept a (Value) -> Subject transformation. Check.

(Gist permalink.)

Onto (2).

We need to implement (Subject) -> Void again with only a Value instance in hand. That is, a way to mutate our wrapped value with a Subject instance — shaking out the second parameter: (inout Value, Subject) -> Void.

(Gist permalink.)

Now it’s more explicit that mapping a Binding<Value> to a Binding<Subject>, requires two transformations. One with Subject in the covariant position (get) and the other in the contravariant (set).

Which then raises the question, how does Apple pull this off with only a WritableKeyPath<Value, Subject>?

Turns out the Writable* bit is important.

Binding.map’s get parameter can be satisfied by the fact that any KeyPath from Value to Subject is equivalent to a (Value) -> Subject function (with the help of SE-0249).

How about set?

Can we convert a WritableKeyPath<Value, Subject> to an (inout Value, Subject) -> Void function?

…we can! Let’s call the conversion setterFromKeyPath:

(Gist permalink.)

This conversion is how WritableKeyPath packs the get-set punch needed to call Binding[dynamicMember:] an implementation of map on the type.

A whole lot of detail packed into those first thirteen minutes of the episode, eh?


  1. Either.bimap and the source for (,)’s Bifunctor instance on Hackage.