Alphabet Soup: Lensing, for freeJames PhillipsBlockedUnblockFollowFollowingApr 24In a previous blog post I introduced the scala library Alphabet Soup, for type-level transformations.
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?Data InjectionsWe 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 Payment and 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 A and B — that is, every atom in A that is not present in B.
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 PaymentResource.
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 A, and X provides the remainder.
Well, 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 mix is 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 MonadState[M, A].
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 stateA 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 A value.
I’ll leave the implementation out, but it is quite satisfying to write.
Data modificationsIt’s a small step from the definitions of mix and 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.
LensingThe 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 becomesThat’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.
DisclaimerWhile 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.
.. More details