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.