对于非重载和重载方法,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
使其按预期隐式转换。
所以我的问题是为什么?为什么应用隐式转换的范围取决于类中是否存在重载方法的事实。
答案 0 :(得分:17)
现在,这是一个棘手的问题。它实际上非常棒,我不知道“懒惰隐含不会覆盖整个块”问题的“解决方法”。谢谢你!
发生的事情与预期类型有关,以及它们如何影响类型推断工作,隐式转换和重载。
首先,我们必须知道Scala中的类型推断是双向的。大多数推理自下而上(给定a: Int
和b: 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
。
预期类型向下传播某些结构,例如块({}
)以及if
和match
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
。如果没有预期的类型,隐式转换将在整个参数上应用后验,即整个块。