磁铁模式和重载方法
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
}
}
现在我们创建的性状Foo
具有单一apply
采取的一个实例Apply
,所以我们可以通过它任意类型的任何值A
因为从那里的隐式转换A => Apply[A]
trait Foo[A] {
def apply(a: Apply[A]): A = a()
}
我们可以确保它可以按照预期的方式使用REPL和这个解决方法去除糖Scala代码。
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")
)
这很好,但假设我们将一个复杂的表达式(with ;
)传递给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
),而不适用于整个表达式。 所以,在我们将它传递给apply
方法的那一刻, i = i + 1
被严格评估,这不是我们所期望的。
好消息(或坏消息)。 我们可以让scalac
在隐式转换中使用整个表达式。 所以i = i + 1
将按照预期延期评估。 为此,我们( Foo.apply
, Foo.apply
!)添加一个重载方法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
i = i + 1; i
根据预期在隐式转换下完成了它。
所以我的问题是为什么呢? 为什么应用隐式转换的范围取决于类中是否存在重载方法的事实。
现在,这是一个棘手的问题。 实际上它非常棒,我不知道“懒惰隐式”的“解决方法”并未涵盖完整的阻止问题。 感谢那!
会发生什么与预期类型有关,以及它们如何影响类型推断工作,隐式转换和重载。
键入推断和预期类型
首先,我们必须知道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
预期的参数类型。 这将类型推断驱动为推断参数x
Int
,从而允许检查x + 1
。
预期类型沿特定结构传播,例如块( {}
)以及if
s和match
es的分支。 因此,你也可以用foo
来呼叫
foo({
val y = 1
x => x + y
})
而typechecker仍然能够推断出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
)。 我们可以像这样“写”这个typechecker“状态”:
{
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]
在这种情况下,整个区块都已经过度化了。
这就结束了解释。 总而言之,主要的区别在于,当对该区块进行类型检查时,是否存在预期类型。 对于预期的类型,隐式转换会尽可能地推低,直到i
。 如果没有期望的类型,隐式转换会在整个参数(即整个块)上后验应用。