postgresqlscalagenericsshapelessfinagle

Scala: missing implicit generic parameter shapeless.LabelledGeneric.Aux[T,L], how to provide it?


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?


Solution

  • 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)
        …
      }
      …
    }