view raw
Dylan Dylan - 7 months ago 47
Scala Question

Using Shapeless to implement an (HList of Function, Input) => Tuple typeclass

I've got some existing code along the lines of

trait Field[T]
object Fields {
case object Id extends Field[Int]
case object Name extends Field[String]
// ... and so on

// basically just a Map[Field[_], Any]
class QueryResultData {
def apply[T](field: Field[T]): T

def query(fields: Set[Field]): QueryMonad[QueryResultData]

So for example if I want to query the Id and Name data, I need to do something like:

val idsAndNames = for {
results <- query(Set(Fields.Id, Fields.Name))
} yield {
val id = results(Fields.Id)
val name = results(Fields.Name)
(id, name)

Having to manually extract each field's result is tedious, especially when the query includes more fields. What I'd like to be able to do is:

val idsAndNames: QueryMonad[(Int, String)] = query(Fields.Id -> Fields.Name)

And have some kind of typeclass handle the
val id = ...
part and reconstruct the tuple for me, e.g.

def query[Fields <: HList, Tuple](fields: Fields)
(implicit extractor: Extractor[Fields, T])
: QueryMonad[T]

How can I implement the
typeclass so that I don't have to manually extract results?

What I've Tried

I figured this was a job for Shapeless, as the
method is meant to work on any number of fields, and is expected to give me back an appropriate tuple.

I defined a

class FieldExtractor[T](field: Field[T]) {
def apply(results: QueryResultData): T = results(field)

and a polymorphic function for Field to FieldExtractor:

object makeFieldExtractor extends (Field ~> FieldExtractor) {
def apply[T](field: Field[T]) = new FieldExtractor[T]

and for simplicity's sake I'll start by dealing with HLists instead of Tuples:

val someFields = Fields.Id :: Fields.Name :: Fields.OtherStuff :: HNil

I tried using my
to convert
. This is where I started running into trouble.

val someFieldExtractors =

error: could not find implicit value for parameter mapper: shapeless.ops.hlist.Mapper[MakeFieldExtractor.type,shapeless.::[Fields.Id.type,shapeless.::[Fields.Name.type,shapeless.::[Fields.OtherStuff.type,shapeless.HNil]]]]

It seems like the problem is that it's seeing types like
when it probably should be seeing
. If I explicitly specify the field types for
, the map works, but I don't want client code to have to do that. The compiler should do that for me. And let's assume I can't just change the
definitions to a
instead of a
case object

I found but didn't manage to make any successful use of it.


Here is how I would done it.

import shapeless.{::, HList, HNil}
import Field._

trait Field[A]
object Field {
  case object IntField extends Field[Int]
  case object StringField extends Field[String]
  // Here is a little trick to proof that for any T that
  // happened to be a subclass of Field[A] the Out is A
  implicit def fieldExtractor[T, A]
  (implicit ev: T <:< Field[A]): Extractor.Aux[T, A] =
    new Extractor[T] {
      override type Out = A

// The extractor for A
trait Extractor[A] {
  type Out // Produces result of type Out

object Extractor {
  // The Aux pattern
  type Aux[A, Out0] = Extractor[A] {
    type Out = Out0

  // Proof that Out for HNil is HNil
  implicit val hnilExtractor: Aux[HNil, HNil] = 
    new Extractor[HNil] {
      override type Out = HNil

  // Proof that Out for T :: H is hlist of extractor result for H and T
  implicit def hconsExtractor[H, HO, T <: HList, TO <: HList]
  (implicit H: Aux[H, HO], T: Aux[T, TO]): Aux[H :: T, HO :: TO] =
    new Extractor[H :: T] {
      override type Out = HO :: TO

type QueryMonad[A] = A

// Use dependent type Out as a result
def query[Fields](fields: Fields)(implicit extractor: Extractor[Fields]): QueryMonad[extractor.Out] = ???

val result = query(IntField :: StringField :: HNil)