Type-safe query builders in Scala revisited: shapeless

Not so long ago, I wrote a post about creating type-safe query builders in Scala from scratch. In it, I also suggested using shapeless library to do what was described. Now, I decided to write how it could be done. The code is in this repository.

Problem reminder

Without going into much details, the problem was to provide a type-safe way to build queries (to an abstract database, for instance) with parameters of different types. Something like

shapeless and HList

There is a Scala library called shapeless, which purpose is to give a convenient way to do type-level generic programming. It has many interesting features, but we are going to use one – heterogeneous lists (HLists). HList is a list that contains elements of different types and handles them in a type-safe manner (i.e. without casting to Any).

Actually, we need to do computations like reversing and converting to a tuple on the type level, and it is possible with shapeless.

Reused parts

We are going to reuse pieces of code from the original post.
Empty tuple, Tuple0, which is derived from Product:

TypedParameter for demonstration:

The Query itself:

Scala automatically converts "str", 1, false into Tuple3("str", 1, false) when the only parameter of a function is Tuple3[String, Int, Boolean]. This allows us to write execute calls in a natural way, without an explicit tuple.

There are two exceptions: no parameters and one parameter. We create implicit conversions for these cases:

QueryBuilder

Here is the code:

Clarification is needed. A query is built by adding parameters to the current QueryBuilder. We store them both on the type level (in a form of HList in QueryBuilder generic parameter) and on the value level (in a form of sequence of TypedParameters). Initial QueryBuilder contains no parameters, i.e. HNil on the type level and Nil on the value level.

A more interesting part is final producing of Query instance (and type). There are two important moments. The first is that the natural way to grow HList is prepending (like in normal Scala List), so our parameter type list will be in the reverse order by the moment of call of build. Therefore, we must reverse the type list (on the type level).

The second moment is that Query works with tuples (actually, Product) and not HList, so we need to convert an HList to the correspondent tuple type. Additionally, default shapeless implementation converts empty HList (HNil) into Unit, and we need it to be converted into our special type Tuple0.

To achieve this, we will use implicit values of special Aux type, which will help the compiler to figure out the types in compile time. See an implicit parameter of build method: (implicit aux: Aux[L, P]). When build is called, type L is known (it is some HList), and the purpose of this implicit trick is to provide consistent advice to the compiler about the correspondent type P for this type L, which is the output type of the method.

For the empty list, we provide the evidence implicitAux0 of type Aux[HNil, Tuple0]. So, when the compiler sees that L is actually HNil, it knows that P is actually Tuple0.

It is more interesting when we need to deal with non-empty lists. To provide evidences for these cases, we use implicit parameters provided by shapeless. reverse: Reverse.Aux[L, RL] helps the compiler to find the type of a reversed list, and tupler: Tupler.Aux[RL, P] helps it to find the correspondent tuple type for this reversed list. So, implicitAux[L <: HList, RL <: HList, P <: Product] is a way to, given a known heterogeneous list type L, find the target product type P through some unknown reversed heterogeneous list type RL.

It should be said, that in client code (where we call build) it is not necessary to import this implicits to make the call work, because the compiler looks for the implicits in many places, including implicit parameter types scope (i.e. object QueryBuilder in this case).

That is all: now we can build queries and executed them in a type-safe way:

Conclusion

Thus we have implemented a type-safe query builders using shapeless, which helped to get rid of lots of boilerplate code. As I said in the previous post on this topic, in most cases, shapeless should be considered as a default choice for such problems.