磁铁模式和重载方法

Vla*_*kov 18 scala implicit

对于非重载和重载方法,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
  }
}
Run Code Online (Sandbox Code Playgroud)

现在我们创建一个Foo具有单个apply实例的特征,Apply因此我们可以将任意类型的值传递给它,A因为存在隐式转换A => Apply[A].

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
}
Run Code Online (Sandbox Code Playgroud)

我们可以确保它使用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")
)
Run Code Online (Sandbox Code Playgroud)

这很好用,但假设我们将一个复杂的表达式(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)
})
Run Code Online (Sandbox Code Playgroud)

我们可以看到,隐式转换仅应用于复杂表达式的最后部分(即i),而不是整个表达式.因此,i = i + 1在我们将其传递给apply方法的时刻进行了严格评估,这不是我们所期望的.

好消息(或坏消息).我们可以scalac在隐式转换中使用整个表达式.所以i = i + 1将按照预期懒洋洋地进行评估.要做到这一点,我们(surprize,surprize!)我们添加一个Foo.apply任何类型的重载方法,但不是Apply.

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
  def apply(s: Symbol): Foo[A] = this
}
Run Code Online (Sandbox Code Playgroud)

然后.

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
}))
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,整个表达式i = i + 1; i使其在预期的隐式转换下进行.

所以我的问题是为什么呢?为什么应用隐式转换的范围取决于类中是否存在重载方法的事实.

sjr*_*jrd 17

现在,这是一个棘手的问题.它实际上非常棒,我不知道"懒惰隐含不会覆盖整个块"问题的"解决方法".感谢那!

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

类型推断和预期类型

首先,我们必须知道Scala中的类型推断是双向的.大多数推理都是自下而上(给定a: Intb: Int推断a + b: Int),但有些事情是自上而下的.例如,推断lambda的参数类型是自上而下的:

def foo(f: Int => Int): Int = f(42)
foo(x => x + 1)
Run Code Online (Sandbox Code Playgroud)

在第二行,解决后foodef foo(f: Int => Int): Int,类型inferencer可以告诉大家,x必须是类型的Int.它是对lambda本身进行类型检查之前这样做的.它将类型信息从函数应用程序传播到lambda,这是一个参数.

自上而下的推断基本上依赖于预期类型的概念.当对节目的AST节点进行类型检查时,类型检查器不会空手而归.它从"上方"(在这种情况下,函数应用程序节点)接收期望的类型.当x => x + 1在上面的例子中对lambda 进行类型检查时Int => Int,预期的类型是,因为我们知道期望的参数类型foo.这会将类型推断驱动为推断Int参数x,从而允许进行类型检查x + 1.

预期类型沿某些构造传播,例如,块({})和ifs和matches 的分支.因此,你也可以打电话foo

foo({
  val y = 1
  x => x + y
})
Run Code Online (Sandbox Code Playgroud)

并且typechecker仍然可以推断x: Int.这是因为,当对块{ ... }进行类型检查时,期望的类型Int => Int被传递给最后一个表达式的类型检查,即x => x + y.

隐式转换和预期类型

现在,我们必须将隐式转换引入混合中.当类型检查的节点产生类型的值T,但该节点的预期类型是U哪里T <: U是假的,typechecker寻找一个隐含的T => U(我这里大概简化事情有点,但关键仍是如此).这就是你的第一个例子不起作用的原因.让我们仔细看看:

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
}

val foo = new Foo[Int] {}
foo({
  i = i + 1
  i
})
Run Code Online (Sandbox Code Playgroud)

调用时foo.apply,参数(即块)的预期类型是Apply[Int](A已经实例化Int).我们可以像这样"写"这个类型检查"状态":

{
  i = i + 1
  i
}: Apply[Int]
Run Code Online (Sandbox Code Playgroud)

这个期望的类型传递给块的最后一个表达式,它给出:

{
  i = i + 1
  (i: Apply[Int])
}
Run Code Online (Sandbox Code Playgroud)

此时,由于i: Int和预期的类型是Apply[Int],typechecker发现了隐式转换:

{
  i = i + 1
  fromLazyVal[Int](i)
}
Run Code Online (Sandbox Code Playgroud)

这导致只是i被夷为平地.

超载和预期类型

好的,有时间在那里抛出超载!当类型检查器看到一个重载方法的应用时,决定一个预期的类型会有更多的麻烦.我们可以通过以下示例看到:

object Foo {
  def apply(f: Int => Int): Int = f(42)
  def apply(f: String => String): String = f("hello")
}

Foo(x => x + 1)
Run Code Online (Sandbox Code Playgroud)

得到:

error: missing parameter type
              Foo(x => x + 1)
                  ^
Run Code Online (Sandbox Code Playgroud)

在这种情况下,类型检查器未能找出期望的类型会导致不推断参数类型.

如果我们对您的问题采取"解决方案",我们会产生不同的后果:

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
})
Run Code Online (Sandbox Code Playgroud)

现在,在对块进行类型检查时,类型检查器没有预期的类型.因此,它将在没有表达式的情况下检查最后一个表达式,并最终将整个块进行类型检查Int:

{
  i = i + 1
  i
}: Int
Run Code Online (Sandbox Code Playgroud)

只有现在,已经有一个已经过类固定的参数,它是否会尝试解决重载问题.由于没有任何重载直接符合,因此它会尝试将隐式转换Int应用于Apply[Int]或者Symbol.它发现fromLazyVal[Int],它适用于整个论证.它不再将它推入块内,给出:

fromLazyVal({
  i = i + 1
  i
}): Apply[Int]
Run Code Online (Sandbox Code Playgroud)

在这种情况下,整个块是lazified.

这结束了解释.总而言之,主要区别在于对块进行类型检查时是否存在预期类型.对于期望的类型,隐式转换被尽可能地向下推,直到只有i.如果没有预期的类型,隐式转换将在整个参数(即整个块)上进行后验.