Warum verbietet der Scala-Compiler überladene Methoden mit Standardargumenten?

147

Während es gültige Fälle geben kann, in denen solche Methodenüberladungen mehrdeutig werden können, warum verbietet der Compiler Code, der weder zur Kompilierungszeit noch zur Laufzeit mehrdeutig ist?

Beispiel:

// This fails:
def foo(a: String)(b: Int = 42) = a + b
def foo(a: Int)   (b: Int = 42) = a + b

// This fails, too. Even if there is no position in the argument list,
// where the types are the same.
def foo(a: Int)   (b: Int = 42) = a + b
def foo(a: String)(b: String = "Foo") = a + b

// This is OK:
def foo(a: String)(b: Int) = a + b
def foo(a: Int)   (b: Int = 42) = a + b    

// Even this is OK.
def foo(a: Int)(b: Int) = a + b
def foo(a: Int)(b: String = "Foo") = a + b

val bar = foo(42)_ // This complains obviously ...

Gibt es Gründe, warum diese Einschränkungen nicht ein wenig gelockert werden können?

Insbesondere beim Konvertieren von stark überlastetem Java-Code in Scala sind Standardargumente sehr wichtig, und es ist nicht schön herauszufinden, dass der Spec / Compiler nach dem Ersetzen vieler Java-Methoden durch eine Scala-Methode willkürliche Einschränkungen auferlegt.

soc
quelle
18
"willkürliche Einschränkungen" :-)
KajMagnus
1
Es sieht so aus, als könnten Sie das Problem mithilfe von Typargumenten umgehen. Dies kompiliert:object Test { def a[A](b: Int, c: Int, d: Int = 7): Unit = {}; def a[A](a:String, b: String = ""): Unit = {}; a(2,3,4); a("a");}
user1609012
@ user1609012: Dein Trick hat bei mir nicht funktioniert. Ich habe es mit Scala 2.12.0 und Scala 2.11.8 ausprobiert.
Landlocked Surfer
4
IMHO ist dies einer der stärksten Schmerzpunkte in Scala. Wenn ich versuche, eine flexible API bereitzustellen, tritt dieses Problem häufig auf, insbesondere beim Überladen von apply () des Begleitobjekts. Obwohl ich etwas Scala über Kotlin bevorzugen, in Kotlin können Sie diese Art tun Überlastung ...
kubisch Salat

Antworten:

113

Ich möchte Lukas Rytz (von hier ) zitieren :

Der Grund ist, dass wir ein deterministisches Namensschema für die generierten Methoden wollten, die Standardargumente zurückgeben. Wenn du schreibst

def f(a: Int = 1)

Der Compiler generiert

def f$default$1 = 1

Wenn Sie zwei Überladungen mit Standardwerten an derselben Parameterposition haben, benötigen wir ein anderes Namensschema. Wir möchten den generierten Bytecode jedoch über mehrere Compilerläufe hinweg stabil halten.

Eine Lösung für die zukünftige Scala Version könnte einzuarbeiten Typnamen der Nicht-Standard - Argumente (die am Anfang eines Verfahrens, welche Versionen überlastet disambiguate) in das Namensschema, zB in diesem Fall:

def foo(a: String)(b: Int = 42) = a + b
def foo(a: Int)   (b: Int = 42) = a + b

es wäre so etwas wie:

def foo$String$default$2 = 42
def foo$Int$default$2 = 42

Jemand, der bereit ist , einen SIP-Vorschlag zu schreiben ?

Eugen Labun
quelle
2
Ich denke, Ihr Vorschlag hier macht sehr viel Sinn, und ich sehe nicht, was so komplex wäre, ihn zu spezifizieren / umzusetzen. Im Wesentlichen sind die Parametertypen Teil der Funktions-ID. Was macht der Compiler derzeit mit foo (String) und foo (Int) (dh überladenen Methoden OHNE Standard)?
Mark
Würde dies nicht effektiv die obligatorische ungarische Notation einführen, wenn auf Scala-Methoden von Java aus zugegriffen wird? Es scheint, als würde dies die Schnittstellen extrem zerbrechlich machen und den Benutzer dazu zwingen, vorsichtig zu sein, wenn sich Typparameter für Funktionen ändern.
blast_hardcheese
Und was ist mit komplexen Typen? A with B, zum Beispiel?
blast_hardcheese
66

