首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >磁体模式和重载方法

磁体模式和重载方法
EN

Stack Overflow用户
提问于 2015-08-18 13:42:57
回答 1查看 1.7K关注 0票数 19

对于非重载和重载的方法,Scala从"Magnet Pattern“解析隐式转换的方式有很大的不同。

假设有如下实现的特征Apply (“磁体模式”的变体)。

代码语言:javascript
复制
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]的隐式转换。

代码语言:javascript
复制
trait Foo[A] {
  def apply(a: Apply[A]): A = a()
}

我们可以使用REPL和this workaround to de-sugar Scala code来确保它像预期的那样工作。

代码语言:javascript
复制
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")
)

这很有效,但是假设我们将一个复杂的表达式(带有;)传递给apply方法。

代码语言:javascript
复制
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将像预期的那样缓慢地进行评估。为了做到这一点,我们(出人意料,出人意料!)我们添加了一个重载方法Foo.apply,它接受任何类型,但不接受Apply

代码语言:javascript
复制
trait Foo[A] {
  def apply(a: Apply[A]): A = a()
  def apply(s: Symbol): Foo[A] = this
}

然后。

代码语言:javascript
复制
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如预期的那样在隐式转换下完成。

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

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2015-08-18 17:31:59

现在,这是一个棘手的问题。这真的很棒,我不知道“惰性隐式不能覆盖完整块”问题的“解决办法”。谢谢你这么说!

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

类型推断和预期类型

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

代码语言:javascript
复制
def foo(f: Int => Int): Int = f(42)
foo(x => x + 1)

在第二行中,在将foo解析为def foo(f: Int => Int): Int之后,类型推断器可以判断x必须是Int类型。在对lambda本身进行类型检查之前,它会这样做。它将类型信息从函数应用程序向下传播到lambda,这是一个参数。

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

预期的类型向下传播某些构造,例如,块({})以及ifmatches的分支。因此,您还可以使用以下命令调用foo

代码语言:javascript
复制
foo({
  val y = 1
  x => x + y
})

而且类型检查器仍然能够推断出x: Int。这是因为,当检查块{ ... }的类型时,期望的类型Int => Int将传递给最后一个表达式的类型检查,即x => x + y

隐式转换和预期类型

现在,我们必须在混合中引入隐式转换。当类型检查一个节点产生一个类型为T的值,但该节点的预期类型是U,其中T <: U为false时,类型检查器将查找隐式T => U (我可能在这里稍微简化了一些,但要点仍然是真的)。这就是你的第一个例子不起作用的原因。让我们仔细看看:

代码语言:javascript
复制
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)。我们可以像这样“编写”这个类型检查器的“状态”:

代码语言:javascript
复制
{
  i = i + 1
  i
}: Apply[Int]

此预期类型将传递给块的最后一个表达式,该表达式提供:

代码语言:javascript
复制
{
  i = i + 1
  (i: Apply[Int])
}

此时,由于i: Int和预期类型为Apply[Int],类型检查器会找到隐式转换:

代码语言:javascript
复制
{
  i = i + 1
  fromLazyVal[Int](i)
}

这只会导致i被延迟。

重载和预期类型

好了,是时候在这里抛出重载了!当类型检查器看到重载方法的应用程序时,它在决定预期的类型时会遇到更多的麻烦。我们可以通过下面的例子看到这一点:

代码语言:javascript
复制
object Foo {
  def apply(f: Int => Int): Int = f(42)
  def apply(f: String => String): String = f("hello")
}

Foo(x => x + 1)

提供:

代码语言:javascript
复制
error: missing parameter type
              Foo(x => x + 1)
                  ^

在这种情况下,类型检查器找不到预期的类型会导致无法推断参数类型。

如果我们采用你的“解决方案”来解决你的问题,我们会有不同的结果:

代码语言:javascript
复制
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进行类型检查

代码语言:javascript
复制
{
  i = i + 1
  i
}: Int

直到现在,通过已经检查类型的参数,它才尝试解决重载问题。由于没有一个重载直接符合,它会尝试应用从IntApply[Int]Symbol的隐式转换。它找到fromLazyVal[Int],并将其应用于整个参数。它不再将其推入块中,给出:

代码语言:javascript
复制
fromLazyVal({
  i = i + 1
  i
}): Apply[Int]

在这种情况下,整个块都是惰性的。

解释到此结束。总而言之,主要的区别是在类型检查块时是否存在预期的类型。对于预期的类型,隐式转换被尽可能地下推,下推到只有i。如果没有预期的类型,隐式转换将后验地应用于整个参数,即整个块。

票数 18
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/32064375

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档