Deriving type class instances for case classes wit

2020-06-19 11:48发布

问题:

I'm working on a CSV parsing library (tabulate). It uses simple type classes for encoding / decoding: encoding, for example, is done with instances of CellEncoder (to encode a single cell) and RowEncoder (to encode entire rows).

Using shapeless, I've found it pretty straightforward to automatically derive the following type class instances:

  • RowEncoder[A] if A is a case class whose fields all have a CellEncoder.
  • RowEncoder[A] if A is an ADT whose alternatives all have a RowEncoder.
  • CellEncoder[A] if A is an ADT whose alternatives all have a CellEncoder.

The thing is, this last one turns out to be almost entirely useless in real life situations: an ADT's alternatives are almost always case classes, and I cannot derive a CellEncoder for a case class that has more than one field.

What I'd like to be able to do, however, is derive a CellEncoder for case classes that have a single field whose type has a CellEncoder. That would cover, for example, Either, scalaz's \/, cats' Xor...

This is what I have so far:

implicit def caseClass1CellEncoder[A, H](implicit gen: Generic.Aux[A, H :: HNil], c: CellEncoder[H]): CellEncoder[A] =
    CellEncoder((a: A) => gen.to(a) match {
      case h :: t => c.encode(h)
    })

This works fine when used explicitly:

case class Bar(xs: String)
caseClass1CellEncoder[Bar, String]
res0: tabulate.CellEncoder[Bar] = tabulate.CellEncoder$$anon$2@7941904b

I can't however get it to work implicitly, the following fails:

implicitly[CellEncoder[Bar]]
>> could not find implicit value for parameter e: tabulate.CellEncoder[Test.this.Bar]

I've also tried the following, with no more success:

implicit def testEncoder[A, H, R <: H :: HNil](implicit gen: Generic.Aux[A, R], c: CellEncoder[H]): CellEncoder[A] =
      CellEncoder((a: A) => gen.to(a) match {
        case h :: t => c.encode(h)
      })

Am I missing something? Is what I'm trying to do even possible?

回答1:

It's a little tricky to get the H inferred correctly, but you can do it with a <:< instance:

import shapeless._

case class CellEncoder[A](encode: A => String)

implicit val stringCellEncoder: CellEncoder[String] = CellEncoder(identity)
implicit val intCellEncoder: CellEncoder[Int] = CellEncoder(_.toString)

case class Bar(xs: String)

implicit def caseClass1CellEncoder[A, R, H](implicit
  gen: Generic.Aux[A, R],
  ev: R <:< (H :: HNil),
  c: CellEncoder[H]
): CellEncoder[A] = CellEncoder(
  (a: A) => ev(gen.to(a)) match {
    case h :: t => c.encode(h)
  }
)

(I've made up a simple CellEncoder for the sake of a complete working example.)

This works because R can be inferred when the compiler is looking for an Generic.Aux[A, R] instance, and can then guide the inference of H when looking for a value for ev.