1

I encountered the following problem with implicits in Scala, using Shapeless's Generic.Aux:

  case class Complex(re: Double, im: Double)

  object Prod2 {
    def unapply[C, A, B](c: C)(implicit C: Generic.Aux[C, A :: B :: HNil]) = Some((C.to(c).head, C.to(c).tail.head))
  }

  val c = Complex(1.0, 2.0)
  val Prod2(re, im) = c

The code above does not compile. It reports

Error:(22, 7) could not find implicit value for parameter C: shapeless.Generic.Aux[nexus.ops.Test.Complex,A :: B :: shapeless.HNil]
  val Prod2(re, im) = c
Error:(22, 7) not enough arguments for method unapply: (implicit C: shapeless.Generic.Aux[nexus.ops.Test.Complex,A :: B :: shapeless.HNil])Some[(A, B)].
Unspecified value parameter C.
  val Prod2(re, im) = c

However, if I manually do

implicitly[Generic.Aux[Complex, Double :: Double :: HNil]]

it is perfectly OK to derive this implicit instance.

Tongfei Chen
  • 613
  • 4
  • 14
  • I wouldn't look at this code with two symbols named `C` in scope. Was that intentional? – som-snytt Jan 30 '19 at 02:39
  • 2
    Isn't the typeclass instance being the same name as the type the Scala norm? e.g. `K: Eq[K]` – Tongfei Chen Jan 30 '19 at 02:52
  • Maybe I am ignorant about that. I know Scala has bugs about shadowing and implicits, I was supposed to contribute a fix in that area, I may still get around to it. – som-snytt Jan 30 '19 at 05:42
  • @som-snytt Sometimes using the same letter is standard: https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/Representable.scala#L18 https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/data/Cokleisli.scala#L167 This depends on code conventions. – Dmytro Mitin Jan 30 '19 at 08:46

2 Answers2

1

The following code works:

import shapeless.ops.hlist.IsHCons
import shapeless.{::, Generic, HList, HNil}

case class Complex(re: Double, im: Double)

object Prod2 {
  def unapply[C, L <: HList, H, T <: HList, H1, T1 <: HList](c: C)(implicit
    C: Generic.Aux[C, L],
    isHCons: IsHCons.Aux[L, H, T],
    isHCons1: IsHCons.Aux[T, H1, T1]) = Some((C.to(c).head, C.to(c).tail.head))
}

val c = Complex(1.0, 2.0)
val Prod2(re, im) = c
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 1
    You beat me by a minute, but my answer gives a little more context and (in my view) a slightly better solution, so I'm going to leave it up. – Travis Brown Jan 30 '19 at 08:35
  • @TravisBrown It's funny that if we remove `Aux` for 2nd `IsHCons`: `def unapply[C, L <: HList, H, T <: HList](c: C)(implicit C: Generic.Aux[C, L], isHCons: IsHCons.Aux[L, H, T], isHCons1: IsHCons[T]): Option[(H, isHCons1.H)] = ...` then we'll have `error during expansion of this match (this is a scalac bug). The underlying error was: type mismatch; found : Double required: isHCons1.H` – Dmytro Mitin Jan 30 '19 at 09:00
1

Unfortunately the compiler simply isn't smart enough to perform the unification that would be necessary to infer A and B here. You can read about some of the details of this problem in section 4.3 of Underscore's Type Astronaut’s Guide to Shapeless. The book provides a workaround using IsHCons, but in this case I think requiring a <:< proof is a little cleaner:

import shapeless.{::, Generic, HList, HNil}

case class Complex(re: Double, im: Double)

object Prod2 {
  def unapply[C, L <: HList, A, B](c: C)(implicit
    C: Generic.Aux[C, L],
    ev: L <:< (A :: B :: HNil)
  ) = Some((C.to(c).head, C.to(c).tail.head))
}

And then:

scala> val c = Complex(1.0, 2.0)
c: Complex = Complex(1.0,2.0)

scala> val Prod2(re, im) = c
re: Double = 1.0
im: Double = 2.0

It's disappointing, but this is a workaround you'll need over and over if you work with Shapeless, so it's good to have it in your toolbox.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680