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.
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)