Search code examples
scalaslickshapelesshlist

Using Slick with shapeless HList


Slick's support for HList is generally a great thing. Unfortunately, it comes with its own implementation that does barely provide any useful operations. I'd therefore like to use the shapeless HList instead. This is supposed to be "trivial", but I have no idea how to get this right. Searching the web I found no evidence that somebody managed to accomplish this task.

I assume that it's enough to implement a ProvenShape (as advertised here), but since I fail to understand the concept of Slick's (Proven)Shapes, I did not manage implement this.

I'm basically aiming to boil this

class   Users( tag: Tag )
extends Table[Long :: String :: HNil]( tag, "users" )
{
    def id = column[Long]( "id", O.PrimaryKey, O.AutoInc )

    def email = column[String]( "email" )

    override def * = ( id, email ) <>[TableElementType, ( Long, String )](
        _.productElements,
        hlist => Some( hlist.tupled )
    )
}

down to

class   Users( tag: Tag )
extends Table[Long :: String :: HNil]( tag, "users" )
{
    def id = column[Long]( "id", O.PrimaryKey, O.AutoInc )

    def email = column[String]( "email" )

    override def * = id :: email :: HNil
}

Solution

  • You hit the nail on the head - if you can produce Shapes for HLists, the rest of Slick's machinery will kick into gear to produce the ProvenShape you need for the default projection.

    Here's a bare-bones implementation that allows you to create Tables of HLists:

    import scala.annotation.tailrec
    import scala.reflect.ClassTag
    import shapeless.{ HList, ::, HNil }
    import slick.lifted.{ Shape, ShapeLevel, MappedProductShape }
    
    final class HListShape[L <: ShapeLevel, M <: HList, U <: HList : ClassTag, P <: HList]
        (val shapes: Seq[Shape[_, _, _, _]]) extends MappedProductShape[L, HList, M, U, P] {
    
      def buildValue(elems: IndexedSeq[Any]) =
        elems.foldRight(HNil: HList)(_ :: _)
    
      def copy(shapes: Seq[Shape[_ <: ShapeLevel, _, _, _]]) =
        new HListShape(shapes)
    
      def classTag: ClassTag[U] = implicitly
    
      def runtimeList(value: HList): List[Any] = {
        @tailrec def loop(value: HList, acc: List[Any] = Nil): List[Any] = value match {
          case HNil     => acc
          case hd :: tl => loop(tl, hd :: acc)
        }
    
        loop(value).reverse
      }
    
      override def getIterator(value: HList): Iterator[Any] =
        runtimeList(value).iterator
    
      def getElement(value: HList, idx: Int): Any =
        runtimeList(value)(idx)
    }
    
    object HListShape {
      implicit def hnilShape[L <: ShapeLevel]: HListShape[L, HNil, HNil, HNil] =
        new HListShape[L, HNil, HNil, HNil](Nil)
    
      implicit def hconsShape[L <: ShapeLevel, M1, M2 <: HList, U1, U2 <: HList, P1, P2 <: HList]
          (implicit s1: Shape[_ <: ShapeLevel, M1, U1, P1], s2: HListShape[_ <: ShapeLevel, M2, U2, P2]):
          HListShape[L, M1 :: M2, U1 :: U2, P1 :: P2] =
        new HListShape[L, M1 :: M2, U1 :: U2, P1 :: P2](s1 +: s2.shapes)
    }
    

    I've put an implementation on Github here. In principle I think Generic could be brought into the fray to map case classes without the need for <>.