Generic Programming with Scala & Shapeless part 2
Last year I spent some time playing with,
and writing about, Scala & Shapeless - walking through the simple example of generating random test data for a case class.
Recently, I have played some more with Shapeless, this time with the goal of
generating React (javascript) components for case classes. It was a very similar exercise, but this time I made use of the LabelledGeneric object, so I could access the field names - so I thought I'd re-visit here and talk a bit about some of the internals of what is going on.
Getting started
As before, I had to define implicits for the simple types I wanted to be able to handle, and the starting point is of course accepting a case class as input.
So there are a few interesting things going on here:
First of all, the method is parameterised with two types:
caseClassToGenerator[A, Repr <: HList] A is simply going to be our case class type, and
Repr is going to be a Shapeless
HList.
Next up, we are expecting several implict method arguments (we will ignore the third implicit for now, that is an implicit that I am using purely for the react side of things - this can be skipped if the method handles everything itself):
implicit generic:
LabelledGeneric.
Aux[
A,
Repr],
gen:
ComponentGenerator[
Repr],
Now, as this method's purpose is to handle the input of a case class, and as we are using shapeless, we want to make sure that from that starting input we can transform it into a
HList so we can then start dealing with the fields one by one (in other words, this is the first step in converting a case class to a generic list that we can then handle element by element). In this setting, the second implicit argument is asking the compiler to check that we have also defined an appropriate
ComponentGenerator (this is my custom type for generating React components) that can handle the generic
HList representation (its no good being able to convert the case class to its generic representation, if we then have no means to actually process a generic
HList).
Straight forward so far?
The first implicit argument is a bit more interesting. Functionally, all
LabelledGeneric.
Aux[
A,
Repr] is doing is asking the compiler to make sure we have an implicit
LabelledGeneric instance that can handle converting between our parameter
A (the case class input type) and
Repr (the
HList representation). This implicit means that if we try to pass some type
A to this method, the compiler will check that we have a shapeless
LabelledGeneric that can handle it - if not, we will get a compile error.
But things get more interesting if we look more at what the .
Aux is doing!
Path dependent types & the Aux pattern
The best way to work out what is going on is to just jump into the Shapeless code and have a dig. I will proceed to use Generic as an example, as its a simpler case, but its the same for LabelledGeneric:
That's a lot simpler than I expected to find, to be honest, but as you can see from the above example, there are two types involved, there is the trait parameter
T and the inner type
Repr, and the Generic trait is just concerned with converting between these two types.
The inner type,
Repr, is what is called a path dependent type, in scala. That is, the type is dependent on the actual instance of the enclosing trait or class. This is a powerful mechanism in Scala (but one that can also catch you out, if you are in the habit of defining classes etc within other classes or traits). This is an important detail for our Generic here, as it could be given any parameter
T, so the corresponding
HList could be anything, but this makes sure it must match the given case class
T - that is, the
Repr is dependent on what
T is.
To try and get our head around it, let's take a look at an example:
Cool, so as we expected, we can see that in our Generic example, we can also see that the type
Repr has been defined matching the
HList representation of our case class. It makes sense that we want the transformed output HList to have its own, specific type (based on whatever input it was transforming), but it would be a real pain to have to actually define that as a type parameter in the class along with our case class type, so it uses this path-dependent type approach.
So, we still haven't got any closer to what this
Aux method is doing, so let's dig into that..
The Aux Pattern
We can see from our code that the
Aux method is taking two parameters, firstly
A - which is the parameter that our Generic will take, but the
Aux method also takes the parameter
Repr - which we know (or at least pretend we can guess) corresponds to the path dependent type that is defined nested inside the Generic trait.
The best way to work out what is going on from is to take a look at the shapeless code!
As we can see, the
Aux type (this is defined within the Generic object) is just an alias for a Generic[
T], where the inner path-dependent type is defined as
Repr - they have a pretty decent explanation of what is going on in the comments, so I will reproduce that here:
(that is abbreviated for the more relevant bits - they have even more detail in the comments that can be read).
That pretty nicely sums up the Aux pattern - the pattern allows us to essentially promote the result of a type-level computation to the higher level parameter. It can be used for a variety of things where we want to reason about the path dependent types, besides this, but this is a common use for the pattern.
So thats all I wanted to get into for now - you can
see the code here, and hopefully with this overview, and the earlier shapeless overview, you can get an understanding of what the LabelledGeneric stuff is doing and how Shapeless is helping me generate React components.
0 comments: