Warum ist das Überladen mit Rückgabetypen nicht zulässig? (zumindest in normalerweise verwendeten Sprachen)

9

Ich kenne nicht alle Programmiersprachen, aber es ist klar, dass normalerweise die Möglichkeit einer Überladung einer Methode unter Berücksichtigung ihres Rückgabetyps (vorausgesetzt, ihre Argumente haben dieselbe Anzahl und denselben Typ) nicht unterstützt wird.

Ich meine so etwas:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

Es ist nicht so, dass es ein großes Problem für die Programmierung ist, aber manchmal hätte ich es begrüßt.

Natürlich gibt es für diese Sprachen keine Möglichkeit, dies zu unterstützen, ohne zu unterscheiden, welche Methode aufgerufen wird, aber die Syntax dafür kann so einfach sein wie [int] method1 (num) oder [long] method1 (num) Auf diese Weise würde der Compiler wissen, welcher aufgerufen werden würde.

Ich weiß nicht, wie Compiler funktionieren, aber das scheint nicht so schwierig zu sein. Deshalb frage ich mich, warum so etwas normalerweise nicht implementiert wird.

Was sind die Gründe, warum so etwas nicht unterstützt wird?

user2638180
quelle
Vielleicht wäre Ihre Frage besser mit einem Beispiel, in dem implizite Konvertierungen zwischen den beiden Rückgabetypen nicht existieren - z . B. Klassen Foound Bar.
Warum sollte eine solche Funktion nützlich sein?
James Youngman
3
@JamesYoungman: Um beispielsweise Zeichenfolgen in verschiedene Typen zu analysieren, können Sie eine Methode int read (Zeichenfolge s), float read (Zeichenfolge s) usw. verwenden. Jede überladene Variante der Methode führt eine Analyse für den entsprechenden Typ durch.
Giorgio
Dies ist übrigens nur ein Problem mit statisch typisierten Sprachen. In dynamisch typisierten Sprachen wie Javascript oder Python ist es ziemlich routinemäßig, mehrere Rückgabetypen zu haben.
Gort den Roboter
1
@StevenBurnap, ähm, nein. Mit zB JavaScript können Sie überhaupt keine Funktionsüberladung durchführen. Dies ist also eigentlich nur ein Problem mit Sprachen, die das Überladen von Funktionsnamen unterstützen.
David Arno

Antworten:

19

Dies erschwert die Typprüfung.

Wenn Sie nur eine Überladung basierend auf Argumenttypen zulassen und nur die Ableitung von Variablentypen aus ihren Initialisierern zulassen, fließen alle Ihre Typinformationen in eine Richtung: den Syntaxbaum hinauf.

var x = f();

given      f   : () -> int  [upward]
given      ()  : ()         [upward]
therefore  f() : int        [upward]
therefore  x   : int        [upward]

Wenn Sie zulassen, dass Typinformationen in beide Richtungen übertragen werden, z. B. indem Sie den Typ einer Variablen aus ihrer Verwendung ableiten, benötigen Sie einen Einschränkungslöser (z. B. Algorithmus W für Hindley-Milner-Typsysteme), um den Typ zu bestimmen.

var x = parse("123");
print_int(x);

given      parse        : string -> T  [upward]
given      "123"        : string       [upward]
therefore  parse("123") : ∃T           [upward]
therefore  x            : ∃T           [upward]
given      print_int    : int -> ()    [upward]
therefore  print_int(x) : ()           [upward]
therefore  int -> ()    = ∃T -> ()     [downward]
therefore  ∃T           = int          [downward]
therefore  x            : int          [downward]

Hier mussten wir den Typ xals ungelöste Typvariable ∃Tbelassen, wobei wir nur wissen, dass er analysierbar ist. Erst später, wenn xes für einen konkreten Typ verwendet wird, verfügen wir über genügend Informationen, um die Einschränkung zu lösen und die zu bestimmen ∃T = int, die Typinformationen über den Syntaxbaum vom Aufrufausdruck in weitergeben x.

Wenn wir den Typ von nicht bestimmen könnten x, wäre entweder dieser Code ebenfalls überladen (so würde der Aufrufer den Typ bestimmen) oder wir müssten einen Fehler über die Mehrdeutigkeit melden.

Daraus kann ein Sprachdesigner schließen:

  • Dies erhöht die Komplexität der Implementierung.

  • Dadurch wird die Typüberprüfung langsamer - in pathologischen Fällen exponentiell.

  • Es ist schwieriger, gute Fehlermeldungen zu erstellen.

  • Es ist zu verschieden vom Status Quo.

  • Ich habe keine Lust, es umzusetzen.

