Traversing Object Graph with Shapeless (and Why You Should Write the Same Code Twice)
I’ve been writing DNS server in Scala for weeks now, on and off. Just my luck, my laptop died before I pushed the git repository to the remote server. Weighting my options, I decided it’d be easier to rewrite the whole thing than to restore the data; and since now I knew what I was facing — I could do things better.
One of the things I had to implement as part of creating a DNS server is to figure out how to serialize or deserialize the DNS packets sent over the network, and specifically each and every one of 79 existing record types¹.
In the old code, I had manually implemented serialize and deserialize methods for each record type and for parts of the data sent over the wire. This time, I thought, why not do it better?
Before reading this you should know how implicit resolution works. I recommend Implicit Design Patterns in Scala by Li Haoyi and The neophyte’s Guide to Scala Part 12: Type Classes.
I first encountered shapeless / “Generic programming for Scala” a few weeks ago, but I didn’t have time to figure out what they’re talking about. But writing the code again, I found the time — and was glad I did.
Let’s start by defining a heterogeneous list as a linked list that can contain different data types, and the data type of each item in the list can be identified by the compiler.
The first line imports a bunch of stuff from Shapeless — we’re going to use this in the examples that follow. in the third line I’ve created a heterogeneous list that contains integer and string. As you can see, the compiler identifies each element’s type:
::[Int, ::[String, HNil]]. You can also see that in the next line, the compiler understands the type of the expression
“Hello” :: a.
Converting Case Classes to Heterogeneous Lists vice versa
Shapeless provides a way to convert case classes to heterogeneous vice versa:
Traversing Heterogeneous Lists using Implicits
In my case, all I wanted is to serialize and deserialize objects, but let’s take it down a notch and write code that counts the number of objects I have in an Object Graph. The structure of our program is going to be :
The result should be 6. Recall that we support only case classes and for each type (/ trait) which isn’t a case class that we want to support, we have to implement
ObjectCounter[A]. Luckily, we can let the users implement it for their own types.
Let’s start by implementing
ObjectCounter[A] for heterogeneous lists. The following implicit methods are added to
object ObjectCounter, since we want the compiler to find them easily:
Our code already works with heterogeneous lists! The compiler is smart enough to choose the
hListCounter method over the
objectCounter method when the object is
To make our initial example code work, we have to use another trick called low priority default implicit². To do this, we’ll create a trait with implicits that will have a lower priority and then extend
object ObjectCounter with that trait. The compiler will then assign the implicits in the trait a lower priority:
I included the
objectCounter in the low priority implicits trait because I want
caseClassCounter to have priority over it. Otherwise, the compiler would throw an
ambiguous implicit values error.
Another thing I had to do is to use
shapeless.Lazy in the
caseClassCounter implicits. Without this, the compiler might throw
diverging implicit expansion error.
Finally, I notified the compiler what is the type of heterogeneous list returns for the case class counter, which allows it to find an object counter for that type of list. I used
Generic.Aux, which is what
Go ahead and run the example code, If you did everything right,
6 will be printed to the screen.
Using Shapeless you can write functions that don’t care about the underlying object type, which explains the library’s tagline — Generic programming for Scala.
Detaching emotionally from the code you write is a great way to learn and evolve your coding skills. I wish I had time to write each piece of code twice or more.