Scala REPL の再帰的オーバーロード セマンティクス - JVM 言語
-
02-07-2019 - |
質問
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:」でクローズされました。これら 2 つのメソッドは一緒にコンパイルする必要があるため、インタプリタのセマンティクスを考慮すると、これをサポートできる方法がわかりません。」彼は、これらのメソッドを囲むオブジェクトに入れることを推奨しました。
その理由を説明できる JVM 言語実装または Scala の専門家はいますか?たとえば、メソッドが相互に呼び出し合った場合に問題が発生するのはわかりますが、この場合はどうでしょうか。
または、これは質問が大きすぎて、さらに前提条件の知識が必要だと思われる場合は、言語実装、特に JVM に関する書籍やサイトへの適切なリンクを持っている人はいますか?(私は John Rose のブログと、Programming Language Pragmatics という本については知っています...しかしそれだけです。:)
解決
この問題は、通訳者がほとんどの場合、次のことを行う必要があるという事実によるものです。 交換する 既存の要素をオーバーロードするのではなく、指定された名前を付けます。たとえば、私はよく何かを実験し、次のようなメソッドを作成します。 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 コンパイラーの制限ではなく、意図的な設計上の決定です。バグで述べたように、ルート スコープ以外の範囲内にいる場合でもオーバーロードすることができます。テストメソッドをクラス内に囲むことが私にとって最良の解決策のように思えます。
他のヒント
% 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")
()
REPL は、両方の行をコピーして同時に貼り付けると受け入れます。
で示されているように、 即席の 答えは、過負荷になる可能性があります。 ダニエルさんの デザインの決定に関するコメントは正しいですが、不完全で少し誤解を招きやすいと思います。ないよ 非合法化する (可能性があるため) 過負荷の可能性がありますが、簡単に達成できるものではありません。
これにつながる設計上の決定は次のとおりです。
- 以前のすべての定義が使用可能である必要があります。
- 毎回入力されたすべてのコードを再コンパイルするのではなく、新しく入力されたコードのみがコンパイルされます。
- (ダニエルが述べたように) 定義を再定義することが可能でなければなりません。
- クラスやオブジェクトだけでなく、valsやdefsなどのメンバーも定義できる必要があります。
問題は...これらすべての目標を達成するにはどうすればよいでしょうか?あなたの例をどのように処理しますか?
def foo(x: Int): Unit = {}
def foo(x: String): Unit = {println(foo(2))}
4番目の項目Aから val
または def
内でのみ定義できます class
, trait
, object
または package 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 の仕組みにより、それらの 1 つを定義すると、それらを拡張することはできません。もちろん、すべてを再コンパイルすることもできますが、それは破棄しました。したがって、別の場所に配置する必要があります。
package $line1 { // input line
object $read { // what was read
object $iw { // definitions
def foo(x: String): Unit = { println(foo(2)) }
}
}
}
これは、サンプルがオーバーロードではない理由を説明しています。これらは 2 つの異なる場所で定義されています。これらを同じ行に置くと、extempore の例に示すように、すべて一緒に定義されてしまい、オーバーロードになってしまいます。
他の設計上の決定に関しては、新しいパッケージはそれぞれ以前のパッケージから定義と「res」をインポートします。インポートは相互にシャドウし合うことができるため、内容を「再定義」することが可能になります。