Using slickless with Plain SQL

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:

  1. Slick provides GetResult[T] instances for basic types, such as String and Long;
  2. Rather than use r.nextLong and r.nextString we can use r.<<[T], which Slick provides for the Ts it can get the next value of; and
  3. 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.


Like what you're reading?

Join our newsletter


Comments

We encourage discussion of our blog posts on our Gitter channel. Please review our Community Guidelines before posting there. We encourage discussion in good faith, but do not allow combative, exclusionary, or harassing behaviour. If you have any questions, contact us!