2
votes

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 Extractor 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 query method is meant to work on any number of fields, and is expected to give me back an appropriate tuple.

I defined a FieldExtractor type:

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 makeFieldExtractor to convert someFields into someFieldExtractors. This is where I started running into trouble.

val someFieldExtractors = someFields.map(makeFieldExtractor)

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 Fields.Id.type when it probably should be seeing Field[Int]. If I explicitly specify the field types for someFields, 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 Id/Name definitions to a val instead of a case object.

I found https://github.com/milessabin/shapeless/blob/master/examples/src/main/scala/shapeless/examples/klist.scala but didn't manage to make any successful use of it.

1

1 Answers

2
votes

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 http://gigiigig.github.io/posts/2015/09/13/aux-pattern.html
  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)