Intro
shapeless is a library for type class and dependent type-based programming in Scala. I first became interested in it when I watched a presentation by Dave Gurnell called The Type Astronaut’s Guide to Shapeless.
In the video, he discusses treating Scala case classes generically,
as lists parameterized by the type of each of its members.
Let’s say you
wanted to store both a String and an Int in a list, you could use
use the most recent ancestor type as the generic type of a List[Any].
Another way to represent it would be to use a List[Either[String,
Int]], but this doesn’t scale for an arbitrary number of types.
Instead, what you might do, is represent the list using a nested
2-tuple. An example:
The expression has the type
Shapless basically does this, but defines a :: class (similar to how
Seq is defined) and a HNil object to denote the end of the list. It
also provides macro-based methods to convert case classes to this
representation, and a host of other useful features.
Although I found this presentation very interesting, I didn’t have much of an interest in taking my understanding further until I encountered the project julienrf/play-json-derived-codecs.
If you have ever used
Play Framework,
you have probably used the its json implementation. One of the nice
things you can do, is to automatically create a JSON formatter
(play.api.libs.json.Format) for a
case class at compile time. Play uses a macro to do this.
It’s really convenient until you want to create a Format for all the
case classes which extend a sealed trait.
For example, you may define a Drawing like below, defining a Group
recursively as a Drawing which contains other Drawings.
Implementing a Format for this is more tedious than you would expect.
It is something like:
Ignoring the problem that we do not encode type information anywhere in the JSON, this is all tedious, repetitive boilerplate. There must be a better way!
Enter play-json-derived-codecs, which you use as you’d expect:
That’s it! What is this black magic?
How it works
Although the code to implement play-json-derived-codecs is really
short, I had to read a whole book to figure out how it works. Dave
Gurnell actually worked on a whole book on Shapeless called
The Type Astronaut’s Guide to Shapeless.
This book is invaluable to understanding how things work, and explains
almost exactly how Shapeless is used in play-json-derived-codecs. In
case you’d rather not read the book yourself, continue reading to
discover how it works.
A Format is actually a combination of a Reads and a Writes. For
the sake of brevity, I will describe how the Writes works, as creating
a Reads is similar.
The first important thing to know about the way Play’s JSON library
works is that to write a value as a JsValue, we must have an implicit
Writes for that type available. Some examples follow.
It is simple to create a Writes for a case class that uses only simple
types because Writes for all of these simple types have already been
defined.
And creating a Writes[Hierarchy] is simple once we have defined a
Writes[Employee]. We’d get an error about a missing Writes[Employee]
if we hadn’t defined it above.
This is important to know because we can see Writes are usually built
recusively. Here is our strategy to build something which creates
Writes for sealed traits and their subclasses.
- Convert a case class to its generic representation
- Convert a generic representation (
HList) to aJsValue - Somehow handle the relationship between traits and subclasses
Generic
To convert a case class to its generic representation, we use the
LabelledGeneric class. This will encode not only the types of each
parameter but their names as well, which is required to create
JsObjects.
Calling LabelledGeneric[Employee].to, we get back an HList whose
first value is "Jamie", a String tagged with the field name “name”.
It is followed by the value 1, which is an Int tagged with the field
name “age”.
Let’s see if we can create an OWrites for something like this. Below,
an OWrites is like a Writes but always writes things as a JsObject
instead of any JsValue.
When we encounter an HNil, we will return an empty object.
HNil is the base case. For the other cases, we need to process the
HList as a head and tail.
And this is the definition of FieldType from Shapeless:
Hopefully this definition is straightforward, but we are missing various writes and the field name. How do we get these? We can get them through implicit parameters that shapeless will provide. Using shapeless, the code above becomes:
Witness is a class which allows us to retrieve the field names used in
a LabelledGeneric’s KeyTags. Lazy is a class which makes it easier for the
compiler to work with recursively-defined implicits.
We can use it right now if we are willing to convert Employees to
HLists manually.
But why do that when we can do it automatically?
We went right to the finalrimplementation, but hopefully it makes sense given
the previous one. First, we need a way to convert an Employee to its
HList representation. To do this, we need a LabelledGeneric. Then, we
need an OWrites for HLists. We just created that!
We’ve basically just replicated exactly what Json.writes[Employee]
will do, but in a more complicated way. Why? Remember that we want to be
able to automatically create a Writes for a sealed trait and its
implementations.
Shapeless represents a sealed trait and its implementors as a
Coproduct.
Recall our definition of a Drawing:
We can get a LabelledGeneric representation of a Rectangle:
So we just need an OWrites for a Coproduct. We will recursively
write the list until we reach an Inl, at which point we know we have
the value we need, and can write that, along with type information.
And here is the code with all of the shapeless boilerplate added in.
We also need to define the base case, a OWrites[CNil]. Since we
shouldn’t ever have a CNil, we throw an exception if we reach it.
Having done that, we can now write a Rectangle:
And a Group:
We are pretty much done here. You’ll notice that play-json-derived-codecs is
a little more complicated. Mostly, this is because it allows you to
specify NameAdapters which control naming of case class fields, and a
TypeTagOWrites which allows you to customize how the type parameter
is manifested in the JSON.
Conclusion
Above, we were introduced to the dependent type and generic programming
Scala library Shapeless. We learned about how Play Framework represents
JSON, and how we can use Writes to write types to JsValues. Finally,
we learned how to use Shapeless to automatically create Writes for
sealed traits and their subclasses, just like
julienrf/play-json-derived-codecs does.
Here’s a Scalafiddle showing the code that we worked on above. Press the Run button to see the results.