Reputation: 34071
I am trying to understand coproduct of shapeless and have following example, that does not work:
import shapeless.{HList, ::, HNil}
import shapeless.Generic
import shapeless.{Coproduct, :+:, CNil, Inl, Inr}
trait CsvEncoder[A] {
def encode(value: A): List[String]
}
sealed trait Shape
final case class Rectangle(width: Double, height: Double) extends Shape
final case class Circle(radius: Double) extends Shape
object CsvEncoder {
def createEncoder[A](func: A => List[String]): CsvEncoder[A] = {
new CsvEncoder[A] {
override def encode(value: A): List[String] = func(value)
}
}
implicit val booleanEncoder: CsvEncoder[Boolean] =
createEncoder(value => if (value) List("yes") else List("no"))
implicit val intEncoder: CsvEncoder[Int] =
createEncoder(value => List(value.toString))
implicit val stringEncoder: CsvEncoder[String] =
createEncoder(value => List(value))
implicit val doubleEncoder: CsvEncoder[Double] =
createEncoder(value => List(value.toString))
def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] = enc
implicit val cnilEncoder: CsvEncoder[CNil] =
createEncoder(cnil => throw new Exception("Inconceivable!"))
implicit def coproductEncoder[H, T <: Coproduct](
implicit
hEncoder: CsvEncoder[H],
tEncoder: CsvEncoder[T]
): CsvEncoder[H :+: T] = createEncoder {
case Inl(h) => hEncoder.encode(h)
case Inr(t) => tEncoder.encode(t)
}
def writeCsv[A](values: List[A])(implicit enc: CsvEncoder[A]): String =
values.map(value => enc.encode(value).mkString(",")).mkString("\n")
}
object Main {
def main(args: Array[String]) {
println("----------------------------------------------------------")
val shapes: List[Shape] = List(
Rectangle(3.0, 4.0),
Circle(1.0)
)
println(CsvEncoder.writeCsv(shapes))
}
}
The compiler complains:
Error:(162, 32) could not find implicit value for parameter enc: CsvEncoder[Shape]
println(CsvEncoder.writeCsv(shapes))
Error:(162, 32) not enough arguments for method writeCsv: (implicit enc: CsvEncoder[Shape])String.
Unspecified value parameter enc.
println(CsvEncoder.writeCsv(shapes))
Do I have create an instance of CsvEncoder
for Shape
? Shapeless should care it for me, or?
Upvotes: 0
Views: 506
Reputation: 19039
I'd recommend re/reading The Type Astronaut's Guide to Shapeless Book, it really is an amazing book. It seems the code you have posted is partly from there.
The CSV Encoder example uses Employee
, and writes the CsvEncoder
by hand (page 23):
implicit val iceCreamEncoder: CsvEncoder[IceCream] =
new CsvEncoder[IceCream] {
def encode(i: IceCream): List[String] =
List(
i.name,
i.numCherries.toString,
if(i.inCone) "yes" else "no"
)
}
If you do not want to have to write all this by hand, you're gonna need a bit more boilerplate.
This is described in 3.2.1 of the book, around page 27:
import shapeless.{HList, ::, HNil}
implicit val hnilEncoder: CsvEncoder[HNil] =
createEncoder(hnil => Nil)
implicit def hlistEncoder[H, T <: HList](
implicit
hEncoder: CsvEncoder[H],
tEncoder: CsvEncoder[T]
): CsvEncoder[H :: T] =
createEncoder {
case h :: t =>
hEncoder.encode(h) ++ tEncoder.encode(t)
}
We have the recursion case, hlistEncoder
(that returns CsvEncoder[H :: T]
), that takes the head and the tail, and the base case encoder, hnilEncoder
(that returns CsvEncoder[HNil]
, because a HList always has a HNil
at its end, like every other linked list).
This, however, is enough to CSV-encode any HList, but a Rectangle
isn't a HList
! You need to use a Generic
to convert back-and-forth. This is explained in 3.2.2 of the book, right after the previous part (I converted the code to use Generic.Aux
, which is one of the most important tools of shapeless):
implicit def genericEncoder[A, R](
implicit
gen: Generic.Aux[A, R],
enc: CsvEncoder[R]
): CsvEncoder[A] =
createEncoder(a => enc.encode(gen.to(a)))
The gen.to
converts a
(say Rectangle
) to an HList (so, Double :: Double :: HNil
, and encodes that.
It also converts Shape
to Rectangle :+: Circle :+: CNil
, so your Main
works as-is :).
Upvotes: 3