Überladen Methoden mehr als syntaktischen Zucker? [geschlossen]

19

Überlastet die Methode eine Art Polymorphismus? Für mich scheint es einfach die Unterscheidung von Methoden mit dem gleichen Namen und verschiedenen Parametern zu sein. Also stuff(Thing t)und stuff(Thing t, int n)sind ganz andere Methoden, was den Compiler und die Laufzeit angeht.

Auf der Anruferseite entsteht die Illusion, dass dieselbe Methode auf verschiedene Arten von Objekten unterschiedlich wirkt - Polymorphismus. Das ist aber nur eine Illusion, denn eigentlich stuff(Thing t)und stuff(Thing t, int n)auch ganz andere Methoden.

Überladen Methoden mehr als syntaktischen Zucker? Vermisse ich etwas?


Eine gebräuchliche Definition für syntaktischen Zucker ist, dass er rein lokal ist . Das heißt, das Ändern eines Codeteils in das gesüßte Äquivalent oder umgekehrt beinhaltet lokale Änderungen, die sich nicht auf die Gesamtstruktur des Programms auswirken. Und ich denke, dass das Überladen von Methoden genau diesem Kriterium entspricht. Schauen wir uns ein Beispiel an, um Folgendes zu demonstrieren:

Betrachten Sie eine Klasse:

class Reader {
    public String read(Book b){
        // .. translate the book to text
    }
    public String read(File b){
        // .. translate the file to text
    }
}

Betrachten Sie nun eine andere Klasse, die diese Klasse verwendet:

/* might not be the best example */
class FileProcessor {
    Reader reader = new Reader();
    public void process(File file){
        String text = reader.read(file);
        // .. do stuff with the text
    }
}

Okay. Nun wollen wir sehen, was sich ändern muss, wenn wir die Methodenüberladung durch reguläre Methoden ersetzen:

Die readMethoden im ReaderWechsel zu readBook(Book)und readFile(file). Nur eine Frage der Namensänderung.

Der aufrufende Code in FileProcessorändert sich geringfügig: reader.read(file)ändert sich zu reader.readFile(file).

Und das ist es.

Wie Sie sehen, ist der Unterschied zwischen der Verwendung von Methodenüberladung und deren Nichtverwendung rein lokal . Und deshalb glaube ich, dass es sich um reinen syntaktischen Zucker handelt.

Ich würde gerne Ihre Einwände hören, wenn Sie welche haben, vielleicht fehlt mir etwas.

Aviv Cohn
quelle
48
Letztendlich ist jede Programmiersprache nur syntaktischer Zucker für Raw Assembler.
Philipp
31
@Philipp: Sorry, aber das ist eine wirklich blöde Aussage. Programmiersprachen beziehen ihren Nutzen aus der Semantik, nicht aus der Syntax. Features wie ein Typensystem geben Ihnen tatsächliche Garantien, obwohl Sie möglicherweise tatsächlich mehr schreiben müssen .
back2dos
3
Fragen Sie sich Folgendes: Überlastet der Operator nur syntaktischen Zucker? Was auch immer die Antwort auf diese Frage ist, die Sie für wahr halten, ist auch die Antwort auf die Frage, die Sie gestellt haben;)
back2dos
5
@ back2dos: Stimme dir voll und ganz zu. Ich habe den Satz "Alles ist nur syntaktischer Zucker für Assembler" viel zu oft gelesen und es ist eindeutig falsch. Syntaktischer Zucker ist eine alternative (möglicherweise schönere) Syntax für eine vorhandene Syntax, die keine neue Semantik hinzufügt.
Giorgio
6
@ Giorgio: richtig! Eine genaue Definition findet sich in Matthias Felleisens Leitartikel zur Expressivität. Grundsätzlich gilt: Syntaktischer Zucker ist rein lokal. Wenn Sie die globale Struktur des Programms ändern müssen, um die Verwendung der Sprachfunktion zu entfernen, ist dies kein syntaktischer Zucker. Das heißt, das Umschreiben von polymorphem OO-Code in Assembler umfasst typischerweise das Hinzufügen einer globalen Dispatch-Logik, die nicht rein lokal ist, weshalb OO nicht "nur syntaktischer Zucker für Assembler" ist.
Jörg W Mittag

Antworten:

29

Um dies zu beantworten, benötigen Sie zunächst eine Definition für "syntaktischen Zucker". Ich werde mit Wikipedia gehen :

In der Informatik ist syntaktischer Zucker eine Syntax in einer Programmiersprache, die die Lesbarkeit und Ausdrucksfähigkeit der Dinge verbessern soll. Dadurch wird die Sprache für den menschlichen Gebrauch "süßer": Dinge können klarer, prägnanter oder in einem alternativen Stil ausgedrückt werden, den manche bevorzugen.

[...]

Insbesondere wird ein Konstrukt in einer Sprache syntaktischer Zucker genannt, wenn es aus der Sprache entfernt werden kann, ohne dass dies Auswirkungen darauf hat, was die Sprache kann

Nach dieser Definition sind Features wie Javas Varargs oder Scalas For-Comprehension syntaktischer Zucker: Sie werden in zugrunde liegende Sprachfeatures übersetzt (ein Array im ersten Fall, Aufrufe von map / flatmap / filter im zweiten Fall) und würden diese entfernen Ändern Sie nicht die Dinge, die Sie mit der Sprache tun können.

Das Überladen von Methoden ist unter dieser Definition jedoch kein syntaktischer Zucker, da das Entfernen die Sprache grundlegend ändern würde (Sie könnten nicht mehr auf ein bestimmtes, auf Argumenten basierendes Verhalten zurückgreifen).

Richtig, Sie können das Überladen von Methoden simulieren, solange Sie eine Möglichkeit haben, auf die Argumente einer Methode zuzugreifen, und Sie können ein "if" -Konstrukt verwenden, das auf den angegebenen Argumenten basiert. Aber wenn Sie diesen syntaktischen Zucker betrachten, müssen Sie alles über einer Turing-Maschine betrachten, um ebenfalls syntaktischer Zucker zu sein.

