Scala 磁模式与过载方法

Scala 磁模式与过载方法,scala,implicit,Scala,Implicit,对于非重载方法和重载方法,Scala如何解析来自“磁铁模式”的隐式转换有着显著的区别 假设有一个traitApply(一个“磁铁模式”的变体)实现如下 trait Apply[A] { def apply(): A } object Apply { implicit def fromLazyVal[A](v: => A): Apply[A] = new Apply[A] { def apply(): A = v } } 现在我们创建了一个traitFoo,它有一个app

对于非重载方法和重载方法,Scala如何解析来自“磁铁模式”的隐式转换有着显著的区别

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

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
实例,因此我们可以传递任意类型的值
a
,因为有一个从
a=>apply[a]
的隐式转换

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
}
我们可以使用REPL和确保它按预期工作

这非常有效,但是假设我们将一个复杂的表达式(带有
)传递给
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
将按照预期进行延迟评估。为此,我们(supprize,supprize!)添加了一个重载方法
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;我
按预期进行了隐式转换


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

现在,这是一个棘手的问题。它实际上非常棒,我不知道“lazy隐式”的“变通方法”不包括完整块问题。谢谢你

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

类型推断和预期类型 首先,我们必须知道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,lambda是一个参数

自上而下的推理基本上依赖于预期类型的概念。在对程序的AST节点进行打字检查时,typechecker不会空手启动。它从“上面”(在本例中为函数应用程序节点)接收预期类型。在上面的示例中,当类型检查lambda
x=>x+1
时,预期的类型是
Int=>Int
,因为我们知道
foo
预期的参数类型。这将驱动类型推断,为参数
x
推断
Int
,从而允许类型检查
x+1

预期类型向下传播到某些构造,例如块(
{}
)和
if
s和
match
es的分支。因此,您也可以使用

foo({
  val y = 1
  x => x + y
})
而且类型检查器仍然能够推断
x:Int
。这是因为,在对块
{…}
进行类型检查时,预期的类型
Int=>Int
被传递到最后一个表达式的类型检查,即
x=>x+y

隐式转换和预期类型 现在,我们必须在混合中引入隐式转换。当类型检查时,一个节点产生一个类型为
T
的值,但该节点的预期类型是
U
,其中
tu
(我可能在这里简化了一些,但要点仍然正确)。这就是为什么您的第一个示例不起作用。让我们仔细看看:

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
被延迟

重载和预期类型 好的,是时候在那里抛出重载了!当typechecker看到重载方法的应用程序时,它在确定预期类型时会遇到更多的麻烦。我们可以通过以下示例看到:

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)
                  ^
在这种情况下,typechecker未能找出预期类型会导致无法推断参数类型

如果我们采用您的“解决方案”解决您的问题,我们会产生不同的后果:

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
})
现在,在对块进行类型检查时,typechecker没有可使用的预期类型。因此,它将对没有表达式的最后一个表达式进行类型检查,并最终将整个块作为
Int
进行类型检查:

{
  i = i + 1
  i
}: Int
直到现在,对于一个已经检查过类型的参数,它才尝试解析重载。由于没有任何重载直接符合,因此它尝试应用从
Int
apply[Int]
Symbol
的隐式转换。它从lazyval[Int]中查找
,并将其应用于整个参数。它不再将其推到块内,提供:

fromLazyVal({
  i = i + 1
  i
}): Apply[Int]
在这种情况下,整个块被延迟

解释到此结束。总而言之,主要区别在于在对块进行类型检查时,是否存在预期类型。对于预期的类型,隐式转换被向下推
fromLazyVal({
  i = i + 1
  i
}): Apply[Int]