磁铁模式和重载方法

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.applyFoo.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: Intb: 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 。 如果没有期望的类型,隐式转换会在整个参数(即整个块)上后验应用。

链接地址: http://www.djcxy.com/p/78953.html

上一篇: Magnet pattern and overloaded methods

下一篇: Implicit jsonFormat for case class with varargs