【问题标题】:Shapeless' Lazy and default parameters cause implicit resolution to failShapeless' Lazy 和默认参数导致隐式解析失败
【发布时间】:2017-02-19 05:01:45
【问题描述】:

我的一个项目混合使用了一些似乎不能很好地混合在一起的 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))
  }
}

运行.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]

我必须承认我在这里完全不知所措。我很想了解会发生什么,如果存在解决方法,我很想知道。

【问题讨论】:

    标签: scala shapeless


    【解决方案1】:

    简答:

    如果将上下文绑定到隐式类,它也可以正常工作。你必须牺牲值类来做到这一点,但我认为预先告诉编译器只有具有ShowAs 会被它丰富:

    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,因为它不再只适用于单身人士。

    现在假设您在genericShow 中没有Lazy。当您尝试调用 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] 的公开隐式搜索

    现在很明显存在问题,因为它会回到 #2 并再次发生,永远不会取得任何进展。

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

    宏检查打开的隐式(宏可以访问)并进入其自己的隐式解析算法,该算法始终完全解析打开的隐式,然后继续查找T 的隐式实例(对于Lazy[T])。如果要解决已经打开的隐式问题,它会替换一个虚拟树(本质上是告诉编译器“我知道了,不要担心”)来跟踪打结的依赖关系,以便完成其余的解析。最后,它会清理虚拟树(我不太明白它是如何工作的;那里的代码数量惊人,而且非常复杂!)

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

    1. 使用原始ShowOps,对一个值调用.show 会导致它被隐式包装在ShowOps[A] 中。 A会是什么?会是FooAnyRefAny吗?它会是一个独特的单一类型吗?这不是很清楚,因为当时对 A 没有约束,Scala 不知道您对 .show 的调用实际上会约束它(由于上下文绑定)。
    2. 没有Lazy,这可以正常工作,因为如果Scala 选择了错误的A 并且.show 没有进行类型检查,它会意识到它的错误并退出它选择的A
    3. 有了Lazy,还有很多其他的逻辑在发生,这让Scala 认为无论A 选择什么都可以。但是到了闭环的时候,它就行不通了,到那时再退出也为时已晚。
    4. 不知何故,未指定的默认参数会影响 Scala 在 ShowOps[A] 中选择 A 的初始选择。

    【讨论】:

    • 我知道我不应该只是评论说谢谢,但这真是太棒了。我很敬畏。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-03
    • 1970-01-01
    • 1970-01-01
    • 2012-04-30
    • 2020-07-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多