Jon Purdy
quelle
1
Außerdem: Es ist so schwer zu verstehen, dass Ihr Compiler in einigen Fällen Entscheidungen trifft, die der Programmierer absolut nicht erwartet hat, was zu schwer zu findenden Fehlern führt.
Gnasher729
1
@ gnasher729: Ich bin anderer Meinung. Ich verwende regelmäßig Haskell, das über diese Funktion verfügt, und ich bin noch nie von der Wahl der Überlastung (z. B. Typklasseninstanz) gebissen worden. Wenn etwas nicht eindeutig ist, muss ich nur eine Typanmerkung hinzufügen. Und ich habe mich trotzdem dafür entschieden, eine vollständige Typinferenz in meiner Sprache zu implementieren, weil dies unglaublich nützlich ist. Diese Antwort war, dass ich den Anwalt des Teufels spielte.
Jon Purdy
4

Weil es mehrdeutig ist. Am Beispiel von C #:

var foo = method(42);

Welche Überlastung sollen wir verwenden?

Ok, vielleicht war das etwas unfair. Der Compiler kann nicht herausfinden, welcher Typ verwendet werden soll, wenn wir ihn nicht in Ihrer hypothetischen Sprache angeben. Implizites Tippen ist in Ihrer Sprache also unmöglich und es gibt anonyme Methoden und Linq dazu ...

Wie wäre es mit diesem? (Leicht neu definierte Signaturen, um den Punkt zu veranschaulichen.)

short method(int num) { ... }
int method(int num) { ... }

....

long foo = method(42);

Sollten wir die intÜberlastung oder die shortÜberlastung verwenden? Wir wissen es einfach nicht, wir müssen es mit Ihrer [int] method1(num)Syntax angeben . Um ehrlich zu sein, ist es ein bisschen mühsam, zu analysieren und zu schreiben .

long foo = [int] method(42);

Die Syntax ähnelt überraschend einer generischen Methode in C #.

long foo = method<int>(42);

(C ++ und Java haben ähnliche Funktionen.)

Kurz gesagt, Sprachdesigner haben sich entschieden, das Problem auf eine andere Weise zu lösen, um das Parsen zu vereinfachen und viel leistungsfähigere Sprachfunktionen zu ermöglichen.

Sie sagen, Sie wissen nicht viel über Compiler. Ich würde wärmstens empfehlen, etwas über Grammatiken und Parser zu lernen. Sobald Sie verstanden haben, was eine kontextfreie Grammatik ist, haben Sie eine viel bessere Vorstellung davon, warum Mehrdeutigkeit eine schlechte Sache ist.

Badeente
quelle
Guter Punkt über Generika, obwohl erscheinen Sie werden conflating shortund int.
Robert Harvey
Ja, @RobertHarvey, du hast recht. Ich kämpfte um ein Beispiel, um den Punkt zu veranschaulichen. Es würde besser funktionieren, wenn methodein Short oder Int zurückgegeben würde und der Typ als Long definiert würde.
RubberDuck
Das scheint ein bisschen besser zu sein.
RubberDuck
Ich kaufe Ihre Argumente nicht, dass Sie keine Typinferenz, anonyme Unterprogramme oder Monadenverständnisse in einer Sprache mit Überladung des Rückgabetyps haben können. Haskell macht es und Haskell hat alle drei. Es hat auch parametrischen Polymorphismus. Bei Ihrem Punkt zu long/ int/ shortgeht es mehr um die Komplexität der Untertypisierung und / oder impliziten Konvertierungen als um die Überladung von Rückgabetypen. Schließlich sind Zahlenliterale bei ihrem Rückgabetyp in C ++, Java, C♯ und vielen anderen überladen, und das scheint kein Problem zu sein. Sie können sich einfach eine Regel ausdenken: z. B. den spezifischsten / allgemeinsten Typ auswählen.
Jörg W Mittag
@ JörgWMittag Mein Punkt war nicht, dass es unmöglich machen würde, nur dass es die Dinge unnötig komplex macht.
RubberDuck
0

Alle Sprachfunktionen erhöhen die Komplexität, sodass sie genügend Nutzen bieten müssen, um die unvermeidlichen Fallstricke, Eckfälle und Benutzerverwirrungen zu rechtfertigen, die jede Funktion verursacht. Für die meisten Sprachen bietet diese Sprache einfach nicht genug Nutzen, um dies zu rechtfertigen.

In den meisten Sprachen würde man erwarten, dass der Ausdruck method1(2)einen bestimmten Typ und einen mehr oder weniger vorhersehbaren Rückgabewert hat. Wenn Sie jedoch eine Überladung der Rückgabewerte zulassen, ist es unmöglich zu sagen, was dieser Ausdruck im Allgemeinen bedeutet, ohne den Kontext zu berücksichtigen. Überlegen Sie, was passiert, wenn Sie eine unsigned long long foo()Methode haben, deren Implementierung endet return method1(2)? Sollte das die long-returning overload oder die int-returning overload aufrufen oder einfach einen Compilerfehler geben?

