磁铁模式和重载方法

Magnet pattern and overloaded methods

对于非重载和重载方法,Scala 如何解决来自 "Magnet Pattern" 的隐式转换存在显着差异。

假设有一个特征 Apply("Magnet Pattern" 的变体)实现如下。

trait Apply[A] {
 def apply(): A
}
object Apply {
  implicit def fromLazyVal[A](v: => A): Apply[A] = new Apply[A] {
    def apply(): A = v
  }
}

现在我们创建一个 trait Foo,它有一个 apply 接受 Apply 的实例,所以我们可以传递给它任意类型的任何值 A 因为那里来自 A => Apply[A].

的隐式转换
trait Foo[A] {
  def apply(a: Apply[A]): A = a()
}

我们可以使用 REPL 和 this workaround to de-sugar Scala code.

确保它按预期工作
scala> val foo = new Foo[String]{}
foo: Foo[String] = $anon@3a248e6a

scala> showCode(reify { foo { "foo" } }.tree)
res9: String =    
$line21$read.foo.apply(
  $read.INSTANCE.Apply.fromLazyVal("foo")
)

这很好用,但假设我们将一个 复杂表达式 (带有 ;)传递给 apply 方法。

scala> val foo = new Foo[Int]{}
foo: Foo[Int] = $anon@5645b124

scala> var i = 0
i: Int = 0

scala> showCode(reify { foo { i = i + 1; i } }.tree)
res10: String =
$line23$read.foo.apply({
  $line24$read.`i_=`($line24$read.i.+(1));
  $read.INSTANCE.Apply.fromLazyVal($line24$read.i)
})

正如我们所见,隐式转换仅应用于复杂表达式的最后部分(即 i),而不是整个表达式。因此,i = i + 1 在我们将其传递给 apply 方法时被严格评估,这不是我们一直期望的。

好(或坏)消息。我们可以使 scalac 在隐式转换中使用整个表达式。因此 i = i + 1 将按预期进行延迟评估。为此,我们(惊喜,惊喜!)我们添加了一个重载方法 Foo.apply 可以接受任何类型,但不是 Apply.

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
  def apply(s: Symbol): Foo[A] = this
}

然后。

scala> var i = 0
i: Int = 0

scala> val foo = new Foo[Int]{}
foo: Foo[Int] = $anon@3ff00018

scala> showCode(reify { foo { i = i + 1; i } }.tree)
res11: String =
$line28$read.foo.apply($read.INSTANCE.Apply.fromLazyVal({
  $line27$read.`i_=`($line27$read.i.+(1));
  $line27$read.i
}))

正如我们所见,整个表达式 i = i + 1; i 按照预期在隐式转换下进行。

所以我的问题是为什么会这样?为什么应用隐式转换的范围取决于class.

中是否有重载方法。

现在,这是一个棘手的问题。它实际上非常棒,我不知道 "workaround" 到 "lazy implicit does not cover full block" 问题。谢谢!

所发生的事情与预期类型有关,以及它们如何影响类型推断工作、隐式转换和重载。

类型推断和预期类型

首先,我们必须知道 Scala 中的类型推断是双向的。大多数推理都是自下而上的(给定 a: Intb: Int,推断 a + b: Int),但有些事情是自上而下的。例如,推断 lambda 的参数类型是自上而下的:

def foo(f: Int => Int): Int = f(42)
foo(x => x + 1)

在第二行中,在将 foo 解析为 def foo(f: Int => Int): Int 之后,类型推断器可以判断 x 必须是 Int 类型。它会在 对 lambda 本身进行类型检查之前这样做。它将类型信息从函数应用程序向下传播到作为参数的 lambda。

自上而下的推理基本上依赖于预期类型的概念。在对程序的 AST 节点进行类型检查时,类型检查器不会空手而归。它从 "above"(在本例中为函数应用程序节点)接收预期类型。在对上例中的 lambda x => x + 1 进行类型检查时,期望的类型是 Int => Int,因为我们知道 foo 期望的参数类型。这驱动类型推断为参数 x 推断 Int,这反过来允许类型检查 x + 1.

预期类型向下传播到某些结构,例如,块 ({}) 和 ifs 和 matches 的分支。因此,您也可以使用

调用 foo
foo({
  val y = 1
  x => x + y
})

并且类型检查器仍然能够推断出 x: Int。这是因为,当对块 { ... } 进行类型检查时,预期类型 Int => Int 被传递给最后一个表达式的类型检查,即 x => x + y.

隐式转换和预期类型

现在,我们必须将隐式转换引入组合中。当对节点进行类型检查时产生类型为 T 的值,但该节点的预期类型为 U,其中 T <: U 为假,类型检查器查找隐式 T => U(我可能在这里稍微简化了一些事情,但要点仍然是正确的)。这就是为什么您的第一个示例不起作用的原因。让我们仔细看看:

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
}

val foo = new Foo[Int] {}
foo({
  i = i + 1
  i
})

调用 foo.apply 时,参数(即块)的预期类型为 Apply[Int]A 已实例化为 Int)。我们可以 "write" 这个类型检查器 "state" 像这样:

{
  i = i + 1
  i
}: Apply[Int]

此预期类型向下传递 到块的最后一个表达式,它给出:

{
  i = i + 1
  (i: Apply[Int])
}

此时,由于i: Int且预期类型为Apply[Int],类型检查器发现隐式转换:

{
  i = i + 1
  fromLazyVal[Int](i)
}

这只会导致 i 被惰化。

重载和预期类型

好的,是时候加入重载了!当类型检查器看到重载方法的应用程序时,它在确定预期类型时会遇到更多麻烦。我们可以通过以下示例看到:

object Foo {
  def apply(f: Int => Int): Int = f(42)
  def apply(f: String => String): String = f("hello")
}

Foo(x => x + 1)

给出:

error: missing parameter type
              Foo(x => x + 1)
                  ^

在这种情况下,类型检查器无法确定预期类型会导致无法推断出参数类型。

如果我们将您的 "solution" 用于您的问题,我们会有不同的结果:

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
  def apply(s: Symbol): Foo[A] = this
}

val foo = new Foo[Int] {}
foo({
  i = i + 1
  i
})

现在,在对块进行类型检查时,类型检查器没有预期的类型 可以使用。因此它将对最后一个没有表达式的表达式进行类型检查,并最终将整个块类型检查为 Int:

{
  i = i + 1
  i
}: Int

直到现在,对于已经过类型检查的参数,它才尝试解决重载问题。由于 none 的重载直接符合,它尝试应用从 IntApply[Int]Symbol 的隐式转换。它找到 fromLazyVal[Int],并将其应用 到整个参数 。它不再将它推入块内,给出:

fromLazyVal({
  i = i + 1
  i
}): Apply[Int]

在这种情况下,整个块都是延迟的。

说明到此结束。总而言之,主要区别在于在对块进行类型检查时是否存在预期类型。对于预期的类型,隐式转换被尽可能地向下推,向下推到 i。如果没有预期的类型,隐式转换将事后应用于整个参数,即整个块。