Reputation: 93
I've written a self-contained example to reproduce the problem.
For better understanding of this problem I wrote a simple type class Encoder
which takes care of encoding the given type T:
trait Encoder[T] {
def encode(value: T): String
}
with its companion object for nice syntax and short-hand function for creating the type class instances:
object Encoder {
def instance[T](f: T => String): Encoder[T] = new Encoder[T] {
def encode(value: T): String = f(value)
}
def apply[T](implicit encoder: Encoder[T]): Encoder[T] = encoder
object syntax {
implicit class EncoderOps[T](value: T) {
def encode(implicit encoder: Encoder[T]): String = encoder.encode(value)
}
}
}
There is an ADT called Fragment
for representing the fragments of a page and I've implemented the Encoder for all fragment type.
sealed trait Fragment
final case class TextFragment(value: String) extends Fragment
object TextFragment {
implicit val encoder: Encoder[TextFragment] = Encoder.instance(_.value)
}
final case class NumberFragment(value: Int) extends Fragment
object NumberFragment {
implicit val encoder: Encoder[NumberFragment] =
Encoder.instance(_.value.toString)
}
In order to make the call like here possible:
val fragments: List[Fragment] =
List(TextFragment("text fragment"), NumberFragment(123))
println {
fragments.map(_.encode).mkString("\n")
}
I've implemented a generic Encoder's of Coproduct and HList with help of Shapeless:
implicit def hlistEncoder[H, T <: HList](
implicit
hEncoder: Lazy[Encoder[H]],
tEncoder: Encoder[T]
): Encoder[H :: T] = instance {
case h :: t => hEncoder.value.encode(h) ++ tEncoder.encode(t)
}
implicit def genericEncoder[A, Repr](implicit gen: Generic.Aux[A, Repr],
encoder: Encoder[Repr]): Encoder[A] =
instance(fragment => encoder.encode(gen.to(fragment)))
implicit val cnilEncoder: Encoder[CNil] = instance(
cnil => throw new Exception("not allowed"))
implicit def coproductEncoder[H, T <: Coproduct](
implicit hEncoder: Lazy[Encoder[H]],
tEncoder: Encoder[T]
): Encoder[H :+: T] = instance {
case Inl(h) => hEncoder.value.encode(h)
case Inr(t) => tEncoder.encode(t)
}
It works as expected, but ...
... it doesn't work if I add a generic fragment like here:
final case class GenericFragment[T](value: T) extends Fragment
object GenericFragment {
implicit def encoder[T](
implicit encoder: Encoder[T]): Encoder[GenericFragment[T]] =
Encoder.instance(fragment => encoder.encode(fragment.value))
}
and with some Encoders for Int and String values:
implicit val stringEncoder: Encoder[String] = instance(identity)
implicit val intEncoder: Encoder[Int] = instance(_.toString)
Then it doesn't compile anymore ...
val fragments: List[Fragment] =
List(TextFragment("text fragment"), NumberFragment(123))
println {
fragments.map(_.encode).mkString("\n")
}
Here an output with -Xlog-implicits
in scalacOptions:
[info] /Users/../example/src/main/scala/playground/PlaygroundApp.scala:92:21: shapeless.this.Generic.materialize is not a valid implicit value for shapeless.Generic.Aux[playground.Fragment,Repr] because:
[info] hasMatchingSymbol reported error: exception during macro expansion:
[info] java.lang.IndexOutOfBoundsException: -1
[info] at scala.collection.LinearSeqOptimized.apply(LinearSeqOptimized.scala:63)
[info] at scala.collection.LinearSeqOptimized.apply$(LinearSeqOptimized.scala:61)
[info] at scala.collection.immutable.List.apply(List.scala:86)
[info] at shapeless.CaseClassMacros.$anonfun$ctorsOfAux$4(generic.scala:421)
[info] at scala.collection.immutable.List.map(List.scala:283)
[info] at shapeless.CaseClassMacros.substituteArgs$1(generic.scala:419)
[info] at shapeless.CaseClassMacros.$anonfun$ctorsOfAux$3(generic.scala:446)
[info] at scala.collection.immutable.List.flatMap(List.scala:335)
[info] at shapeless.CaseClassMacros.ctorsOfAux(generic.scala:413)
[info] at shapeless.CaseClassMacros.ctorsOfAux$(generic.scala:382)
[info] at shapeless.GenericMacros.ctorsOfAux(generic.scala:989)
[info] at shapeless.CaseClassMacros.distinctCtorsOfAux(generic.scala:379)
[info] at shapeless.CaseClassMacros.distinctCtorsOfAux$(generic.scala:374)
[info] at shapeless.GenericMacros.distinctCtorsOfAux(generic.scala:989)
[info] at shapeless.CaseClassMacros.ctorsOf(generic.scala:371)
[info] at shapeless.CaseClassMacros.ctorsOf$(generic.scala:371)
[info] at shapeless.GenericMacros.ctorsOf(generic.scala:989)
[info] at shapeless.GenericMacros.mkCoproductGeneric(generic.scala:1036)
[info] at shapeless.GenericMacros.materialize(generic.scala:1003)
[info] fragments.map(_.encode).mkString("\n")
[info] ^
[info] /Users/../example/src/main/scala/playground/PlaygroundApp.scala:92:21: playground.this.Encoder.genericEncoder is not a valid implicit value for playground.Encoder[playground.Fragment] because:
[info] hasMatchingSymbol reported error: could not find implicit value for parameter gen: shapeless.Generic.Aux[playground.Fragment,Repr]
[info] fragments.map(_.encode).mkString("\n")
[info] ^
[info] /Users/../example/src/main/scala/playground/PlaygroundApp.scala:92:21: shapeless.this.Generic.materialize is not a valid implicit value for shapeless.Generic.Aux[playground.Fragment,Repr] because:
[info] hasMatchingSymbol reported error: exception during macro expansion:
[info] java.lang.IndexOutOfBoundsException: -1
[info] at scala.collection.LinearSeqOptimized.apply(LinearSeqOptimized.scala:63)
[info] at scala.collection.LinearSeqOptimized.apply$(LinearSeqOptimized.scala:61)
[info] at scala.collection.immutable.List.apply(List.scala:86)
[info] at shapeless.CaseClassMacros.$anonfun$ctorsOfAux$4(generic.scala:421)
[info] at scala.collection.immutable.List.map(List.scala:283)
[info] at shapeless.CaseClassMacros.substituteArgs$1(generic.scala:419)
[info] at shapeless.CaseClassMacros.$anonfun$ctorsOfAux$3(generic.scala:446)
[info] at scala.collection.immutable.List.flatMap(List.scala:335)
[info] at shapeless.CaseClassMacros.ctorsOfAux(generic.scala:413)
[info] at shapeless.CaseClassMacros.ctorsOfAux$(generic.scala:382)
[info] at shapeless.GenericMacros.ctorsOfAux(generic.scala:989)
[info] at shapeless.CaseClassMacros.distinctCtorsOfAux(generic.scala:379)
[info] at shapeless.CaseClassMacros.distinctCtorsOfAux$(generic.scala:374)
[info] at shapeless.GenericMacros.distinctCtorsOfAux(generic.scala:989)
[info] at shapeless.CaseClassMacros.ctorsOf(generic.scala:371)
[info] at shapeless.CaseClassMacros.ctorsOf$(generic.scala:371)
[info] at shapeless.GenericMacros.ctorsOf(generic.scala:989)
[info] at shapeless.GenericMacros.mkCoproductGeneric(generic.scala:1036)
[info] at shapeless.GenericMacros.materialize(generic.scala:1003)
[info] fragments.map(_.encode).mkString("\n")
[info] ^
[info] /Users/../example/src/main/scala/playground/PlaygroundApp.scala:92:21: playground.this.Encoder.genericEncoder is not a valid implicit value for playground.Encoder[playground.Fragment] because:
[info] hasMatchingSymbol reported error: could not find implicit value for parameter gen: shapeless.Generic.Aux[playground.Fragment,Repr]
[info] fragments.map(_.encode).mkString("\n")
[info] ^
[error] /Users/../example/src/main/scala/playground/PlaygroundApp.scala:92:21: could not find implicit value for parameter encoder: playground.Encoder[playground.Fragment]
[error] fragments.map(_.encode).mkString("\n")
Used versions:
Why doesn't it work with a generic fragment type? Can somebody explain me why? Hopefully there is a good workaround to fix the problem.
Thank you for your help.
Upvotes: 3
Views: 252
Reputation: 20080
It doesn't look like the problem is introducing the generic. If we look at this code
val fragments: List[Fragment] =
List(TextFragment("text fragment"), NumberFragment(123))
we recognize that List
takes as type parameter the first common bound of its types, which is Fragment
. With this assignment, you have lost type information about the fragments. List
is a Coproduct
, but you do not have any Encoder[Fragment]
in scope so the implicit def coproductEncoder
is not applicable.
You either need to use an HList
of EncoderOps[TextFragment] :: EncoderOps[NumberFragment]]
if you really need to keep the type parameters or simply get rid of the type parameter like so:
trait Encodable{
def encode:String
}
object Encodable{
implicit def asEncodable[T](t:T)(implicit encoder:Encoder[T]) = new Encodable {
def encode = encoder.encode(t)
}
}
val encodableFragments:List[Encodable] = List(TextFragment("text fragment"), NumberFragment(123))
Upvotes: 2