Wenn Sie dem Compiler durch Annotieren des Rückgabetyps helfen müssen, erfinden Sie nicht nur mehr Syntax (was alle oben genannten Kosten für das Vorhandensein der Funktion erhöht), sondern Sie tun auch das Gleiche wie das Erstellen zwei unterschiedlich benannte Methoden in einer "normalen" Sprache. Ist [long] method1(2)intuitiver als long_method1(2)?


Auf der anderen Seite erlauben einige funktionale Sprachen wie Haskell mit sehr starken statischen Typsystemen diese Art von Verhalten, da ihre Typinferenz stark genug ist, dass Sie den Rückgabetyp in diesen Sprachen selten mit Anmerkungen versehen müssen. Dies ist jedoch nur möglich, weil diese Sprachen die Typensicherheit mehr als jede herkömmliche Sprache wirklich erzwingen und alle Funktionen rein und referenziell transparent sein müssen. In den meisten OOP-Sprachen ist dies niemals möglich.

Ixrec
quelle
2
"zusammen mit der Forderung, dass alle Funktionen rein und referenziell transparent sein müssen": Wie erleichtert dies das Überladen von Rückgabetypen?
Giorgio
@Giorgio Es ist nicht so - Rust erzwingt keine Funktionsreinheit und kann dennoch eine Überladung des Rückgabetyps durchführen (obwohl ihre Überladung in Rust sich stark von anderen Sprachen unterscheidet (Sie können nur mithilfe von Vorlagen überladen))
Idan Arye
Der Teil [long] und [int] sollte eine Möglichkeit haben, die Methode explizit aufzurufen. In den meisten Fällen konnte aus dem Typ der Variablen, der die Ausführung der Methode zugewiesen wird, direkt abgeleitet werden, wie sie aufgerufen werden sollte.
user2638180
0

Es ist in Swift verfügbar und funktioniert dort einwandfrei. Natürlich kann man nicht auf beiden Seiten einen mehrdeutigen Typ haben, daher muss er auf der linken Seite bekannt sein.

Ich habe dies in einer einfachen Kodierungs- / Dekodierungs-API verwendet .

public protocol HierDecoder {
  func dech() throws -> String
  func dech() throws -> Int
  func dech() throws -> Bool

Das bedeutet, dass Aufrufe, bei denen Parametertypen bekannt sind, z. B. initeines Objekts, sehr einfach funktionieren:

    private static let typeCode = "ds"
    static func registerFactory() {
        HierCodableFactories.Register(key:typeCode) {
            (from) -> HierCodable in
            return try tgDrawStyle(strokeColor:from.dech(), fillColor:from.dechOpt(), lineWidth:from.dech(), glowWidth: from.dech())
        }
    }
    func typeKey() -> String { return tgDrawStyle.typeCode }
    func encode(to:HierEncoder) {
        to.ench(strokeColor)
        to.enchOpt(fillColor)
        to.ench(lineWidth)
        to.ench(glowWidth)
    }

Wenn Sie genau hinschauen, werden Sie den dechOptobigen Anruf bemerken . Ich fand heraus, dass das Überladen desselben Funktionsnamens, bei dem das Unterscheidungsmerkmal ein optionales Element zurückgab, zu fehleranfällig war, da der aufrufende Kontext die Erwartung hervorrufen konnte, dass es sich um ein optionales handelt.

Andy Dent
quelle
-5
int main() {
    auto var1 = method1(1);
}
DeadMG
quelle
In diesem Fall könnte der Compiler a) den Aufruf ablehnen, weil er nicht eindeutig ist. B) den ersten / letzten auswählen. C) den Typ von verlassen var1und mit der Typinferenz fortfahren. Sobald ein anderer Ausdruck die Art der var1Verwendung für die Auswahl des bestimmt richtige Umsetzung. Unter dem Strich zeigt die Darstellung eines Falls, in dem die Typinferenz nicht trivial ist, selten, dass ein anderer Punkt als diese Typinferenz im Allgemeinen nicht trivial ist.
back2dos
1
Kein starkes Argument. Rust verwendet zum Beispiel die Typinferenz stark und kann in einigen Fällen, insbesondere bei Generika, nicht sagen, welchen Typ Sie benötigen. In solchen Fällen müssen Sie den Typ lediglich explizit angeben, anstatt sich auf die Typinferenz zu verlassen.
8bittree
1
Äh ... das zeigt keinen überladenen Rückgabetyp. method1muss deklariert werden, um einen bestimmten Typ zurückzugeben.
Gort den Roboter