磁铁模式和重载方法
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: 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 节点进行类型检查时,类型检查器不会空手而归。它从 "above"(在本例中为函数应用程序节点)接收预期类型。在对上例中的 lambda x => x + 1
进行类型检查时,期望的类型是 Int => Int
,因为我们知道 foo
期望的参数类型。这驱动类型推断为参数 x
推断 Int
,这反过来允许类型检查 x + 1
.
预期类型向下传播到某些结构,例如,块 ({}
) 和 if
s 和 match
es 的分支。因此,您也可以使用
调用 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 的重载直接符合,它尝试应用从 Int
到 Apply[Int]
或 Symbol
的隐式转换。它找到 fromLazyVal[Int]
,并将其应用 到整个参数 。它不再将它推入块内,给出:
fromLazyVal({
i = i + 1
i
}): Apply[Int]
在这种情况下,整个块都是延迟的。
说明到此结束。总而言之,主要区别在于在对块进行类型检查时是否存在预期类型。对于预期的类型,隐式转换被尽可能地向下推,向下推到 i
。如果没有预期的类型,隐式转换将事后应用于整个参数,即整个块。
对于非重载和重载方法,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: 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 节点进行类型检查时,类型检查器不会空手而归。它从 "above"(在本例中为函数应用程序节点)接收预期类型。在对上例中的 lambda x => x + 1
进行类型检查时,期望的类型是 Int => Int
,因为我们知道 foo
期望的参数类型。这驱动类型推断为参数 x
推断 Int
,这反过来允许类型检查 x + 1
.
预期类型向下传播到某些结构,例如,块 ({}
) 和 if
s 和 match
es 的分支。因此,您也可以使用
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 的重载直接符合,它尝试应用从 Int
到 Apply[Int]
或 Symbol
的隐式转换。它找到 fromLazyVal[Int]
,并将其应用 到整个参数 。它不再将它推入块内,给出:
fromLazyVal({
i = i + 1
i
}): Apply[Int]
在这种情况下,整个块都是延迟的。
说明到此结束。总而言之,主要区别在于在对块进行类型检查时是否存在预期类型。对于预期的类型,隐式转换被尽可能地向下推,向下推到 i
。如果没有预期的类型,隐式转换将事后应用于整个参数,即整个块。