Pausing with Binding’s dynamic member subscript19 Jul 2020 ⇐ 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.
(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.
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.
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 (
Which then raises the question, how does Apple pull this off with only a
Turns out the
Writable* bit is important.
get parameter can be satisfied by the fact that any
Subject is equivalent to a
(Value) -> Subject function (with the help of SE-0249).
Can we convert a
WritableKeyPath<Value, Subject> to an
(inout Value, Subject) -> Void function?
…we can! Let’s call the conversion
This conversion is how
WritableKeyPath packs the
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?