dreamcocoa
dreamcocoa

Reputation: 93

Why could it not resolve implicits, if a Coproduct member is generic in my ADT?

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

Answers (1)

Edmondo
Edmondo

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

Related Questions