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

时间:2008-09-23 12:35:49

标签: scala recursion jvm jvm-languages

使用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上? (我知道约翰罗斯的博客,以及编程语言语用学一书......但这就是它。:)

4 个答案:

答案 0 :(得分:11)

问题在于,解释器通常必须用给定名称替换现有元素,而不是重载它们。例如,我经常会尝试使用某些东西,通常会创建一个名为test的方法:

def test(x: Int) = x + x

稍后,假设我正在运行不同的实验,我创建了另一个名为test的方法,与第一个方法无关:

def test(ls: List[Int]) = (0 /: ls) { _ + _ }

这不是一个完全不切实际的场景。事实上,正是大多数人使用解释器的方式,通常都没有意识到。如果解释器任意决定将test的两个版本都保留在范围内,那么可能会导致使用测试时的语义差异混乱。例如,我们可能会调用test,意外传递Int而不是List[Int](不是世界上最不可能发生的事故):

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

随着时间的推移,解释器的根范围将变得非常混乱,各种版本的方法,字段等。我倾向于让我的解释器一次打开几天,但如果允许这样的重载,我们将是因为事情变得太混乱,所以经常被迫“冲洗”翻译。

这不是JVM或Scala编译器的限制,而是一个深思熟虑的设计决策。正如bug中所提到的,如果你在root范围之外的其他东西,你仍然可以重载。将测试方法包含在一个类中对我来说似乎是最好的解决方案。

答案 1 :(得分: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")
()

答案 2 :(得分:4)

如果您复制两行并且同时粘贴,则REPL将接受。

答案 3 :(得分:1)

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

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

  1. 以前的所有定义都必须可用。
  2. 只编译新输入的代码,而不是重新编译每次输入的所有内容。
  3. 必须有可能重新定义定义(如Daniel所述)。
  4. 必须能够定义诸如val和defs之类的成员,而不仅仅是类和对象。
  5. 问题是......如何实现所有这些目标?我们如何处理您的示例?

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

    从第4项开始,只能在valdefclasstrait内定义objectpackage 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)) }
        }
      }
    }
    

    这解释了为什么你的例子不是重载:它们在两个不同的地方定义。如果你把它们放在同一行,那么它们都会被一起定义,这会使它们重载,如示例中所示。

    至于其他设计决策,每个新包导入定义和以前包中的“res”,导入可以相互影响,这样就可以“重新定义”东西。