kdgregory
quelle
22
Das Entfernen der Überladung würde nichts an der Sprache ändern. Sie könnten immer noch genau die gleichen Dinge tun wie zuvor; Sie müssen nur einige Methoden umbenennen. Das ist eine geringfügigere Änderung als das Desugaring von Schleifen.
Doval
9
Wie gesagt, Sie könnten den Ansatz verfolgen, dass alle Sprachen (einschließlich Maschinensprachen) einfach syntaktischer Zucker auf einer Turing-Maschine sind.
kdgregory
9
So wie ich es sehe, können Sie Methodenüberladung einfach tun sum(numbersArray)und sum(numbersList)anstelle von sumArray(numbersArray)und sumList(numbersList). Ich stimme Doval zu, es scheint nur syntatischer Zucker zu sein.
Aviv Cohn
3
Die meiste Sprache. Versuchen Implementierung instanceof, Klassen, Vererbung, Schnittstellen, Generika, Reflexion oder Zugriffsbezeichner mit if, whileund Booleschen Operatoren, mit der exakt gleichen Semantik . Keine Eckfälle. Beachten Sie, dass ich Sie nicht herausfordere, dieselben Dinge wie die spezifischen Verwendungen dieser Konstrukte zu berechnen. Ich weiß bereits, dass Sie alles mit Boolescher Logik und Verzweigung / Schleife berechnen können. Ich bitte Sie, perfekte Kopien der Semantik dieser
Doval,
6
@Doval, kdgregory: Um syntaktischen Zucker zu definieren, müssen Sie ihn relativ zu einer bestimmten Semantik definieren. Wenn die einzige Semantik, die Sie haben, "Was berechnet dieses Programm?" Ist, dann ist klar, dass alles nur syntaktischer Zucker für eine Turing-Maschine ist. Wenn Sie dagegen eine Semantik haben, in der Sie über Objekte und bestimmte Operationen auf ihnen sprechen können, können Sie diese Operationen durch Entfernen bestimmter Syntax nicht mehr ausdrücken, obwohl die Sprache möglicherweise noch vollständig ist.
Giorgio
13

Der Begriff syntaktischer Zucker bezieht sich typischerweise auf Fälle, in denen das Merkmal durch eine Substitution definiert ist. Die Sprache definiert nicht, was ein Feature tut, sondern, dass es genau mit etwas anderem äquivalent ist. Also zum Beispiel für jede Schleife

for(Object alpha: alphas) {
}

Wird:

for(Iterator<Object> iter = alpha.iterator(); iter.hasNext()) {
   alpha = iter.next();
}

Oder nehmen Sie eine Funktion mit variablen Argumenten:

void foo(int... args);

foo(3, 4, 5);

Welches wird:

void Foo(int[] args);

foo(new int[]{3, 4, 5});

Es gibt also eine triviale Substitution der Syntax, um das Feature in Bezug auf andere Features zu implementieren.

Schauen wir uns die Methodenüberladung an.

void foo(int a);
void foo(double b);

foo(4.5);

Dies kann wie folgt umgeschrieben werden:

void foo_int(int a);
void foo_double(double b);

foo_double(4.5);

Aber das ist nicht gleichbedeutend. Innerhalb des Java-Modells ist dies etwas anderes. foo(int a)implementiert keine foo_intzu erstellende Funktion. Java implementiert keine Methodenüberladung, indem es mehrdeutigen Funktionen lustige Namen gibt. Um als syntaktischer Zucker zu gelten, müsste Java so tun, als ob Sie wirklich geschrieben hätten foo_intund foo_doublefunktionieren, aber das tut es nicht.

