Search code examples
scalashapeless

Cannot derive implicit instance of type class using shapeless


I want to derive instances of type classes from unary case classes. But when i try to implicitly derive it i always get an error message. If i derive it explicitly using implicit method - it works. I'm not sure, but maybe the reason is that i missed some implicit types in my function

import shapeless._

import scala.reflect.ClassTag
import scala.reflect.runtime.universe.TypeTag

sealed trait Foo[A] {
  def hello(): Unit
}

object Foo {
  def apply[A](implicit foo: Foo[A]): foo.type = foo

  def instance[A](implicit tag: ClassTag[A]): Foo[A] = new Foo[A] {
    override def hello(): Unit = println(s"Hello from ${tag.runtimeClass.getName}")
  }
}

trait Instances extends LowestPriority {
  implicit val intHelloInstance: Foo[Int] = Foo.instance[Int]
}

trait LowestPriority {
  implicit def derive[A: TypeTag, L <: HList, H](
    implicit gen: Generic.Aux[A, L],
    H: Lazy[Foo[H]],
    isUnary: (H :: HNil) =:= L
  ): Foo[A] =
    new Foo[A] {
      override def hello(): Unit = {
        print(s"Derived: ")
        H.value.hello()
      }
    }
}

object T extends Instances {

  case class A(a: Int)

  def main(args: Array[String]): Unit = {
    intHelloInstance.hello()
//    val a: Foo[A] = derive[A, Int :: HNil, Int] // this works
//    a.hello()
    Foo[A].hello() // error
  }
}

From logs:

Information:(45, 8) shapeless.this.Generic.materialize is not a valid implicit value for shapeless.Generic.Aux[H,L] because: hasMatchingSymbol reported error: H is not a case class, case class-like, a sealed trait or Unit Foo[A].hello()

How can i fix it?


Solution

  • This is one of the cases when behavior depends on... the order of implicits to resolve.

    If you modify signature to:

      implicit def derive[A, L <: HList, H](
        implicit gen: Generic.Aux[A, L],
        isUnary: (H :: HNil) =:= L, // swapped
        H: Lazy[Foo[H]]             // with this
      ): Foo[A] = ...
    

    compiler will:

    • try to find some HList L that could be paired with A
    • then prove that L equal to some H :: HNil figuring out H in the process
    • finally using that H to fetch Lazy[Foo[H]]

    and you will successfully compile Foo[A].hello().

    When these two last argument are swapped to what you have in your question, compiler has to

    • assume some H (which in our test case most likely WON"T be Int)
    • then forcefully try to adjust L to match it
    • then go back to Generic which now is forced to prove that A is representable by some H :: HNil where H is most likely not Int and failing to do it but with a misguiding error information

    This is one of these cases that shows that shapeless based derivation is sometimes tricky, and also here it shows that shapeless authors assumed in macros that the most likely cause of macro expansion failure is that A was not case class, while as we just saw it might be also compiler forcing some impossible proof because type inference got things wrong because we resolved them in wrong order.