Alphabet Soup: Lensing, for free
It provides a mapping between arbitrarily nested types, provided the compiler can calculate a path between them, all powered with gratitude by
Generic from shapeless.
A quick example:
Target here could be any combination of the atomic source types, in any order and in any nesting. It works in exactly the same way for case classes and hlists, in both source and target position, too.
What else can it do?
We saw in the previous blog post that a particular use-case might be centred around manipulating similar case classes into one another.
We want to create a
PaymentResource and we have a
Payment and a
User, which are completely unrelated. But because alphabet soup is entirely type based and not name based — truly shapeless data — all we have to do is summon the mixer
Mixer[(Payment, User), PaymentResource] and provide a tuple of our instances. It’s simple, and easy.
— — Small Disclaimer — —
There is a caveat while doing this of course. One of the tenets of alphabet soup is that everything should be a unique type. Why should a
User.Age be able to be used in place of a
Pet.Age? In principle, it shouldn’t.
But that means if you do have a repeated type in
User then the value from
Payment will be chosen, because it is in the first part of the tuple and the first encountered in the type-level searching algorithm. If you need special logic around which duplicate-typed value gets used, then you have a genuine business case which requires you to map things explicitly. If you don’t care, sit back and let alphabet soup do the work for you.
— — End Disclaimer — —
So the tuple trick is handy. You can just chuck your data into a bucket and alphabet soup will work it out for you.
Now, let’s try and consider what the inverse of a
mix operation might be.
mix goes from a larger type
A to a smaller type
B. The inverse of such an operation isn’t strictly possible —
B => A isn’t expressible without more information on the left.
What extra information do we need on the left? We need the difference between
B — that is, every atom in
A that is not present in
That’s not quite possible. What do you call the type
A minus B? It’s not something that exists, it’s a conceptual type. It doesn’t have a name like
Looking at the problem another way, we know from above that if there are repeated atoms between a tuple’s arguments, then the first one gets chosen.
So we need a map
(B, X) => A for some
X, where every atom in
B makes its way into
X provides the remainder.
X = A will do:
(B, A) => A. For every atom in
A, we look on the left. If it is in
B we take it. If it’s not, we take it from the
X instance instead.
So the opposite of
def inject(b: B, a: A): A — and this is provided for you in
Mixer[A, B] too. And it makes sense as the inverse of
mix: if you’re undoing a
mix operation, it means you had an
A to begin with to operate on.
Let’s have a look at an example:
This is a toy example, of course. It can be useful for very complex structures you can’t be bothered to write out manually, but also for completely generic structures, such as
In the previous blog post recall I showed how an
ApplicativeAsk[M, B] could be derived from an
ApplicativeAsk[M, A] given a
Mixer[A, B]: Well, now with
inject we can also derive a
MonadState[M, B] from a
MonadState[M, A] — we project our state
A down to
B, run the sub-mutations (
set(b: B) and/or
modify(f: B => B)) and then inject the result back into our parent
I’ll leave the implementation out, but it is quite satisfying to write.
It’s a small step from the definitions of
inject to wonder if we could instead pass a function
B => B into our
A, rather than a mutated
B value. That is, run the mutation ‘in place’.
It’s very easy to define:
And now, harking back to our
Norman example we can do the following:
And we’ve just made Norman older.
The above is cool, but doesn’t immediately reveal the full power behind the idea. What we’ve just done is zoomed in to the
Age value within
Person and modified it, with no boilerplate or fiddly code. That’s a lens operation.
Let’s indulge ourselves briefly and define some nicer syntax:
Now our above example becomes
That’s pretty powerful, and minimal. Let’s have a look at a more complex example, taken from Monacle’s documentation (Monacle being one of the go-to scala lensing libraries).
We have our data tree:
Monacle’s example is that we have an employee, and we want to upper-case the first letter of his street name. In vanilla scala it would look like this:
Hideous. We actually have one more layer than the monacle example due to our value classes! But we’ve hidden it by adding ‘capitalize’ to our value class directly.
This is what Monacle’s solution looks like:
You can see that while we have the
Atom overhead, lensing libraries produce path-definition overhead, where you must generate the (reusable) lenses between the structures.
Now, in our new indulgent syntax, our lensing example becomes:
which is pretty amazing, and literally could not be simpler. Every piece of calculation is devolved to the compiler, thanks to the principle of having unique types describing our tree. (All calculation, that is, except the
capitalize on our value class. Since that resides inside an
Atom we must implement that ourselves).
The example also works with chaining modifications, obviously:
And as a final bonus, our lensing works on any sub-tree we could imagine. A quick contrived example where we want to clean some data and not others:
Alphabet soup de-structures
CleanableData for you, extracts each element from
employee, restructures it into
CleanableData, cleans it and then injects it back in to
employee — all automatically. The tree of
CleanableData can be completely arbitrary, as long as it is made up of atoms of
Employee. You don’t need to know or care how the atoms are structured or where they live; truly shapeless programming.
While the above lensing capabilities are very cool, our syntax comes with very large overhead. Generating a
Mixer every time we call
modify is not cheap. Frankly it’s decadent. Planning is underway to make such a syntax cheaper to implement (pull requests welcome!), but for right now the above is an academic example of the power of type-level programming.