I'm writing code for working with DB with finagle-postgres, I have mappers and sure I want to move common parts of code, as much as I can, to generic Mapper.
So, for example, I have Mapper
case class ProcessState(
...
)
class ProcessStateMapper(client: PostgresClient)
extends EntityMapper[ProcessState]("process_state", client)(rowDecoder) {
...
def create(state: ProcessState): Future[ProcessState] = {
val fields = Updates(state)
val columnNames = fields.updates.map(_._1).mkString(", ")
val placeholders = (1 to fields.updates.size).map(i => s"$$$i").mkString(", ")
for {
inserted <- client.prepareAndExecute(
s"INSERT INTO $tableName ($columnNames) VALUES ($placeholders)", fields.params: _*)
} yield {
require(inserted > 0, s"Failed to create $tableName: $state")
state
}
}
}
This create method works fine, Updates
requires implicit shapeless.LabelledGeneric.Aux[T,L]
, and in this context it finds it.
But if I move method to generic class, because of type erasure, it can't find implicit value...
abstract class EntityMapper[T <: Product](
val tableName: String, val client: PostgresClient)(
implicit val rowDecoder: RowDecoder[T]) {
...
def create(state: T): Future[T] = {
val fields = Updates(state)
val columnNames = fields.updates.map(_._1).mkString(", ")
val placeholders = (1 to fields.updates.size).map(i => s"$$$i").mkString(", ")
for {
inserted <- client.prepareAndExecute(
s"INSERT INTO $tableName ($columnNames) VALUES ($placeholders)", fields.params: _*)
} yield {
require(inserted > 0, s"Failed to create $tableName: $state")
state
}
}
}
So this code doesn't compile with error
could not find implicit value for parameter gen: shapeless.LabelledGeneric.Aux[T,L]
val fields = Updates(state)
So I try to provide this param
abstract class EntityMapper[T <: Product](
val tableName: String, val client: PostgresClient)(
implicit val rowDecoder: RowDecoder[T], val lgen: LabelledGeneric.Aux[T, _]) {
...
}
class ProcessStateMapper(client: PostgresClient)
extends EntityMapper[ProcessState]("process_state", client)(rowDecoder, ProcessStateMapper.lgen) {
...
}
object ProcessStateMapper {
...
val lgen: LabelledGeneric.Aux[ProcessState, _] = LabelledGeneric[ProcessState]
}
But it doesn't work, I tried some other ways of providing it, but they all failed. Do you know what's the proper way of doing this?
Updates#apply
requires more than just a LabelledGeneric.Aux[P, L]
.
The complete signature is:
def apply[P <: Product, L <: HList, MP <: HList](p: P)(implicit
gen: LabelledGeneric.Aux[P, L],
mapper: Mapper.Aux[toLabelledParam.type, L, MP],
toList: ToList[MP, (String, Param[_])],
columnNamer: ColumnNamer
): Updates
In order to be able to call the function, you need to add all these implicit parameters to your class as well. And since the L
and MP
type parameters need to be in scope too, you need to add them as type parameters for your class as well. So you end up with this:
abstract class EntityMapper[P <: Product, L <: HList, MP <: HList](
val tableName: String, val client: PostgresClient)(
implicit rowDecoder: RowDecoder[T],
gen: LabelledGeneric.Aux[P, L],
mapper: Mapper.Aux[toLabelledParam.type, L, MP],
toList: ToList[MP, (String, Param[_])],
columnNamer: ColumnNamer) {
This stinks! Not only because of all of those implicits, but also because now you have two completely meaningless type parameters in your class.
But you can work around that with some implicits:
trait Updatable[P <: Product] {
def apply(p: P): Updates
}
object Updatable {
implicit def instance[P <: Product, L <: HList, MP <: HList](
implicit gen: LabelledGeneric.Aux[P, L],
mapper: Mapper.Aux[toLabelledParam.type, L, MP],
toList: ToList[MP, (String, Param[_])],
columnNamer: ColumnNamer): Updatable[P] = new Updatable[P] {
def apply(p: P): Updates = Updates(p)
}
}
And now you can write your EntityMapper
like so:
abstract class EntityMapper[T <: Product](
val tableName: String,
val client: PostgresClient)(
implicit updatable: Updatable[T]) {
…
def create(state: T): Future[T] = {
val fields = updatable(state)
…
}
…
}