无形'延迟和默认参数导致隐式解析失败

时间:2017-02-18 16:59:59

标签: scala shapeless

我的一个项目使用scala功能混合,看起来不能很好地混合在一起:

  • 类型类和无形自动类型类实例派生
  • 隐式转换(为具有类型类实例的类型添加有用的语法)
  • 默认参数,因为即使它们通常是一件坏事,它们也太方便了

我遇到的问题是类型类实例派生失败,如果:

  • 未明确指定默认参数
  • 无形推导使用Lazy

这是我为重现问题而编写的尽可能少的代码:

Show.scala

import shapeless._

trait Show[A] {
  def show(a: A): String
}

object Show {
  def from[A](f: A => String): Show[A] = new Show[A] {
    override def show(a: A) = f(a)
  }

  implicit val intShow: Show[Int] = Show.from(_.toString)

  implicit def singletonShow[A](implicit
    sa: Show[A]
  ): Show[A :: HNil] = Show.from { 
    case (a :: HNil) => sa.show(a)
  }

  implicit def singletonCaseClassShow[A, H <: HList](implicit
    gen: Generic.Aux[A, H],
    sh: Lazy[Show[H]]
  ): Show[A] = Show.from {
    a => sh.value.show(gen.to(a))
  }
}

Run.scala

object Run extends App {
  implicit class ShowOps[A](val a: A) extends AnyVal {
    def show(header: String = "> ")(implicit sa: Show[A]): String =
      header + sa.show(a)
  }

  case class Foo(i: Int)

  println(Foo(12).show())
}

无法使用以下错误消息进行编译:

Run.scala:10: could not find implicit value for parameter sa: Show[Run.Foo]
[error]   println(Foo(12).show())

编译错误由以下任一方法修复:

  • header
  • 中明确将show参数传递给Run.scala
  • Lazy封包移至Show[H]中的隐式Show.scala

我必须承认我在这里完全失败了。我很想知道会发生什么,如果有的话,我很想知道解决方法。

1 个答案:

答案 0 :(得分:13)

简答:

如果将上下文绑定到隐式类,它也可以正常工作。你必须牺牲价值类才能做到这一点,但我认为,事先告诉编译器,只有A Show的{​​{1}}会被它所丰富,这也更清晰:

implicit class Show2Ops[A : Show](a: A) {
  def show2(header: String = "> ") = header + implicitly[Show[A]].show(a)
}

println(Foo(12).show2())

长理论:

Lazy做了一些有趣的技巧,很难遵循。你没有具体询问Lazy正在做什么,但我很好奇,因为我一直使用它而不确定它是如何工作的。所以我看了一下。就像我所说的那样,它就是这样的。

您有一个带有递归字段的案例类:

case class A(first: Int, next: Option[A])

并假设您在Show Option的同伴中有另一个案例:

implicit def opt[A](implicit showA: Show[A]): Show[Option[A]] = Show.from {
  case Some(a) => s"Some(${showA.show(a)})"
  case None => "None"
}

而不是singletonShow你有一个真正的HNil案例和一个归纳案例,这是典型的:

implicit val hnil: Show[HNil] = Show.from(_ => "")
implicit def hcons[H, T <: HList](implicit
  showH: Show[H],
  showT: Show[T]
): Show[H :: T] = Show.from {
  case h :: t => showH(h) + ", " + showT(t) // for example
}

让我们将singletonCaseClassShow重命名为genericShow,因为它不仅仅适用于单身人士。

现在假设您Lazy中没有genericShow。当您尝试召唤Show[A]时,编译器会转到:

  1. genericShow[A]开放隐式搜索Show[A]
  2. hcons[Int :: Option[A] :: HNil]开放隐式搜索Show[A]Show[Int :: Option[A] :: HNil
  3. intShow开放式隐式搜索Show[A]Show[Int]以及Show[Option[A] :: HNil]
  4. hcons[Option[A] :: HNil]开放隐式搜索Show[A]Show[Option[A] :: HNil]
  5. opt[A]开放式隐式搜索Show[A]Show[Option[A]]以及Show[Option[A] :: HNil]
  6. genericShow[A]开放式隐式搜索Show[A]Show[Option[A]]以及Show[Option[A] :: HNil]
  7. 现在很明显存在一个问题,因为它会回到#2并再次发生,从未取得任何进展。

    Lazy如何克服这个问题是在编译器尝试实现它的隐式实例时进入宏。因此,当您在implicit showH: Lazy[Show[H]]而不是hcons中使用Show[H]时,编译器会转到该宏来查找Lazy[Show[H]],而不是保留在隐式Show个案例中。

    宏检查open implicits(哪些宏有助于访问)并进入其自己的隐式解析算法,该算法始终完全解决开放的implicits,然后继续查找T的隐式实例(对于Lazy[T] })。如果要解析已经打开的隐式,它会替换一个虚拟树(基本上告诉编译器“我得到了这个,不要担心它”),它跟踪打结的依赖关系,以便其余的解析完成。最后,它清理了虚拟树(我无法弄清楚它是如何工作的;那里有大量令人惊讶的代码并且它非常复杂!)

    那么为什么Lazy似乎搞乱了你的默认参数情况呢?我认为这是一些事情的汇合(只是一个假设):

    1. 使用原始ShowOps,在值上调用.show会导致将其隐式包装在ShowOps[A]中。什么是A?会是FooAnyRefAny吗?它会成为一种独特的单一类型吗?这不是很清楚,因为当时A没有约束,Scala不知道你对.show的调用会实际约束它(由于上下文绑定)。
    2. 如果没有Lazy,就可以了,因为如果Scala选择了错误的A并且.show没有进行类型检查,那么它将意识到它的错误并退出{{1它选择了。
    3. 使用A,还有很多其他逻辑正在进行中,它有点欺骗Scala认为它所选择的Lazy是好的。但是当关闭循环的时候,它就没有用了,到那时为止已经太晚了。
    4. 不知何故,默认参数未指定会影响Scala在A中选择A的初始选择。