Shapeless 的 Lazy 和默认参数导致隐式解析失败
Shapeless' Lazy and default parameters cause implicit resolution to fail
我的一个项目混合使用了 scala 功能,这些功能似乎不能很好地结合在一起:
- 类型classes,无形自动类型class实例推导
- 隐式转换(向具有类型 class 实例的类型添加有用的语法)
- 默认参数,因为尽管它们通常是坏事,但在这里使用起来太方便了
我 运行 遇到的问题是类型 class 实例派生失败,如果:
- 没有明确指定默认参数
- 无形推导使用
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())
编译错误由以下任一方法修复:
- 在
Run.scala
中将 header
参数显式传递给 show
- 删除
Lazy
包装到 Show.scala
中的隐式 Show[H]
我必须承认我在这里完全不知所措。我很想了解会发生什么,如果存在解决方法,我很想知道。
简答:
如果将上下文绑定移动到隐式 class,它也可以正常工作。你必须牺牲值 class 才能做到这一点,但我认为预先告诉编译器只有 A
具有 Show
的值才会被它丰富:
implicit class Show2Ops[A : Show](a: A) {
def show2(header: String = "> ") = header + implicitly[Show[A]].show(a)
}
println(Foo(12).show2())
长论:
Lazy
做了一些有趣的技巧,但很难遵循。您没有具体询问 Lazy
在做什么,但我对此很好奇,因为我一直在使用它而不确定它是如何工作的。所以我看了一下。据我所知,它是这样的。
您有一个带有递归字段的案例 class:
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
因为它不再只适用于单身人士了。
现在假设您在 genericShow
中没有 Lazy
。当您尝试召唤 Show[A]
时,编译器会转到:
genericShow[A]
打开隐式搜索 Show[A]
hcons[Int :: Option[A] :: HNil]
打开隐式搜索 Show[A]
和 Show[Int :: Option[A] :: HNil
intShow
打开隐式搜索 Show[A]
和 Show[Int]
和 Show[Option[A] :: HNil]
hcons[Option[A] :: HNil]
打开隐式搜索 Show[A]
和 Show[Option[A] :: HNil]
opt[A]
打开隐式搜索 Show[A]
和 Show[Option[A]]
和 Show[Option[A] :: HNil]
genericShow[A]
打开隐式搜索 Show[A]
和 Show[Option[A]]
以及 Show[Option[A] :: HNil]
现在很明显有问题了,因为它会回到#2 并再次发生,永远不会取得任何进展。
如何 Lazy
通过在编译器尝试具体化它的隐式实例时进入宏来克服这个问题。因此,当您在 hcons
中使用 implicit showH: Lazy[Show[H]]
而不是仅使用 Show[H]
时,编译器会转到该宏来查找 Lazy[Show[H]]
而不是停留在您的隐式 Show
情况下。
该宏检查开放式隐式(宏可以方便地访问)并进入其自身的隐式解析算法,该算法始终在继续查找 T
的隐式实例之前完全解析开放式隐式(对于 Lazy[T]
).如果要解析一个已经打开的隐式,它会用一个虚拟树(本质上是告诉编译器 "I got this, don't worry about it")来跟踪打结的依赖关系,以便解析的其余部分可以完成。最后,它清理了虚拟树(我不太明白这是如何工作的;那里的代码数量惊人,而且非常复杂!)
那么为什么 Lazy
似乎搞乱了您的默认参数情况?我认为这是几件事的汇合(只是一个假设):
- 对于原始
ShowOps
,对值调用 .show
会导致它隐式包装在 ShowOps[A]
中。 A会是什么?会是Foo
、AnyRef
、Any
吗?它会是一个独特的单一类型吗?这不是很清楚,因为那时 A
没有约束,Scala 不知道你对 .show
的调用实际上会约束它(由于上下文绑定)。
- 没有
Lazy
,这就可以了,因为如果 Scala 选择了错误的 A
而 .show
没有进行类型检查,它将意识到它的错误并退出 A
它选择了。
- 对于
Lazy
,有很多其他的逻辑在进行,这有点欺骗 Scala 认为它选择的任何 A
都是好的。但是到了闭环的时候,却没有成功,到时候再退出也来不及了。
- 不知何故,未指定的默认参数会影响 Scala 在
ShowOps[A]
中选择 A
的初始选择。
我的一个项目混合使用了 scala 功能,这些功能似乎不能很好地结合在一起:
- 类型classes,无形自动类型class实例推导
- 隐式转换(向具有类型 class 实例的类型添加有用的语法)
- 默认参数,因为尽管它们通常是坏事,但在这里使用起来太方便了
我 运行 遇到的问题是类型 class 实例派生失败,如果:
- 没有明确指定默认参数
- 无形推导使用
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())
编译错误由以下任一方法修复:
- 在
Run.scala
中将 - 删除
Lazy
包装到Show.scala
中的隐式
header
参数显式传递给 show
Show[H]
我必须承认我在这里完全不知所措。我很想了解会发生什么,如果存在解决方法,我很想知道。
简答:
如果将上下文绑定移动到隐式 class,它也可以正常工作。你必须牺牲值 class 才能做到这一点,但我认为预先告诉编译器只有 A
具有 Show
的值才会被它丰富:
implicit class Show2Ops[A : Show](a: A) {
def show2(header: String = "> ") = header + implicitly[Show[A]].show(a)
}
println(Foo(12).show2())
长论:
Lazy
做了一些有趣的技巧,但很难遵循。您没有具体询问 Lazy
在做什么,但我对此很好奇,因为我一直在使用它而不确定它是如何工作的。所以我看了一下。据我所知,它是这样的。
您有一个带有递归字段的案例 class:
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
因为它不再只适用于单身人士了。
现在假设您在 genericShow
中没有 Lazy
。当您尝试召唤 Show[A]
时,编译器会转到:
genericShow[A]
打开隐式搜索Show[A]
hcons[Int :: Option[A] :: HNil]
打开隐式搜索Show[A]
和Show[Int :: Option[A] :: HNil
intShow
打开隐式搜索Show[A]
和Show[Int]
和Show[Option[A] :: HNil]
hcons[Option[A] :: HNil]
打开隐式搜索Show[A]
和Show[Option[A] :: HNil]
opt[A]
打开隐式搜索Show[A]
和Show[Option[A]]
和Show[Option[A] :: HNil]
genericShow[A]
打开隐式搜索Show[A]
和Show[Option[A]]
以及Show[Option[A] :: HNil]
现在很明显有问题了,因为它会回到#2 并再次发生,永远不会取得任何进展。
如何 Lazy
通过在编译器尝试具体化它的隐式实例时进入宏来克服这个问题。因此,当您在 hcons
中使用 implicit showH: Lazy[Show[H]]
而不是仅使用 Show[H]
时,编译器会转到该宏来查找 Lazy[Show[H]]
而不是停留在您的隐式 Show
情况下。
该宏检查开放式隐式(宏可以方便地访问)并进入其自身的隐式解析算法,该算法始终在继续查找 T
的隐式实例之前完全解析开放式隐式(对于 Lazy[T]
).如果要解析一个已经打开的隐式,它会用一个虚拟树(本质上是告诉编译器 "I got this, don't worry about it")来跟踪打结的依赖关系,以便解析的其余部分可以完成。最后,它清理了虚拟树(我不太明白这是如何工作的;那里的代码数量惊人,而且非常复杂!)
那么为什么 Lazy
似乎搞乱了您的默认参数情况?我认为这是几件事的汇合(只是一个假设):
- 对于原始
ShowOps
,对值调用.show
会导致它隐式包装在ShowOps[A]
中。 A会是什么?会是Foo
、AnyRef
、Any
吗?它会是一个独特的单一类型吗?这不是很清楚,因为那时A
没有约束,Scala 不知道你对.show
的调用实际上会约束它(由于上下文绑定)。 - 没有
Lazy
,这就可以了,因为如果 Scala 选择了错误的A
而.show
没有进行类型检查,它将意识到它的错误并退出A
它选择了。 - 对于
Lazy
,有很多其他的逻辑在进行,这有点欺骗 Scala 认为它选择的任何A
都是好的。但是到了闭环的时候,却没有成功,到时候再退出也来不及了。 - 不知何故,未指定的默认参数会影响 Scala 在
ShowOps[A]
中选择A
的初始选择。