Winston Ewert
quelle
2
Ich glaube nicht, dass jemals jemand gesagt hat, dass die Transformation für Syntaxzucker trivial sein muss. Selbst wenn But, the transformation isn't trivial. At the least, you have to determine the types of the parameters.ja , finde ich die Behauptung sehr lückenhaft, da die Typen nicht bestimmt werden müssen ; Sie sind zur Kompilierungszeit bekannt.
Doval
3
"Um als syntaktischer Zucker zu gelten, müsste Java so tun, als hätten Sie wirklich foo_int- und foo_double-Funktionen geschrieben, aber das tut es nicht." - Was wäre ein Unterschied zwischen foo(int)/ foo(double)und foo_int/, solange es sich um Überladungsmethoden und nicht um Polymorphismen handelt foo_double? Ich kenne Java nicht wirklich gut, aber ich würde mir vorstellen, dass eine solche Umbenennung wirklich in JVM stattfindet (naja - wahrscheinlich foo(args)eher dann foo_args- zumindest in C ++ mit Symbol-Mangling (ok - Symbol-Mangling ist technisch ein Implementierungsdetail und kein Teil davon der Sprache)
Maciej Piechotka
2
@Doval: "Ich glaube nicht, dass jemals jemand gesagt hat, dass die Transformation für Syntaxzucker trivial sein muss." - Stimmt, aber es muss lokal sein . Die einzige nützliche Definition von syntaktischem Zucker, die ich kenne, stammt aus Matthias Felleisens berühmtem Aufsatz über Sprachexpressivität, und im Grunde heißt es, wenn Sie ein Programm in der Sprache L + y (dh eine Sprache L mit einem Merkmal y ) in neu schreiben können Sprache L (dh eine Teilmenge dieser Sprache ohne das Merkmal y ), ohne die globale Struktur des Programms zu ändern (dh nur lokale Änderungen vorzunehmen), dann ist y syntaktischer Zucker in L + y und tut
Jörg W Mittag
2
… Die Ausdruckskraft von L nicht erhöhen . Wenn Sie jedoch nicht , kann das tun, das heißt , wenn Sie auf die globale Struktur des Programms , um Änderungen vorzunehmen haben, dann ist es nicht syntaktischer Zucker und macht in der Tat machen L + y ausdrucksvoller als L . Beispielsweise ist Java mit erweiterter forSchleife nicht aussagekräftiger als Java ohne. (Es ist schöner, prägnanter, lesbarer und rundum besser, würde ich argumentieren, aber nicht aussagekräftiger.) Ich bin mir jedoch nicht sicher, was den Überlastungsfall angeht. Ich werde wahrscheinlich die Zeitung noch einmal lesen müssen, um sicherzugehen. Mein Darm sagt, es ist syntaktischer Zucker, aber ich bin nicht sicher.
Jörg W Mittag
2
@MaciejPiechotka, wenn es ein Teil der Sprachdefinition wäre, dass Funktionen so umbenannt wurden und Sie unter diesen Namen auf die Funktion zugreifen könnten, wäre dies meiner Meinung nach syntaktischer Zucker. Aber weil es als Implementierungsdetail verborgen ist, ist es meiner Meinung nach kein syntaktischer Zucker.
Winston Ewert
8

Muss es angesichts der Tatsache, dass die Namensverfälschung funktioniert, nicht mehr als syntaktischer Zucker sein?

Der Anrufer kann sich vorstellen, dass er dieselbe Funktion aufruft, wenn er es nicht ist. Aber er konnte die wahren Namen aller seiner Funktionen kennen. Nur wenn es möglich wäre, durch Übergabe einer untypisierten Variablen an eine typisierte Funktion einen verzögerten Polymorphismus zu erzielen und den Typ so festzulegen, dass der Aufruf entsprechend dem Namen an die richtige Version geleitet werden kann, wäre dies ein echtes Sprachmerkmal.

Leider habe ich noch nie eine Sprache gesehen, die dies tut. Wenn es Unklarheiten gibt, lösen diese Compiler diese nicht auf. Sie bestehen darauf, dass der Schreiber sie für sie auflöst.

Jon Jay Obermark
quelle
Die Funktion, die Sie dort suchen, heißt "Multiple Dispatch". Viele Sprachen unterstützen es, einschließlich Haskell, Scala und (seit 4.0) C #.
Iain Galloway
Ich möchte die Parameter für Klassen von der direkten Methodenüberladung trennen. In dem direkten Fall der Methodenüberladung schreibt der Programmierer alle Versionen, der Compiler weiß nur, wie er eine auswählt. Das ist nur syntaktischer Zucker und wird durch einfache Namensverknüpfung auch für den Mehrfachversand gelöst. --- Bei Vorhandensein von Parametern in Klassen generiert der Compiler den Code nach Bedarf, und dies ändert sich vollständig.
Jon Jay Obermark
2
Ich denke du missverstehst. Zum Beispiel kann in C #, wenn einer der Parameter auf ein Verfahren ist , dynamicdann tritt die Überladungsauflösung zur Laufzeit nicht zur Übersetzungszeit . Das ist Multiple Dispatch, und es kann nicht durch Umbenennen von Funktionen repliziert werden.
Iain Galloway
Ziemlich cool. Ich kann immer noch auf Variablentypen testen, daher ist dies nur eine eingebaute Funktion, die syntaktischem Zucker überlagert ist. Es ist ein Sprachfeature, aber nur knapp.
Jon Jay Obermark
7

Je nach Sprache ist es syntaktischer Zucker oder nicht.

In C ++ können Sie beispielsweise Überladungen und Vorlagen verwenden, die ohne Komplikationen nicht möglich wären (schreiben Sie alle Instanziierungen der Vorlage manuell oder fügen Sie viele Vorlagenparameter hinzu).

Beachten Sie, dass dynamischer Dispatch eine Form der Überlastung ist, dynamisch auf einigen Parametern aufgelöst (für einige Sprachen nur ein besonderer, diese , aber nicht alle Sprachen sind begrenzt , so), und ich würde diese Form der Überlastung syntaktischen Zuckers nicht nennen.

Ein Programmierer
quelle
Ich bin mir nicht sicher, wie es den anderen Antworten besser geht, wenn sie im Wesentlichen falsch sind.
Telastyn
5

Für heutige Sprachen ist es nur syntaktischer Zucker; In einer völlig sprachunabhängigen Art und Weise ist es mehr als das.

Früher hat diese Antwort einfach gesagt, dass es mehr als syntaktischer Zucker ist, aber wenn Sie in den Kommentaren sehen, hat Falco den Punkt angesprochen, dass es ein Puzzlestück gibt, das allen modernen Sprachen fehlt. Sie kombinieren Methodenüberladung nicht mit der dynamischen Bestimmung, welche Funktion im selben Schritt aufgerufen werden soll. Dies wird später noch geklärt.

Hier ist , warum es sollte mehr sein.

Stellen Sie sich eine Sprache vor, die sowohl Methodenüberladung als auch untypisierte Variablen unterstützt. Sie könnten die folgenden Methodenprototypen haben:

bool someFunction(int arg);

bool someFunction(string arg);

In einigen Sprachen sind Sie wahrscheinlich damit abgefunden, beim Kompilieren zu wissen, welche davon von einer bestimmten Codezeile aufgerufen wird. In einigen Sprachen werden jedoch nicht alle Variablen eingegeben (oder sie werden alle implizit als Objectoder wie auch immer eingegeben ). Stellen Sie sich daher vor, Sie erstellen ein Wörterbuch, dessen Schlüssel Werten verschiedener Typen zugeordnet sind:

dict roomNumber; // some hotels use numbers, some use letters, and some use
                 // alphanumerical strings.  In some languages, built-in dictionary
                 // types automatically use untyped values for their keys to map to,
                 // so it makes more sense then to allow for both ints and strings in
                 // your code.

Was wäre, wenn Sie sich für someFunctioneine dieser Zimmernummern bewerben wollten ? Sie nennen das:

someFunction(roomNumber[someSortOfKey]);

Heißt someFunction(int)oder someFunction(string)heißt? Hier sehen Sie ein Beispiel, bei dem es sich nicht um vollständig orthogonale Methoden handelt, insbesondere in höheren Sprachen. Die Sprache muss - während der Laufzeit - herausfinden, welche davon aufgerufen werden sollen, und muss diese daher immer noch als mindestens einigermaßen dieselbe Methode betrachten.

Warum nicht einfach Vorlagen verwenden? Warum nicht einfach ein untypisiertes Argument verwenden?

Flexibilität und feinkörnigere Kontrolle. Manchmal ist die Verwendung von Vorlagen / untypisierten Argumenten besser, manchmal jedoch nicht.

Sie müssen sich Fälle überlegen, in denen Sie beispielsweise zwei Methodensignaturen haben, die jeweils ein intund ein stringals Argument verwenden, bei denen die Reihenfolge in jeder Signatur jedoch unterschiedlich ist. Möglicherweise haben Sie einen guten Grund, dies zu tun, da die Implementierung jeder Signatur im Großen und Ganzen dasselbe tut, jedoch mit einer geringfügig anderen Wendung. Die Protokollierung kann beispielsweise unterschiedlich sein. Selbst wenn sie genau dasselbe tun, können Sie möglicherweise bestimmte Informationen automatisch aus der Reihenfolge abrufen, in der die Argumente angegeben wurden. Technisch könnte man einfach Pseudo-Switch-Anweisungen verwenden, um den Typ jedes der übergebenen Argumente zu bestimmen, aber das wird chaotisch.

Ist das nächste Beispiel also eine schlechte Programmierpraxis?

bool stringIsTrue(int arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(Object arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(string arg)
{
    if (arg == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Ja, im Großen und Ganzen. In diesem speziellen Beispiel könnte es jemanden davon abhalten, dies auf bestimmte primitive Typen anzuwenden und unerwartetes Verhalten zurückzugewinnen (was eine gute Sache sein könnte); aber nehmen wir einfach an, dass ich den obigen Code abgekürzt habe und dass Sie tatsächlich Überladungen für alle primitiven Typen sowie für Objects haben. Dann ist dieses nächste Stück Code wirklich angemessener:

bool stringIsTrue(untyped arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Aber was ist, wenn Sie dies nur für ints und strings benötigen und wenn Sie möchten, dass es unter einfacheren oder komplizierteren Bedingungen als wahr zurückgegeben wird? Dann haben Sie einen guten Grund, Überladung zu verwenden:

bool appearsToBeFirstFloor(int arg)
{
    if (arg.digitAt(0) == 1)
    {
        return true;
    }
    else
    {
        return false;
    }
}

bool appearsToBeFirstFloor(string arg)
{
    string firstCharacter = arg.characterAt(0);
    if (firstCharacter.isDigit())
    {
        return appearsToBeFirstFloor(int(firstCharacter));
    }
    else if (firstCharacter.toUpper() == "A")
    {
        return true;
    }
    else
    {
        return false;
    }
}

Aber warum geben Sie diesen Funktionen nicht einfach zwei unterschiedliche Namen? Sie haben immer noch die gleiche Kontrolle, nicht wahr?

Wie bereits erwähnt, verwenden einige Hotels Zahlen, einige Buchstaben und einige eine Mischung aus Zahlen und Buchstaben:

appearsToBeFirstFloor(roomNumber[someSortOfKey]);

// will treat ints and strings differently, without you having to write extra code
// every single spot where the function is being called

Dies ist immer noch nicht genau derselbe genaue Code, den ich im wirklichen Leben verwenden würde, aber er sollte den Punkt veranschaulichen, den ich gerade gut mache.

Aber ... Deshalb ist es in modernen Sprachen nicht mehr als syntaktischer Zucker.

Falco wies in den Kommentaren darauf hin, dass derzeitige Sprachen Methodenüberladung und dynamische Funktionsauswahl grundsätzlich nicht im selben Schritt mischen. Früher verstand ich bestimmte Sprachen so, dass Sie sie appearsToBeFirstFloorim obigen Beispiel überladen konnten. Die Sprache bestimmte dann zur Laufzeit, welche Version der Funktion aufgerufen werden soll, abhängig vom Laufzeitwert der untypisierten Variablen. Diese Verwirrung ergab sich teilweise aus der Arbeit mit ECMA-Sprachen wie ActionScript 3.0, in denen Sie leicht nach dem Zufallsprinzip bestimmen können, welche Funktion zur Laufzeit in einer bestimmten Codezeile aufgerufen wird.

Wie Sie vielleicht wissen, unterstützt ActionScript 3 das Überladen von Methoden nicht. Wie bei VB.NET können Sie Variablen deklarieren und festlegen, ohne explizit einen Typ zuzuweisen. Wenn Sie jedoch versuchen, diese Variablen als Argumente an überladene Methoden zu übergeben, möchten Sie den Laufzeitwert nicht lesen, um zu bestimmen, welche Methode aufgerufen werden soll. es möchte stattdessen eine Methode mit Argumenten vom Typ Objectoder ohne Typ oder so etwas finden. Das obige intvs. stringBeispiel würde also auch in dieser Sprache nicht funktionieren. C ++ hat ähnliche Probleme, wie wenn Sie so etwas wie einen Void-Zeiger oder einen anderen Mechanismus wie diesen verwenden, müssen Sie den Typ beim Kompilieren immer noch manuell disambiguieren.

Also wie der erste Header sagt ...

Für heutige Sprachen ist es nur syntaktischer Zucker; In einer völlig sprachunabhängigen Art und Weise ist es mehr als das. Das Überladen von Methoden nützlicher und relevanter zu machen, wie im obigen Beispiel, kann tatsächlich eine gute Funktion sein, um eine vorhandene Sprache zu ergänzen (wie dies für AS3 allgemein implizit gefordert wurde), oder es könnte auch als eine von vielen verschiedenen Grundpfeilern für dienen die Schaffung einer neuen prozeduralen / objektorientierten Sprache.

Panzerkrise
quelle
3
Können Sie Sprachen benennen, die zur Laufzeit wirklich mit Function-Dispatch umgehen und nicht zur Kompilierungszeit? ALLE Sprachen, die ich kenne, erfordern während der Kompilierung die Gewissheit, welche Funktion aufgerufen wird ...
Falco
@Falco ActionScript 3.0 verarbeitet es zur Laufzeit. Sie könnten zum Beispiel verwenden , um eine Funktion , dass die Rendite ein von drei Strings zufällig, und dann Wert verwenden , seine Rückkehr eines von drei Funktionen zufällig zu nennen: this[chooseFunctionNameAtRandom](); Wenn chooseFunctionNameAtRandom()Rückkehr entweder "punch", "kick"oder "dodge", dann kann man auf diese Weise einen sehr einfachen Zufall implementieren Ein Element in der KI eines Feindes in einem Flash-Spiel.
Panzercrisis
1
Ja - aber beide sind echte semantische Methoden, um einen dynamischen Funktionsversand zu erreichen. Java hat diese Methoden ebenfalls. Aber sie unterscheiden sich von Überladung, Überladung ist statisch und nur syntaktischer Zucker, während dynamischer Versand und Vererbung echte Sprachfunktionen sind, die neue Funktionen bieten!
Falco
1
... Ich habe in C ++ auch Void-Zeiger und Basisklassen-Zeiger ausprobiert, aber der Compiler wollte, dass ich sie selbst disambiguiere, bevor ich sie an eine Funktion übergebe. Jetzt frage ich mich, ob ich diese Antwort löschen soll. Es fängt an, so auszusehen, als würden Sprachen fast immer dazu übergehen, dynamische Funktionsauswahl mit Funktionsüberladung in derselben Anweisung oder Anweisung zu kombinieren, aber dann in letzter Sekunde zurücktreten. Es wäre allerdings eine nette Sprachfunktion. Vielleicht muss jemand eine Sprache machen, die diese hat.
Panzercrisis
1
Lassen Sie die Antwort unverändert, und überlegen Sie vielleicht, einen Teil Ihrer Recherchen aus den Kommentaren in die Antwort aufzunehmen?
Falco
2

Es hängt wirklich von Ihrer Definition des "syntaktischen Zuckers" ab. Ich werde versuchen, einige der Definitionen anzusprechen, die mir einfallen:

  1. Ein Feature ist syntaktischer Zucker, wenn ein Programm, das es verwendet, immer in ein anderes übersetzt werden kann, das das Feature nicht verwendet.

    Hier nehmen wir an, dass es einen primitiven Satz von Merkmalen gibt, die nicht übersetzt werden können: mit anderen Worten, keine Schleifen der Art "Sie können Merkmal X durch Merkmal Y ersetzen" und "Sie können Merkmal Y durch Merkmal X ersetzen". Wenn eines der beiden Merkmale zutrifft, kann das andere Merkmal in Merkmalen ausgedrückt werden, die nicht das erste sind, oder es ist ein primitives Merkmal.

  2. Wie Definition 1, jedoch mit der zusätzlichen Anforderung, dass das übersetzte Programm genauso typsicher ist wie das erste, dh durch Desugaring gehen keinerlei Informationen verloren.

  3. Die Definition des OP: Ein Feature ist syntaktischer Zucker, wenn seine Übersetzung die Struktur des Programms nicht verändert, sondern nur "lokale Änderungen" erfordert.

Nehmen wir Haskell als Beispiel für Überladung. Haskell bietet eine benutzerdefinierte Überladung über Typklassen. Zum Beispiel sind die +und *-Operationen in der NumTypklasse definiert und jeder Typ, der eine (vollständige) Instanz einer solchen Klasse hat, kann mit verwendet werden +. Beispielsweise:

instance Num a => Num (b, a) where
    (x, y) + (_, y') = (x, y + y')
    -- other definitions

("Hello", 1) + ("World", 3) -- -> ("Hello", 4)

Eine bekannte Sache bei Haskells Typenklassen ist, dass man sie loswerden kann . Dh Sie können jedes Programm, das Typklassen verwendet, in ein gleichwertiges Programm übersetzen, das diese nicht verwendet.

Die Übersetzung ist ganz einfach:

  • Eine Klassendefinition gegeben:

    class (P_1 a, ..., P_n a) => X a where
        op_1 :: t_1   ... op_m :: t_m
    

    Sie können es in einen algebraischen Datentyp übersetzen:

    data X a = X {
        X_P_1 :: P_1 a, ... X_P_n :: P_n a,
        X_op_1 :: t_1, ..., X_op_m :: t_m
    }
    

    Hier X_P_iund X_op_isind Selektoren . Wenn also ein Wert vom Typ auf den Wert X aangewendet wird X_P_1, wird der in diesem Feld gespeicherte Wert zurückgegeben, sodass es sich um Funktionen mit dem Typ X a -> P_i a(oder X a -> t_i) handelt.

    Für eine sehr grobe Anologie könnte man sich die Werte für type X aals structs vorstellen und dann, wenn xes sich um einen Typ handelt, X adie Ausdrücke:

    X_P_1 x
    X_op_1 x
    

    könnte gesehen werden als:

    x.X_P_1
    x.X_op_1
    

    (Es ist einfach, nur Positionsfelder anstelle von benannten Feldern zu verwenden, aber benannte Felder sind in den Beispielen einfacher zu handhaben und vermeiden Kesselplattencode.)

  • Eine Instanzdeklaration gegeben:

    instance (C_1 a_1, ..., C_n a_n) => X (T a_1 ... a_n) where
        op_1 = ...; ...;  op_m = ...
    

    Sie können es in eine Funktion übersetzen, bei der die Wörterbücher für die C_1 a_1, ..., C_n a_nKlassen einen Wörterbuchwert (dh einen Wert vom Typ X a) für den Typ zurückgeben T a_1 ... a_n.

    Mit anderen Worten kann die obige Instanz in eine Funktion übersetzt werden wie:

    f :: C_1 a_1 -> ... -> C_n a_n -> X (T a_1 ... a_n)
    

    (Beachten Sie, dass sein nkann 0).

    Und in der Tat können wir es definieren als:

    f c1 ... cN = X {X_P_1=get_P_1_T, X_P_n=get_P_n_T,
                     X_op_1=op_1, ..., X_op_m=op_m}
        where
            op_1 = ...
            ...
            op_m = ...
    

    wo op_1 = ...auf op_m = ...die in den Definitionen instanceErklärung und das get_P_i_Tsind die durch die definierten Funktionen P_iInstanz des TTyps (Diese müssen vorhanden sein, da P_is sind Superklassen von X).

  • Aufruf einer überladenen Funktion gegeben:

    add :: Num a => a -> a -> a
    add x y = x + y
    

    Wir können die Wörterbücher explizit in Bezug auf die Klasseneinschränkungen übergeben und einen entsprechenden Aufruf erhalten:

    add :: Num a -> a -> a -> a
    add dictNum x y = ((+) dictNum) x y
    

    Beachten Sie, wie die Klasseneinschränkungen einfach zu einem neuen Argument wurden. Das +im übersetzten Programm ist der Selektor wie zuvor erklärt. Mit anderen Worten, die übersetzte addFunktion "entpackt" bei gegebenem Wörterbuch für den Typ ihres Arguments zuerst die eigentliche Funktion, um das Ergebnis mit zu berechnen, (+) dictNumund wendet diese Funktion dann auf die Argumente an.

Dies ist nur eine sehr kurze Skizze über das Ganze. Bei Interesse lesen Sie bitte die Artikel von Simon Peyton Jones et al.

Ich glaube, ein ähnlicher Ansatz könnte auch für Überladungen in anderen Sprachen verwendet werden.

Dies zeigt jedoch, dass, wenn Ihre Definition von syntaktischem Zucker (1) ist, Überladung syntaktischer Zucker ist . Weil du es loswerden kannst.

Das übersetzte Programm verliert jedoch einige Informationen über das ursprüngliche Programm. Beispielsweise wird nicht erzwungen, dass die Instanzen für die übergeordneten Klassen vorhanden sind. (Auch wenn die Operationen zum Extrahieren der Wörterbücher des übergeordneten Elements immer noch von diesem Typ sein müssen, können Sie undefinedWerte übergeben oder andere polymorphe Werte eingeben, damit Sie einen Wert für erstellen können, X yohne die Werte für zu erstellen P_i y, sodass die Übersetzung nicht alle verliert die Typensicherheit). Daher ist es kein syntaktischer Zucker gemäß (2)

Wie für (3). Ich weiß nicht, ob die Antwort ein Ja oder ein Nein sein soll.

Ich würde nein sagen, weil beispielsweise eine Instanzdeklaration zu einer Funktionsdefinition wird. Überladene Funktionen erhalten einen neuen Parameter (dh es werden sowohl die Definition als auch alle Aufrufe geändert).

Ich würde ja sagen, weil die beiden Programme immer noch eins zu eins abbilden, so dass die "Struktur" nicht wirklich so stark verändert wird.


Trotzdem würde ich sagen, dass die pragmatischen Vorteile des Überladens so groß sind, dass die Verwendung eines "abfälligen" Begriffs wie "syntaktischer Zucker" nicht richtig erscheint.

Sie können die gesamte Haskell-Syntax in eine sehr einfache Core-Sprache übersetzen (die beim Kompilieren tatsächlich ausgeführt wird), sodass der größte Teil der Haskell-Syntax als "syntaktischer Zucker" für etwas angesehen werden kann, das nur aus Lambda-Kalkül und ein bisschen neuen Konstrukten besteht. Wir können jedoch zustimmen, dass die Haskell-Programme viel einfacher zu handhaben und sehr präzise sind, wohingegen die übersetzten Programme schwerer zu lesen oder zu überlegen sind.

Bakuriu
quelle
2

Wenn der Versand zur Kompilierungszeit aufgelöst wird, was nur vom statischen Typ des Argumentausdrucks abhängt, können Sie mit Sicherheit argumentieren, dass es sich um "syntaktischen Zucker" handelt, der zwei verschiedene Methoden mit unterschiedlichen Namen ersetzt, vorausgesetzt, der Programmierer "kennt" den statischen Typ und könnte einfach den richtigen Methodennamen anstelle des überladenen Namens verwenden. Es ist auch eine Form des statischen Polymorphismus, aber in dieser begrenzten Form ist es normalerweise nicht sehr mächtig.

Natürlich wäre es lästig, die Namen der von Ihnen aufgerufenen Methoden zu ändern, wenn Sie den Typ einer Variablen ändern. In der Sprache C wird dies jedoch als überschaubar angesehen, sodass C keine Funktionsüberladung aufweist (obwohl es hat jetzt generische Makros).

In C ++ - Vorlagen und in allen Sprachen, in denen nicht-triviale Ableitungen statischer Typen verwendet werden, können Sie nicht wirklich behaupten, dass dies "syntaktischer Zucker" ist, es sei denn, Sie argumentieren auch, dass Ableitungen statischer Typen "syntaktischer Zucker" sind. Es wäre ein Ärgernis, keine Vorlagen zu haben, und im Kontext von C ++ wäre es ein "unüberschaubares Ärgernis", da sie für die Sprache und ihre Standardbibliotheken so idiomatisch sind. In C ++ ist es also eher mehr als ein netter Helfer, es ist wichtig für den Stil der Sprache, und ich denke, man muss es mehr nennen als "syntaktischer Zucker".

In Java ist dies möglicherweise mehr als nur eine Annehmlichkeit, wenn man bedenkt, wie viele Überladungen von PrintStream.printund vorliegen PrintStream.println. Es gibt jedoch ebenso viele DataInputStream.readXMethoden, da Java beim Rückgabetyp nicht überlastet, was in gewisser Weise nur der Einfachheit halber dient. Das sind alles für primitive Typen.

Ich erinnere mich nicht , was in Java passiert , wenn ich Klassen Aund Berstrecken O, ich überlastete Methoden foo(O), foo(A)und foo(B), und dann in einem allgemeinen mit <T extends O>nenne ich foo(t)wo teine Instanz T. In dem Fall , wo Tist Aerhalte ich Versand auf der Grundlage der Überlast oder ist es , als ob ich genannt foo(O)?

In diesem Fall sind Java-Methodenüberladungen genauso besser als Zucker wie C ++ - Überladungen. Unter Verwendung Ihrer Definition könnte ich in Java lokal eine Reihe von Typprüfungen schreiben (was zerbrechlich wäre, da neue Überladungen foozusätzliche Prüfungen erfordern würden). Abgesehen davon, dass ich diese Zerbrechlichkeit akzeptiere, kann ich an der Call-Site keine lokalen Änderungen vornehmen , um sie zu korrigieren. Stattdessen müsste ich auf das Schreiben von generischem Code verzichten. Ich würde argumentieren, dass das Verhindern von aufgeblähtem Code syntaktischer Zucker sein könnte, aber das Verhindern von fragilem Code ist mehr als das. Aus diesem Grund ist statischer Polymorphismus im Allgemeinen mehr als nur syntaktischer Zucker. Die Situation in einer bestimmten Sprache kann unterschiedlich sein, je nachdem, wie weit die Sprache es Ihnen erlaubt, den statischen Typ "nicht zu kennen".

Steve Jessop
quelle
In Java werden Überladungen zur Kompilierungszeit behoben. In Anbetracht der Verwendung der Typ-Löschung wäre es für sie unmöglich, anders zu sein. Wenn weiter, auch ohne Typ Löschung, T:Animalist , Art SiameseCatund bestehende Überlastungen sind Cat Foo(Animal), SiameseCat Foo(Cat)und Animal Foo(SiameseCat), die Überlastung sollte , wenn ausgewählt werden Tist SiameseCat?
Supercat
@supercat: macht Sinn. Also hätte ich die Antwort herausfinden können, ohne mich zu erinnern (oder sie natürlich auszuführen). Daher sind Java-Überladungen nicht besser als Zucker, wie C ++ - Überladungen generischen Code betreffen. Es bleibt möglich, dass sie auf andere Weise besser sind als nur eine lokale Transformation. Ich frage mich, ob ich mein Beispiel in C ++ ändern oder es als etwas eingebildetes Java belassen soll, das kein echtes Java ist.
Steve Jessop
Überladungen können hilfreich sein, wenn Methoden optionale Argumente haben, sie können jedoch auch gefährlich sein. Angenommen, die Zeile long foo=Math.round(bar*1.0001)*5wird in geändert long foo=Math.round(bar)*5. Wie würde sich das auf die Semantik auswirken, wenn barz. B. 123456789L gleich ist?
Supercat
@supercat Ich würde behaupten, die wirkliche Gefahr besteht in der impliziten Konvertierung von longnach double.
Doval
@Doval: An double?
Supercat
1

Es sieht so aus, als ob "syntaktischer Zucker" abwertend klingt, als ob er nutzlos oder leichtfertig ist. Deshalb löst die Frage viele negative Antworten aus.

Aber Sie haben Recht, das Überladen von Methoden fügt der Sprache keine Funktion hinzu, außer dass derselbe Name für verschiedene Methoden verwendet werden kann. Sie können den Parametertyp explizit angeben, das Programm funktioniert trotzdem.

Gleiches gilt für Paketnamen. String ist nur syntaktischer Zucker für java.lang.String.

In der Tat eine Methode wie

void fun(int i, String c);

in der Klasse sollte MyClass so etwas wie "my_package_MyClass_fun_int_java_lang_String" heißen. Dies würde die Methode eindeutig identifizieren. (Die JVM macht so etwas intern). Aber das willst du nicht schreiben. Aus diesem Grund lässt der Compiler Sie Spaß schreiben (1, "eins") und identifizieren, welche Methode es ist.

Mit dem Überladen können Sie jedoch eines tun: Wenn Sie eine Methode mit der gleichen Anzahl von Argumenten überladen, ermittelt der Compiler automatisch, welche Version am besten zu dem Argument passt, das durch übereinstimmende Argumente angegeben wird Das angegebene Argument ist eine Unterklasse des deklarierten Arguments.

Wenn Sie zwei überladene Prozeduren haben

addParameter(String name, Object value);
addParameter(String name, Date value);

Sie müssen nicht wissen, dass es für Dates eine bestimmte Version des Verfahrens gibt. addParameter ("hello", "world") ruft die erste Version auf, addParameter ("now", new Date ()) ruft die zweite auf.

Natürlich sollten Sie vermeiden, eine Methode mit einer anderen Methode zu überladen, die etwas völlig anderes tut.

Florian F
quelle
1

Interessanterweise hängt die Antwort auf diese Frage von der Sprache ab.

Insbesondere besteht eine Wechselwirkung zwischen Überladung und generischer Programmierung (*), und je nachdem, wie generische Programmierung implementiert ist, kann es sich lediglich um syntaktischen Zucker (Rust) oder um absolut notwendige (C ++) handeln.

Das heißt, wenn generische Programmierung mit expliziten Schnittstellen implementiert wird (in Rust oder Haskell wären dies Typklassen), ist Überladung nur syntaktischer Zucker. oder vielleicht sogar nicht Teil der Sprache.

Wenn die generische Programmierung hingegen mit Duck-Typing implementiert wird (sei es dynamisch oder statisch), ist der Name der Methode ein wesentlicher Vertrag, und daher ist eine Überladung für die Funktionsfähigkeit des Systems obligatorisch.

(*) Wird im Sinne eines einmaligen Schreibens einer Methode verwendet, um verschiedene Typen einheitlich zu bearbeiten.

Matthieu M.
quelle
0

In einigen Sprachen handelt es sich zweifellos nur um syntaktischen Zucker. Was ein Zucker ist, hängt jedoch von Ihrer Sichtweise ab. Ich werde diese Diskussion für später in dieser Antwort belassen.

Im Moment möchte ich nur erwähnen, dass es sich in einigen Sprachen sicherlich nicht um syntaktischen Zucker handelt. Zumindest nicht, ohne dass Sie eine völlig andere Logik / einen anderen Algorithmus verwenden müssen, um dasselbe zu implementieren. Es ist, als würde man behaupten, Rekursion sei syntaktischer Zucker (was der Fall ist, da man alle rekursiven Algorithmen mit einer Schleife und einem Stapel schreiben kann).

Ein Beispiel für eine sehr schwer zu ersetzende Verwendung stammt aus einer Sprache, die diese Funktion ironischerweise nicht als "Funktionsüberladung" bezeichnet. Stattdessen wird es "Mustervergleich" genannt (was als Übermenge der Überladung angesehen werden kann, da wir nicht nur Typen, sondern auch Werte überladen können).

Hier ist die klassisch naive Implementierung der Fibonacci-Funktion in Haskell:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

Es ist anzunehmen, dass die drei Funktionen durch ein if / else ersetzt werden können, wie es üblicherweise in jeder anderen Sprache durchgeführt wird. Aber das macht die Definition im Grunde ganz einfach:

fib n = fib (n-1) + fib (n-2)

viel chaotischer und drückt den mathematischen Begriff der Fibonacci-Folge nicht direkt aus.

Manchmal kann es sich also um Syntaxzucker handeln, wenn Sie nur eine Funktion mit unterschiedlichen Argumenten aufrufen können. Aber manchmal ist es viel grundlegender.


Nun zur Diskussion, wofür das Überladen von Operatoren ein Zucker sein kann. Sie haben einen Anwendungsfall identifiziert - er kann verwendet werden, um ähnliche Funktionen mit unterschiedlichen Argumenten zu implementieren. So:

function print (string x) { stdout.print(x) };
function print (number x) { stdout.print(x.toString) };

kann alternativ implementiert werden als:

function printString (string x) {...}
function printNumber (number x) {...}

oder auch:

function print (auto x) {
    if (x instanceof String) {...}
    if (x instanceof Number) {...}
}

Das Überladen von Operatoren kann aber auch ein Zucker für die Implementierung optionaler Argumente sein (einige Sprachen haben das Überladen von Operatoren, aber keine optionalen Argumente):

function print (string x) {...}
function print (string x, stream io) {...}

kann verwendet werden, um Folgendes zu implementieren:

function print (string x, stream io=stdout) {...}

In einer solchen Sprache (google "Ferite language") wird durch das Entfernen der Operatorüberladung eine Funktion drastisch entfernt - optionale Argumente. Gewährt in Sprachen, in denen beide Funktionen (c ++) das eine oder das andere entfernen, hat dies keine Auswirkungen, da beide zur Implementierung optionaler Argumente verwendet werden können.

Slebetman
quelle
Haskell ist ein gutes Beispiel dafür, warum das Überladen von Operatoren kein syntaktischer Zucker ist, aber ich denke, ein besseres Beispiel wäre das Dekonstruieren eines algebraischen Datentyps mit Pattern Matching (was meines Wissens ohne Pattern Matching unmöglich ist).
11684
@ 11684: Können Sie auf ein Beispiel verweisen? Ich kenne Haskell ehrlich gesagt überhaupt nicht, fand aber die Mustererkennung sublim elegant, als ich das Fib-Beispiel sah (auf computerphile auf youtube).
Slebetman
Wenn Sie einen Datentyp wie diesen haben data PaymentInfo = CashOnDelivery | Adress String | UserInvoice CustomerInfo, können Sie die Muster für die Typkonstruktoren abgleichen.
11684
Wie folgt aus : getPayment :: PaymentInfo -> a getPayment CashOnDelivery = error "Should have been paid already" getPayment (Adress addr) = -- code to notify administration to send a bill getPayment (UserInvoice cust) = --whatever. I took the data type from a Haskell tutorial and have no idea what an invoice is. Ich hoffe dieser Kommentar ist etwas verständlich.
11684
0

Ich denke, es ist einfacher syntaktischer Zucker in den meisten Sprachen (zumindest weiß ich ...), da sie alle einen eindeutigen Funktionsaufruf zur Kompilierungszeit erfordern. Und der Compiler ersetzt den Funktionsaufruf einfach durch einen expliziten Zeiger auf die richtige Implementierungssignatur.

Beispiel in Java:

String s; int i;
mangle(s);  // Translates to CALL ('mangle':LString):(s)
mangle(i);  // Translates to CALL ('mangle':Lint):(i)

Am Ende könnte es also komplett durch ein einfaches Compiler-Makro mit Suchen und Ersetzen ersetzt werden, das die überladene Funktion mangle durch mangle_String und mangle_int ersetzt - da die Argumentliste Teil der eventuellen Funktionskennung ist, ist dies praktisch das, was passiert -> und deshalb ist es nur syntaktischer Zucker.

Wenn es nun eine Sprache gibt, in der die Funktion zur Laufzeit wirklich festgelegt ist, wie bei überschriebenen Methoden in Objekten, wäre dies anders. Aber ich glaube nicht, dass es eine solche Sprache gibt, da method.overloading zu Mehrdeutigkeiten neigt, die der Compiler nicht auflösen kann und die vom Programmierer mit einer expliziten Umwandlung behandelt werden müssen. Dies ist zur Laufzeit nicht möglich.

Falco
quelle
0

In Java werden Typinformationen kompiliert und es wird beim Kompilieren entschieden, welche der Überladungen aufgerufen wird.

Das Folgende ist ein Ausschnitt aus sun.misc.Unsafe(dem Dienstprogramm für Atomics), der im Klassendatei-Editor von Eclipse angezeigt wird.

  // Method descriptor #143 (Ljava/lang/Object;I)I (deprecated)
  // Stack: 4, Locals: 3
  @java.lang.Deprecated
  public int getInt(java.lang.Object arg0, int arg1);
    0  aload_0 [this]
    1  aload_1 [arg0]
    2  iload_2 [arg1]
    3  i2l
    4  invokevirtual sun.misc.Unsafe.getInt(java.lang.Object, long) : int [231]
    7  ireturn
      Line numbers:
        [pc: 0, line: 213]

Wie Sie sehen können, sind Typinformationen der aufgerufenen Methode (Zeile 4) im Aufruf enthalten.

Dies bedeutet, dass Sie einen Java-Compiler erstellen können, der Typinformationen enthält. Wenn Sie beispielsweise eine solche Notation verwenden, lautet die Quelle wie folgt:

@Deprecated
public in getInt(Object arg0, int arg1){
     return getInt$(Object,long)(arg0, arg1);
}

und die Besetzung zu lang wäre optional.

In anderen statisch typisierten kompilierten Sprachen sehen Sie ein ähnliches Setup, bei dem der Compiler entscheidet, welche Überladung je nach Typ aufgerufen wird, und sie in die Bindung / den Aufruf einbezieht.

Die Ausnahme bilden dynamische C-Bibliotheken, in denen die Typinformationen nicht enthalten sind, und der Versuch, eine überladene Funktion zu erstellen, führt zu einer Beschwerde des Linkers.

Ratschenfreak
quelle