Scala REPL中的递归重载语义 - JVM语言

8

使用Scala的命令行REPL:

def foo(x: Int): Unit = {}
def foo(x: String): Unit = {println(foo(2))}

提供

error: type mismatch;
found: Int(2)
required: String

似乎在REPL中无法定义重载递归方法。我认为这是Scala REPL中的一个错误,并进行了报告,但几乎立即被关闭并标记为“wontfix”。他解释说:“考虑到编译器的语义,我看不出如何支持这个功能,因为这两个方法必须一起编译。”他建议将方法放入一个封闭对象中。
有没有JVM语言实现或Scala专家可以解释原因?我可以看到如果方法相互调用,这将是一个问题,但在这种情况下呢?
或者如果这个问题太大,你认为我需要更多的先决知识,是否有人有任何好的书籍或网站链接,特别是关于JVM上的语言实现(我知道John Rose的博客和《编程语言实践》这本书...但就只有这些了:)
4个回答

11
问题是因为解释器通常需要替换具有给定名称的现有元素,而不是对它们进行重载。例如,我经常通过尝试运行来测试某些内容,通常会创建一个名为test的方法。
def test(x: Int) = x + x

稍后假设我运行了一个不同的实验并创建了另一个名为test的方法,与第一个方法无关。
def test(ls: List[Int]) = (0 /: ls) { _ + _ }

这并不是一个完全不现实的场景。 实际上,这正是大多数人使用解释器的方式,通常甚至没有意识到这一点。 如果解释器任意决定在作用域中保留test的两个版本,则可能导致在使用test时出现令人困惑的语义差异。 例如,我们可能会调用test,无意中传递了Int而不是List[Int](世界上最不可能发生的事情之一):

test(1 :: Nil)  // => 1
test(2)         // => 4  (expecting 2)

随着时间的推移,解释器的根作用域会变得非常混乱,有各种不同版本的方法、字段等。我经常让我的解释器开放几天,但如果允许这样的重载,随着事情变得越来越混乱,我们将被迫定期"刷新"解释器。
这不是JVM或Scala编译器的限制,而是一个故意的设计决策。正如在该错误中提到的那样,如果你在除根作用域之外的其他地方内部工作,仍然可以进行重载。将测试方法封装在类中似乎是最好的解决方案。

5
% scala28
Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_20).
Type in expressions to have them evaluated.
Type :help for more information.

scala> def foo(x: Int): Unit = () ; def foo(x: String): Unit = { println(foo(2)) } 
foo: (x: String)Unit <and> (x: Int)Unit
foo: (x: String)Unit <and> (x: Int)Unit

scala> foo(5)

scala> foo("abc")
()

4

如果您同时复制并粘贴这两行代码,REPL将会接受。


1

正如extempore's的回答所示,重载是可能的。Daniel's关于设计决策的评论是正确的,但我认为它不完整且有点误导。没有禁止重载(因为它们是可能的),但它们不容易实现。

导致这种情况的设计决策是:

  1. 所有先前的定义必须可用。
  2. 只编译新输入的代码,而不是每次重新编译所有已输入的内容。
  3. 必须能够重新定义定义(如Daniel所提到的)。
  4. 必须能够定义成员,例如vals和defs,而不仅仅是类和对象。

问题是...如何实现所有这些目标?我们如何处理您的示例?

def foo(x: Int): Unit = {}
def foo(x: String): Unit = {println(foo(2))}

从第四项开始,valdef只能在classtraitobjectpackage object内定义。因此,REPL将定义放在对象内,如下所示(不是实际表示!
package $line1 { // input line
  object $read { // what was read
    object $iw { // definitions
      def foo(x: Int): Unit = {}
    }
    // val res1 would be here somewhere if this was an expression
  }
}

现在,由于JVM的工作方式,一旦您定义了其中一个,就不能扩展它们。当然,您可以重新编译所有内容,但我们放弃了那种做法。因此,您需要将其放置在不同的位置:

package $line1 { // input line
  object $read { // what was read
    object $iw { // definitions
      def foo(x: String): Unit = { println(foo(2)) }
    }
  }
}

这就解释了为什么你的例子不是重载:它们在两个不同的位置定义。如果你把它们放在同一行,它们将被一起定义,这将使它们成为重载,就像 extempore 的例子所示。

至于其他设计决策,每个新包都会导入前面包的定义和“res”,而导入可能会互相遮盖,这使得“重新定义”东西成为可能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接