Es wäre sehr schwierig, eine lesbare und präzise Spezifikation für die Wechselwirkungen der Überladungsauflösung mit Standardargumenten zu erhalten. Natürlich ist es für viele Einzelfälle, wie den hier vorgestellten, leicht zu sagen, was passieren soll. Das reicht aber nicht. Wir brauchen eine Spezifikation, die alle möglichen Eckfälle entscheidet. Die Überladungsauflösung ist bereits sehr schwer anzugeben. Das Hinzufügen von Standardargumenten in der Mischung würde es noch schwieriger machen. Deshalb haben wir uns entschieden, die beiden zu trennen.

Martin Odersky
quelle
4
Danke für deine Antwort. Was mich wahrscheinlich verwirrte, war, dass sich der Compiler im Grunde überall sonst nur beschwert, wenn tatsächlich Unklarheiten bestehen. Aber hier beschwert sich der Compiler, weil es ähnliche Fälle geben kann, in denen Mehrdeutigkeiten auftreten können. Im ersten Fall beschwert sich der Compiler also nur, wenn ein nachgewiesenes Problem vorliegt, im zweiten Fall ist das Compilerverhalten jedoch viel ungenauer und löst Fehler für "scheinbar gültigen" Code aus. Dies mit dem Prinzip des geringsten Erstaunens zu sehen, ist ein bisschen unglücklich.
Soc
2
Bedeutet "Es wäre sehr schwierig, eine lesbare und genaue Spezifikation zu erhalten [...]", dass die tatsächliche Wahrscheinlichkeit besteht, dass sich die aktuelle Situation verbessert, wenn jemand eine gute Spezifikation und / oder Implementierung vornimmt? Die aktuelle Situation imho schränkt die Verwendbarkeit von benannten / Standardparametern ziemlich stark ein ...
soc
Es gibt ein Verfahren zum Vorschlagen von Änderungen an der Spezifikation. scala-lang.org/node/233
James Iry
2
Ich habe einige Kommentare (siehe meine Kommentare unter der verknüpften Antwort) über Scala, die Überlastung verpönt macht, und über einen Bürger zweiter Klasse. Wenn wir die Überlastung in Scala weiterhin absichtlich schwächen, ersetzen wir die Eingabe durch Namen, was meiner Meinung nach eine regressive Richtung ist.
Shelby Moore III
10
Wenn Python das kann, sehe ich keinen guten Grund, warum Scala das nicht konnte. Das Argument für Komplexität ist gut: Durch die Implementierung dieser Funktion wird Scale aus Anwendersicht weniger komplex. Wenn Sie andere Antworten lesen, werden Sie sehen, wie Menschen sehr komplexe Dinge erfinden, um ein Problem zu lösen, das aus Sicht der Benutzer nicht einmal existieren sollte.
Richard Gomes
12

Ich kann Ihre Frage nicht beantworten, aber hier ist eine Problemumgehung:

implicit def left2Either[A,B](a:A):Either[A,B] = Left(a)
implicit def right2Either[A,B](b:B):Either[A,B] = Right(b)

def foo(a: Either[Int, String], b: Int = 42) = a match {
  case Left(i) => i + b
  case Right(s) => s + b
}

Wenn Sie zwei sehr lange Arg-Listen haben, die sich nur in einem Arg unterscheiden, ist es möglicherweise die Mühe wert ...

