Pausing with Binding’s dynamic member subscript
19 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.
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.
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
.
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
:
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?
-
Either.bimap
and the source for(,)
’sBifunctor
instance on Hackage. ↩