磁铁模式和重载方法

时间:2015-08-18 05:42:57

标签: scala implicit

对于非重载和重载方法,Scala如何解析“Magnet Pattern”的隐式转换有很大差异。

假设有一个特征Apply(“磁铁模式”的变体)实现如下。

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

现在我们创建一个特征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$1@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$1@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$1@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使其按预期隐式转换。

所以我的问题是为什么?为什么应用隐式转换的范围取决于类中是否存在重载方法的事实。

1 个答案:

答案 0 :(得分:17)

现在,这是一个棘手的问题。它实际上非常棒,我不知道“懒惰隐含不会覆盖整个块”问题的“解决方法”。谢谢你!

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

类型推断和预期类型

首先,我们必须知道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节点进行类型检查时,类型检查器不会空手而归。它从“上方”(在这种情况下,函数应用程序节点)接收期望的类型。在上面的示例中对lambda x => x + 1进行类型检查时,期望的类型为Int => Int,因为我们知道foo期望的参数类型。这会将类型推断驱动为参数Int的推断x,这反过来允许类型检查x + 1

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

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

并且typechecker仍然可以推断出x: Int。这是因为,当对块{ ... }进行类型检查时,预期的类型Int => Int会向下传递到最后一个表达式的类型检查,即x => x + y

隐式转换和预期类型

现在,我们必须在混合中引入隐式转换。当对节点进行类型检查时,会生成类型为T的值,但该节点的预期类型为U,其中T <: U为false,则类型检查器会查找隐式T => U(I这可能在这里简化了一些事情,但要点仍然是真的。这就是你的第一个例子不起作用的原因。让我们仔细看看:

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)。我们可以像这样“写”这个类型检查“状态”:

{
  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)
                  ^

在这种情况下,类型检查器未能找出预期的类型会导致不推断参数类型。

如果我们对您的问题采取“解决方案”,我们会有不同的后果:

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

只有现在,有了一个已经有类型的参数,它会尝试解决重载问题。由于没有任何重载直接符合,因此它会尝试将Int的隐式转换应用于Apply[Int]Symbol。它找到fromLazyVal[Int],它将应用于整个参数。它不再将它推入块内,给出:

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

在这种情况下,整个块都是lazified。

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