Landei
quelle
1
Nun, ich habe versucht, Standardargumente zu verwenden, um meinen Code präziser und lesbarer zu machen. In einem Fall habe ich der Klasse eine implizite Konvertierung hinzugefügt, bei der nur der alternative Typ in den akzeptierten Typ konvertiert wurde. Es fühlt sich einfach hässlich an. Und der Ansatz mit den Standardargumenten sollte einfach funktionieren!
Soc
Sie sollten mit solchen Umwandlungen vorsichtig sein, da sie für alle Verwendungen gelten Eitherund nicht nur für foo- diese Weise , wenn ein Either[A, B]Wert angefordert wird, beide Aund Bakzeptiert werden. Man sollte stattdessen einen Typ definieren, der nur von Funktionen mit Standardargumenten (wie foohier) akzeptiert wird , wenn man in diese Richtung gehen möchte; Natürlich wird noch weniger klar, ob dies eine bequeme Lösung ist.
Blaisorblade
9

Was für mich funktioniert hat, ist die Neudefinition (Java-Stil) der Überladungsmethoden.

def foo(a: Int, b: Int) = a + b
def foo(a: Int, b: String) = a + b
def foo(a: Int) = a + "42"
def foo(a: String) = a + "42"

Dies stellt dem Compiler sicher, welche Auflösung Sie gemäß den aktuellen Parametern wünschen.

belka
quelle
3

Hier ist eine Verallgemeinerung der @ Landi-Antwort:

Was Sie wirklich wollen:

def pretty(tree: Tree, showFields: Boolean = false): String = // ...
def pretty(tree: List[Tree], showFields: Boolean = false): String = // ...
def pretty(tree: Option[Tree], showFields: Boolean = false): String = // ...

Workarround

def pretty(input: CanPretty, showFields: Boolean = false): String = {
  input match {
    case TreeCanPretty(tree)       => prettyTree(tree, showFields)
    case ListTreeCanPretty(tree)   => prettyList(tree, showFields)
    case OptionTreeCanPretty(tree) => prettyOption(tree, showFields)
  }
}

sealed trait CanPretty
case class TreeCanPretty(tree: Tree) extends CanPretty
case class ListTreeCanPretty(tree: List[Tree]) extends CanPretty
case class OptionTreeCanPretty(tree: Option[Tree]) extends CanPretty

import scala.language.implicitConversions
implicit def treeCanPretty(tree: Tree): CanPretty = TreeCanPretty(tree)
implicit def listTreeCanPretty(tree: List[Tree]): CanPretty = ListTreeCanPretty(tree)
implicit def optionTreeCanPretty(tree: Option[Tree]): CanPretty = OptionTreeCanPretty(tree)

private def prettyTree(tree: Tree, showFields: Boolean): String = "fun ..."
private def prettyList(tree: List[Tree], showFields: Boolean): String = "fun ..."
private def prettyOption(tree: Option[Tree], showFields: Boolean): String = "fun ..."
Guillaume Massé
quelle
1

Eines der möglichen Szenarien ist


  def foo(a: Int)(b: Int = 10)(c: String = "10") = a + b + c
  def foo(a: Int)(b: String = "10")(c: Int = 10) = a + b + c

Der Compiler wird verwirrt sein, welchen er aufrufen soll. Um andere mögliche Gefahren zu vermeiden, würde der Compiler höchstens eine überladene Methode mit Standardargumenten zulassen.

Nur meine Vermutung :-)

Shiva Wu
quelle
0

Nach meinem Verständnis kann es in den kompilierten Klassen mit Standardargumentwerten zu Namenskollisionen kommen. Ich habe etwas in dieser Richtung gesehen, das in mehreren Threads erwähnt wurde.

Die angegebene Argumentspezifikation finden Sie hier: http://www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017:29/named-args.pdf

Es sagt aus:

 Overloading If there are multiple overloaded alternatives of a method, at most one is
 allowed to specify default arguments.

Auf jeden Fall wird es vorerst nicht funktionieren.

Sie könnten so etwas wie in Java tun, z. B.:

def foo(a: String)(b: Int) =  a + (if (b > 0) b else 42)
Janx
quelle