With shapeless HList
support available for Slick, we can make further use of shapeless to reduce boilerplate code. This post explores how to do this for the Slick GetResult
type class.
Plain SQL
Slick allows you to write plain SQL queries. They look like this:
val action =
sql""" SELECT "id", "email" FROM "users" """.as[Long :: String :: HNil]
Here we are using a shapeless HList
to represent a row as Long :: String :: HNil
. We can do that via the slickless library, which was introduced in a recent post.
When we run this query we’ll get back a Seq[Long :: String :: HNil]
. But there’s a catch: we need to provide Slick with an instance of GetResult[Long :: String :: HNil]
. This tells Slick how to map from our query columns (“id” and “email”) into that HList
structure.
Generating GetResult
for an HList
It’s pretty easy to create such a type class instance by hand. Here’s one that will work:
import slick.jdbc.{ GetResult, PositionedResult }
implicit val longStringGetResult =
new GetResult[Long :: String :: HNil] {
def apply(r: PositionedResult) =
r.nextLong :: r.nextString :: HNil
}
The PositionedResult
is a value Slick gives us to read column values.
Using it is easy, and there’s a shorter GetResult.apply
-style, but it’s still tedious. Thankfully shapeless can automate this for us. (This is not an original idea: I saw this, or something like it, first suggested by Sam Halliday).
To get this working we can make use of three facts:
- Slick provides
GetResult[T]
instances for basic types, such asString
andLong
; - Rather than use
r.nextLong
andr.nextString
we can user.<<[T]
, which Slick provides for theT
s it can get the next value of; and - We can recurse on the
HList
types, just as you would recurse on the head and tail of a regular list.
Let’s do it. We’re going to need a method like this:
// Incomplete: won't compile
implicit def hlistConsGetResult[HList] =
new GetResult[HList] {
def apply(r: PositionedResult) = ???
}
That is, we want an implicit method that the compiler can use to create a GetResult
for any HList
.
To fill this in we note that an HList
is made up of a head and a tail:
implicit def hlistConsGetResult[H, T <: HList]
new GetResult[H :: T] {
def apply(r: PositionedResult) = ???
}
We need to be able to get to a GetResult
instance for the head and tail types, and then use them to build up an HList
value. We can ask the compiler to find those implicitly for us, because we know Slick provides GetResult
instances for the basic types.
implicit def hlistConsGetResult[H, T <: HList]
(implicit
h: GetResult[H],
t: GetResult[T]
) =
new GetResult[H :: T] {
def apply(r: PositionedResult) = ???
}
The final step is to fill in the apply
method and call the implicit GetResult
for the head element, and then recurse on the tail:
implicit def hlistConsGetResult[H, T <: HList]
(implicit
h: GetResult[H],
t: GetResult[T]
) =
new GetResult[H :: T] {
def apply(r: PositionedResult) = (r << h) :: t(r)
}
Notice how the apply
method makes use of r
to create a value, then recurses on the tail of the HList
. We need a stopping condition for this, which is a GetResult[HNil]
:
implicit object hnilGetResult extends GetResult[HNil] {
def apply(r: PositionedResult) = HNil
}
These two implicits will generate a GetResult
for any Hlist
without us having to write it by hand.
Conclusions
By using a shapeless HList
as a row representation we can start to make use of the other funky features in shapeless. We’ve seen an example of creating GetResult
instances without having to implement them by hand. You can find this example on Github.
Once we’ve kicked the tires on this some more, we may roll this into a release of Slickless.