磁铁模式和重载方法
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 。 如果没有期望的类型,隐式转换会在整个参数(即整个块)上后验应用。
