Search code examples
scalatypeclassimplicitshapeless

Could not find implicit value for parameter Mapper


I try to create a simple shapeless based function to convert the case class into a list of string I could then encode as csv.

The point of this is to summon type class CsvEncoder on every member of the case class and then invoke method encode to change it to string.

Type class:

trait CsvEncoder[T] {
  def encode(t: T): String
}

trait LowPriorityEncoder {
  implicit def genericEncoder[T]: CsvEncoder[T] = _.toString
}

object CsvEncoder extends LowPriorityEncoder {

  implicit def optionEncoder[T](
      implicit e: CsvEncoder[T]
  ): CsvEncoder[Option[T]] = _.fold("")(e.encode)

}

And shapeless:

class CsvBuilder[Row](rows: List[Row]) {

  object csvMapper extends Poly1 {

    implicit def caseEncode[V](
        implicit e: CsvEncoder[V]
    ): Case.Aux[FieldType[Symbol, V], FieldType[Symbol, String]] =
      at[FieldType[Symbol, V]](s => field[Symbol](e.encode(s)))
  }

  def build[Repr <: HList, OutRepr <: HList](
      implicit labelledGeneric: LabelledGeneric.Aux[Row, Repr],
      mapper: Mapper.Aux[csvMapper.type, Repr, OutRepr],
      toMap: ToMap.Aux[OutRepr, Symbol, String]
  ): List[Map[String, String]] = {

    def transform(row: Row): Map[String, String] = {
      val formattedRows = labelledGeneric
        .to(row)
        .map(csvMapper)

      val fields = toMap(formattedRows)
        .map {
          case (k: Symbol, value: String) =>
            (k.toString -> value)
        }

      fields
    }

    rows.map(transform(_))

  }
}

When I try to use with simple case class:

final case class ProjectCsvRow(
    project: Option[String],
    something: Option[String],
    site: Option[String],
    state: Option[Int]
)

new CsvBuilder[ProjectCsvRow](
  List(ProjectCsvRow(Some(""), None, None, None))
).build

I'm getting

could not find implicit value for parameter mapper: shapeless.ops.hlist.Mapper.Aux[stabilizer$1.csvMapper.type,Repr,OutRepr]

I think I'm missing something, but I can't really figure it out.

I already checked if there's instance of CsvEncoder for every field.


Solution

  • Firstly, your Poly is incorrect. Type Symbol is too rough. Keys of a record are not just Symbols, they are singleton subtypes of Symbol. You should define

    object csvMapper extends Poly1 {
      implicit def caseEncode[K <: Symbol, V](
        implicit e: CsvEncoder[V]
      ): Case.Aux[FieldType[K, V], FieldType[K, String]] =
        at[FieldType[K, V]](s => field[K](e.encode(s)))
    }
    

    Secondly, making a Poly nested into a class (rather than object) can be dangerous (it starts to depend on an instance of CsvBuilder and therefore on type Row). So move csvMapper outside CsvBuilder (so that its path is stable).