What else can the Alphabet soup library do?
Senior Scala Developer, James Phillips shows us how we can implement data injections, data modifications as well as lensing, for free! You really want to read up on this one.
'In 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:
1 // @Atomic is new syntax since the last blog post! It generates atoms for you automatically
2 @Atomic trait A; @Atomic trait B; @Atomic trait C; @Atomic trait D; @Atomic trait E; @Atomic trait F; @Atomic trait G
3
4 type Source = (A, B, C, D, E, F, G)
5 type Target = (((B, F), C), (D, A))
6
7 val source: Source = ???
8 val result: Target = Mixer[Source, Target].mix(source)
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 Injections
We saw in the previous blog post that a particular use-case might be centred around manipulating similar case classes into one another.
E.g.,
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 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 A value.
I’ll leave the implementation out, but it is quite satisfying to write.
Data modifications
It’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.
Lensing
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:
1 import monocle.Lens
2 import monocle.macros.GenLens
3
4 val company : Lens[Employee, Company] = GenLens[Employee](_.company)
5 val address : Lens[Company , Address] = GenLens[Company](_.address)
6 val street : Lens[Address , Street] = GenLens[Address](_.street)
7 val streetName: Lens[Street , String] = GenLens[Street](_.name)
8
9 (company composeLens address composeLens street composeLens streetName).modify(_.capitalize)(employee)
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.
Disclaimer
This article was written by James Phillips and posted originally on Medium.