Warum unterstützen die meisten Programmiersprachen nur die Rückgabe eines einzelnen Wertes aus einer Funktion? [geschlossen]

118

Gibt es einen Grund, warum Funktionen in den meisten (?) Programmiersprachen so konzipiert sind, dass sie eine beliebige Anzahl von Eingabeparametern unterstützen, jedoch nur einen Rückgabewert?

In den meisten Sprachen ist es möglich, diese Einschränkung zu umgehen, z. B. durch die Verwendung von out-Parametern, die Rückgabe von Zeigern oder die Definition / Rückgabe von Strukturen / Klassen. Es ist jedoch merkwürdig, dass Programmiersprachen nicht so konzipiert wurden, dass sie mehrere Rückgabewerte auf "natürlichere" Weise unterstützen.

Gibt es eine Erklärung dafür?

M4N
quelle
40
weil Sie ein Array zurückgeben können ...
Nathan Hayfield
6
Warum also nicht nur ein Argument zulassen? Ich vermute, es ist an Sprache gebunden. Nimm ein paar Ideen auf, mach sie zu einer Sache, die du zu dem zurückbringst, der das Glück / Unglück hat, zuzuhören. Die Rückkehr ist fast wie eine Meinung. "das" machst du mit "diesen".
Erik Reppen
30
Ich glaube , Python Ansatz ist ganz einfach und elegant: Wenn Sie mehrere Werte kann einfach ein Tupel zurückgeben müssen: def f(): return (1,2,3)und dann können Sie verwenden Tupel-Auspacken auf „Split“ das Tupel: a,b,c = f() #a=1,b=2,c=3. Keine Notwendigkeit, ein Array zu erstellen und Elemente manuell zu extrahieren, keine Notwendigkeit, eine neue Klasse zu definieren.
Bakuriu
7
Ich kann Ihnen mitteilen, dass Matlab eine variable Anzahl von Rückgabewerten hat. Die Anzahl der Ausgabeargumente wird durch die aufrufende Signatur (z. B. [a, b] = f()vs. [a, b, c] = f()) bestimmt und innerhalb fvon ermittelt nargout. Ich bin kein großer Fan von Matlab, aber das ist manchmal recht praktisch.
Gerrit
5
Ich denke, wenn die meisten Programmiersprachen so gestaltet sind, ist fraglich. In der Geschichte der Programmiersprachen gab es einige sehr beliebte Sprachen, die auf diese Weise erstellt wurden (wie Pascal, C, C ++, Java, klassisches VB), aber heutzutage gibt es auch eine Hölle von anderen Sprachen, die immer mehr Fans haben, die eine mehrfache Rückkehr ermöglichen Werte.
Doc Brown

Antworten:

58

Einige Sprachen wie Python unterstützen mehrere Rückgabewerte nativ, während einige Sprachen wie C # sie über ihre Basisbibliotheken unterstützen.

Aber im Allgemeinen werden selbst in Sprachen, die sie unterstützen, mehrere Rückgabewerte nicht oft verwendet, weil sie schlampig sind:

  • Funktionen, die mehrere Werte zurückgeben, sind schwer eindeutig zu benennen .
  • Es ist leicht, die Reihenfolge der Rückgabewerte zu verwechseln

    (password, username) = GetUsernameAndPassword()  
    

    (Aus dem gleichen Grund vermeiden es viele Leute, zu viele Parameter für eine Funktion zu haben. Einige gehen sogar davon aus, dass eine Funktion niemals zwei Parameter desselben Typs haben sollte!)

  • OOP-Sprachen haben bereits eine bessere Alternative zu mehreren Rückgabewerten: Klassen.
    Sie sind stärker typisiert, sie halten die Rückgabewerte als eine logische Einheit gruppiert und sie halten die Namen (Eigenschaften) der Rückgabewerte über alle Verwendungen hinweg konsistent.

Der eine Ort, an dem sie sehr praktisch sind, sind Sprachen (wie Python), in denen mehrere Rückgabewerte von einer Funktion als mehrere Eingabeparameter für eine andere verwendet werden können. Aber die Anwendungsfälle, in denen dies ein besseres Design als die Verwendung einer Klasse ist, sind ziemlich schlank.

BlueRaja - Danny Pflughoeft
quelle
50
Es ist schwer zu sagen, dass das Zurückgeben eines Tupels mehrere Dinge zurückgibt. Es wird ein Tupel zurückgegeben . Der Code, den Sie geschrieben haben, entpackt ihn einfach sauber mit syntaktischem Zucker.
10
@Lego: Ich sehe keinen Unterschied - ein Tupel besteht definitionsgemäß aus mehreren Werten. Was würden Sie als "mehrere Rückgabewerte" betrachten, wenn dies nicht der Fall ist?
BlueRaja - Danny Pflughoeft
19
Es ist eine sehr verschwommene Unterscheidung, aber betrachten Sie ein leeres Tupel (). Ist das eine Sache oder nichts? Persönlich würde ich sagen, es ist eine Sache. Ich kann x = ()ganz gut zuweisen , genau wie ich zuweisen kann x = randomTuple(). Wenn das zurückgegebene Tupel leer ist oder nicht, kann ich das zurückgegebene Tupel trotzdem zuweisen x.
19
... Ich habe nie behauptet, Tupel könnten nicht für andere Dinge verwendet werden. Die Argumentation, dass "Python nicht mehrere Rückgabewerte unterstützt, sondern Tupel", ist jedoch äußerst sinnlos umständlich. Dies ist immer noch eine richtige Antwort.
BlueRaja - Danny Pflughoeft
14
Weder Tupel noch Klassen sind "Mehrfachwerte".
Andres F.
54

Weil Funktionen mathematische Konstrukte sind, die eine Berechnung durchführen und ein Ergebnis zurückgeben. Tatsächlich konzentriert sich vieles, was "unter der Haube" nicht weniger Programmiersprachen liegt, nur auf eine Eingabe und eine Ausgabe, wobei mehrere Eingaben nur eine dünne Umhüllung der Eingabe darstellen - und wenn eine einzelne Wertausgabe mit einer einzigen nicht funktioniert Kohäsive Struktur (oder Tupel oder Maybe) ist die Ausgabe (obwohl dieser "einzelne" Rückgabewert aus vielen Werten besteht).

Dies hat sich nicht geändert, da Programmierer herausgefunden haben, dass outParameter umständliche Konstrukte sind, die nur in einer begrenzten Anzahl von Szenarien nützlich sind. Wie bei vielen anderen Dingen ist der Support nicht da, weil der Bedarf / die Nachfrage nicht da ist.

Telastyn
quelle
5
@FrustratedWithFormsDesigner - Dies kam ein wenig in einer aktuellen Frage . Ich kann einerseits zählen, wie oft ich in 20 Jahren mehrere Ausgänge haben wollte.
Telastyn
61
Funktionen in der Mathematik und Funktionen in den meisten Programmiersprachen sind zwei sehr unterschiedliche Wesen.
Tdammers
17
@tdammers in den frühen Tagen waren sie in Gedanken sehr ähnlich. Fortran, Pascal und dergleichen wurden mehr von Mathematik als von der Computerarchitektur beeinflusst.
9
@tdammers - wie so? Ich meine, für die meisten Sprachen läuft es am Ende auf Lambda-Kalkül hinaus - eine Eingabe, eine Ausgabe, keine Nebenwirkungen. Alles andere ist eine Simulation / ein Hack darüber hinaus. Programmierfunktionen sind möglicherweise nicht in dem Sinne rein, dass mehrere Eingaben dieselbe Ausgabe ergeben, aber der Geist ist da.
Telastyn
16
@SteveEvers: Es ist bedauerlich, dass der Name "function" in der Imperativprogrammierung anstelle der angemesseneren "procedure" oder "routine" verwendet wurde. In der funktionalen Programmierung ähnelt eine Funktion mathematischen Funktionen viel mehr.
Tdammers
35

In der Mathematik ist eine "wohldefinierte" Funktion eine Funktion, bei der es für eine bestimmte Eingabe nur 1 Ausgabe gibt (als Randnotiz können Sie nur einzelne Eingabefunktionen haben und semantisch immer noch mehrere Eingaben mithilfe von currying erhalten ).

Für mehrwertige Funktionen (z. B. Quadratwurzel einer positiven Ganzzahl) ist es ausreichend, eine Sammlung oder eine Folge von Werten zurückzugeben.

Für die Arten von Funktionen, über die Sie sprechen (dh Funktionen, die mehrere Werte verschiedener Typen zurückgeben ), sehe ich das etwas anders als Sie scheinen: Ich sehe die Notwendigkeit / Verwendung von out-Parametern als eine Problemumgehung für ein besseres Design oder eine nützlichere Datenstruktur. Ich würde es zum Beispiel vorziehen, wenn *.TryParse(...)Methoden eine Maybe<T>Monade zurückgeben würden, anstatt einen out-Parameter zu verwenden. Denken Sie an diesen Code in F #:

let s = "1"
match tryParse s with
| Some(i) -> // do whatever with i
| None -> // failed to parse

Die Unterstützung für Compiler / IDE / Analyse ist für diese Konstrukte sehr gut. Dies würde einen Großteil des "Bedarfs" für unsere Parameter lösen. Um ganz ehrlich zu sein, ich kann mir keine andere Methode vorstellen, bei der dies nicht die Lösung wäre.

Für andere Szenarien, an die ich mich nicht erinnern kann, reicht ein einfaches Tupel aus.

Steven Evers
quelle
1
Außerdem möchte ich wirklich in der Lage sein, in C # zu schreiben: Das var (value, success) = ParseInt("foo");würde zur Kompilierungszeit überprüft, da (int, bool) ParseInt(string s) { }deklariert wurde. Ich weiß, dass dies mit Generika gemacht werden kann, aber es wäre trotzdem eine schöne Sprachergänzung.
Grimasse der Verzweiflung
10
@GrimaceofDespair Was Sie wirklich wollen, ist die Syntax zu destrukturieren, nicht mehrere Rückgabewerte.
Domenic
2
@ Warren: Ja. Sehen Sie hier und beachten Sie, dass eine solche Lösung nicht kontinuierlich wäre: en.wikipedia.org/wiki/Well-definition
Steven Evers
4
Der mathematische Begriff der Eindeutigkeit hat nichts mit der Anzahl der Ausgänge zu tun, die eine Funktion zurückgibt. Dies bedeutet, dass die Ausgänge immer gleich sind, wenn die Eingänge gleich sind. Genau genommen geben mathematische Funktionen einen Wert zurück, aber dieser Wert ist oft ein Tupel. Für einen Mathematiker gibt es im Wesentlichen keinen Unterschied zwischen diesem und dem Zurückgeben mehrerer Werte. Argumente, dass Programmierfunktionen nur einen Wert zurückgeben sollten, da mathematische Funktionen nicht sehr überzeugend sind.
Michael Siler
1
@MichaelSiler (Ich stimme Ihrem Kommentar zu) Aber bitte beachten Sie, dass das Argument umkehrbar ist: "Argumente, dass Programmfunktionen mehrere Werte zurückgeben können, weil sie einen einzelnen Tupelwert zurückgeben können, sind auch nicht sehr überzeugend" :)
Andres F.
23

Zusätzlich zu dem, was bereits gesagt wurde, wenn Sie sich die in der Assembly verwendeten Paradigmen ansehen, wenn eine Funktion zurückgibt, hinterlässt sie einen Zeiger auf das zurückgegebene Objekt in einem bestimmten Register. Wenn sie Variablen- / Mehrfachregister verwenden, würde die aufrufende Funktion nicht wissen, wo sie die zurückgegebenen Werte erhält, wenn sich diese Funktion in einer Bibliothek befindet. Dies würde das Verknüpfen mit Bibliotheken erschweren und anstatt eine willkürliche Anzahl von wiederverwendbaren Zeigern festzulegen, würden sie mit einem Zeiger verknüpft. Höhere Sprachen haben nicht die gleiche Entschuldigung.

David
quelle
Ah! Sehr interessanter Detailpunkt. +1!
Steven Evers
Dies sollte die akzeptierte Antwort sein. Normalerweise denken die Leute beim Erstellen von Compilern an Zielmaschinen. Eine andere Analogie ist, warum wir int, float, char / string usw. haben, weil dies von der Zielmaschine unterstützt wird. Auch wenn das Ziel nicht Bare Metal ist (z. B. JVM), möchten Sie dennoch eine anständige Leistung erzielen, indem Sie nicht zu viel emulieren.
Imel96
26
... Sie können auf einfache Weise eine Aufrufkonvention für die Rückgabe mehrerer Werte aus einer Funktion definieren, genauso wie Sie eine Aufrufkonvention für die Übergabe mehrerer Werte an eine Funktion definieren können. Dies ist keine Antwort. -1
BlueRaja - Danny Pflughoeft
2
Es wäre interessant zu wissen, ob Stack-basierte Ausführungs-Engines (JVM, CLR) jemals mehrere Rückgabewerte berücksichtigt / zugelassen haben. Dies sollte recht einfach sein. Der Aufrufer muss nur die richtige Anzahl von Werten eingeben, genauso wie er die richtige Anzahl von Argumenten eingibt!
Lorenzo Dematté
1
@David nein, cdecl erlaubt (theoretisch) eine unbegrenzte Anzahl von Parametern (deshalb sind varargs-Funktionen möglich) . Obwohl einige C-Compiler Sie möglicherweise auf mehrere Dutzend oder hundert Argumente pro Funktion beschränken, halte ich dies immer noch für mehr als angemessen -_-
BlueRaja - Danny Pflughoeft
18

Viele Anwendungsfälle, in denen Sie in der Vergangenheit mehrere Rückgabewerte verwendet hätten, sind mit modernen Sprachfunktionen nicht mehr erforderlich. Möchten Sie einen Fehlercode zurückgeben? Wirf eine Ausnahme oder gib eine zurück Either<T, Throwable>. Möchten Sie ein optionales Ergebnis zurückgeben? Rückgabe an Option<T>. Möchten Sie einen von mehreren Typen zurückgeben? Geben Sie eine Either<T1, T2>oder eine markierte Union zurück.

Und selbst in den Fällen , in denen Sie wirklich brauchen mehrere Werte, moderne Sprachen in der Regel zurück Tupeln oder irgendeine Art von Datenstruktur (Liste, ein Array - Wörterbuch) oder Objekte sowie irgendeine Form von Destrukturierung binden oder Musteranpassung unterstützen, die Verpackung macht Ihre mehreren Werte in einen einzigen Wert und dann wieder in mehrere Werte trivial destrukturieren.

Hier einige Beispiele für Sprachen, die die Rückgabe mehrerer Werte nicht unterstützen. Ich verstehe nicht wirklich, wie durch das Hinzufügen von Unterstützung für mehrere Rückgabewerte diese deutlich aussagekräftiger werden, um die Kosten einer neuen Sprachfunktion auszugleichen.

Rubin

def foo; return 1, 2, 3 end

one, two, three = foo

one
# => 1

three
# => 3

Python

def foo(): return 1, 2, 3

one, two, three = foo()

one
# >>> 1

three
# >>> 3

Scala

def foo = (1, 2, 3)

val (one, two, three) = foo
// => one:   Int = 1
// => two:   Int = 2
// => three: Int = 3

Haskell

let foo = (1, 2, 3)

let (one, two, three) = foo

one
-- > 1

three
-- > 3

Perl6

sub foo { 1, 2, 3 }

my ($one, $two, $three) = foo

$one
# > 1

$three
# > 3
Jörg W. Mittag
quelle
1
Ich denke, ein Aspekt ist, dass eine Funktion in einigen Sprachen (wie z. B. Matlab) flexibel sein kann, wie viele Werte sie zurückgibt. siehe mein Kommentar oben . Es gibt viele Aspekte in Matlab, die mir nicht gefallen, aber dies ist eine der wenigen (vielleicht einzigen) Funktionen, die ich beim Portieren von Matlab nach z. B. Python vermisse.
Gerrit
1
Aber was ist mit dynamischen Sprachen wie Python oder Ruby? Angenommen, ich schreibe so etwas wie die Matlab- sortFunktion: sorted = sort(array)Gibt nur das sortierte Array zurück, wohingegen [sorted, indices] = sort(array)beide zurückgegeben werden. Die einzige Möglichkeit, die ich mir in Python vorstellen kann, wäre, eine Flagge nach sortdem Vorbild von sort(array, nout=2)oder zu übergeben sort(array, indices=True).
Gerrit
2
@ MikeCellini Ich glaube nicht. Eine Funktion kann feststellen, mit wie vielen Ausgabeargumenten die Funktion aufgerufen wird ( [a, b, c] = func(some, thing)) und entsprechend handeln. Dies ist beispielsweise nützlich, wenn die Berechnung des ersten Ausgabearguments billig ist, die Berechnung des zweiten jedoch teuer ist. Ich kenne keine andere Sprache, in der das Äquivalent von Matlabs nargoutzur Laufzeit verfügbar ist.
Gerrit
1
@gerrit die richtige Lösung in Python ist , dies zu schreiben: sorted, _ = sort(array).
Miles Rout
1
@MilesRout: Und die sortFunktion kann erkennen, dass sie die Indizes nicht berechnen muss? Das ist cool, das wusste ich nicht.
Jörg W Mittag
12

Der wahre Grund, warum ein einzelner Rückgabewert so beliebt ist, sind die Ausdrücke, die in so vielen Sprachen verwendet werden. In jeder Sprache, in der Sie einen Ausdruck haben können, den x + 1Sie bereits als einzelne Rückgabewerte betrachten, weil Sie einen Ausdruck in Ihrem Kopf auswerten, indem Sie ihn in Teile aufteilen und den Wert jedes Teils bestimmen. Sie betrachten xund entscheiden, dass der Wert 3 ist (zum Beispiel), und Sie betrachten 1 und dann betrachten Siex + 1und füge alles zusammen, um zu entscheiden, dass der Wert des Ganzen 4 ist. Jeder syntaktische Teil des Ausdrucks hat einen Wert, keine andere Anzahl von Werten; Das ist die natürliche Semantik von Ausdrücken, die jeder erwartet. Selbst wenn eine Funktion ein Paar von Werten zurückgibt, gibt sie tatsächlich einen Wert zurück, der die Aufgabe von zwei Werten erfüllt, da die Idee einer Funktion, die zwei Werte zurückgibt, die nicht irgendwie in einer einzelnen Sammlung zusammengefasst sind, zu seltsam ist.

Die Leute wollen sich nicht mit der alternativen Semantik befassen, die erforderlich wäre, damit Funktionen mehr als einen Wert zurückgeben. In einer stapelbasierten Sprache wie Forth können Sie beispielsweise eine beliebige Anzahl von Rückgabewerten haben, da jede Funktion einfach den oberen Bereich des Stapels ändert und die Eingaben und Ausgaben nach Belieben verschiebt. Deshalb hat Forth nicht die Art von Ausdrücken, die normale Sprachen haben.

Perl ist eine andere Sprache, die sich manchmal so verhält, als würden Funktionen mehrere Werte zurückgeben, obwohl dies normalerweise nur als Rückgabe einer Liste betrachtet wird. Die Art und Weise, wie Listen in Perl "interpoliert" werden, gibt uns Listen, (1, foo(), 3)die möglicherweise 3 Elemente enthalten, wie die meisten Leute, die Perl nicht kennen, aber genauso gut nur 2 Elemente, 4 Elemente oder eine größere Anzahl von Elementen haben könnten, je nachdem foo(). Listen in Perl sind reduziert, sodass eine syntaktische Liste nicht immer die Semantik einer Liste hat. es kann nur ein Teil einer größeren Liste sein.

Eine andere Möglichkeit, Funktionen mehrere Werte zurückgeben zu lassen, wäre eine alternative Ausdruckssemantik, bei der jeder Ausdruck mehrere Werte haben kann und jeder Wert eine Möglichkeit darstellt. Nehmen Sie noch x + 1einmal, aber stellen Sie sich diesmal vor, dass xzwei Werte {3, 4} vorliegen, dann x + 1wären die Werte von {4, 5} und die Werte von x + x{6, 8} oder vielleicht {6, 7, 8}. , abhängig davon, ob für eine Auswertung mehrere Werte verwendet werden dürfen x. Eine solche Sprache kann mithilfe von Backtracking implementiert werden, ähnlich wie es Prolog verwendet, um eine Abfrage mehrfach zu beantworten.

Kurz gesagt, ein Funktionsaufruf ist eine einzelne syntaktische Einheit und eine einzelne syntaktische Einheit hat einen einzelnen Wert in der Ausdruckssemantik, die wir alle kennen und lieben. Jede andere Semantik würde Sie dazu zwingen, seltsame Dinge zu tun, wie Perl, Prolog oder Forth.

Geo
quelle
9

Wie in dieser Antwort vorgeschlagen , handelt es sich um Hardwareunterstützung, wobei auch die Tradition im Sprachdesign eine Rolle spielt.

Wenn eine Funktion zurückgibt, hinterlässt sie einen Zeiger auf das zurückgegebene Objekt in einem bestimmten Register

Von den drei ersten Sprachen, Fortran, Lisp und COBOL, verwendete die erste einen einzelnen Rückgabewert, da dieser nach dem Vorbild der Mathematik erstellt wurde. Der zweite gab eine beliebige Anzahl von Parametern zurück, so wie er sie erhalten hat: als Liste (man könnte auch argumentieren, dass er nur einen einzigen Parameter übergeben und zurückgegeben hat: die Adresse der Liste). Der dritte Wert gibt null oder eins zurück.

Diese ersten Sprachen hatten großen Einfluss auf die Gestaltung der folgenden Sprachen, obwohl Lisp, die einzige, die mehrere Werte zurückgab, nie große Popularität erlangte.

Als C auf den Markt kam, konzentrierte es sich stark auf die effiziente Nutzung von Hardwareressourcen, wobei ein enger Zusammenhang zwischen der Funktionsweise der C-Sprache und dem Maschinencode, der sie implementiert hat, bestand. Einige der ältesten Funktionen, wie "auto" vs "register" -Variablen, sind das Ergebnis dieser Konstruktionsphilosophie.

Es muss auch darauf hingewiesen werden, dass die Assemblersprache bis in die 80er Jahre weit verbreitet war, als sie schließlich aus der allgemeinen Entwicklung ausstieg. Leute, die Compiler schrieben und Sprachen schufen, kannten sich mit Assembler aus und hielten sich größtenteils an das, was dort am besten funktionierte.

Die meisten von dieser Norm abweichenden Sprachen fanden nie große Beliebtheit und spielten daher keine große Rolle bei den Entscheidungen der Sprachgestalter (die sich natürlich von dem inspirieren ließen, was sie wussten).

Sehen wir uns also die Assemblersprache an. Schauen wir uns zunächst den 6502 an , einen 1975er Mikroprozessor, der berühmt für seine Apple II- und VIC-20-Mikrocomputer war. Es war sehr schwach im Vergleich zu dem, was damals in Großrechnern und Minicomputern verwendet wurde, obwohl es im Vergleich zu den ersten Computern vor 20, 30 Jahren zu Beginn der Programmiersprachen leistungsfähig war.

Wenn Sie sich die technische Beschreibung ansehen, hat sie 5 Register und einige Ein-Bit-Flags. Das einzige "volle" Register war der Programmzähler (PC) - dieses Register zeigt auf den nächsten auszuführenden Befehl. Die anderen Register enthalten den Akkumulator (A), zwei "Index" -Register (X und Y) und einen Stapelzeiger (SP).

Durch Aufrufen eines Unterprogramms wird der PC in den Speicher gestellt, auf den der SP zeigt, und der SP wird dann dekrementiert. Die Rückkehr von einem Unterprogramm erfolgt in umgekehrter Reihenfolge. Man kann andere Werte auf dem Stapel verschieben und abrufen, aber es ist schwierig, sich auf den Speicher relativ zum SP zu beziehen, so dass das Schreiben von wiedereintretenden Unterroutinen schwierig war. Diese Sache, die wir für selbstverständlich halten und die wir jederzeit als Unterprogramm aufrufen, war in dieser Architektur nicht so verbreitet. Oft wurde ein separater "Stapel" erstellt, so dass die Parameter und die Subroutinen-Rücksprungadresse getrennt gehalten wurden.

Wenn Sie sich den Prozessor ansehen, der den 6502 und den 6800 inspiriert hat , hat er ein zusätzliches Register, das Indexregister (IX), und das SP, das den Wert vom SP empfangen kann.

Beim Aufrufen einer wiedereintretenden Unterroutine auf dem Computer wurden die Parameter auf dem Stapel abgelegt, der PC abgelegt, der PC auf die neue Adresse geändert, und dann wurden die lokalen Variablen der Unterroutine auf dem Stapel abgelegt . Da die Anzahl der lokalen Variablen und Parameter bekannt ist, kann die Adressierung relativ zum Stapel erfolgen. Eine Funktion, die zwei Parameter empfängt und zwei lokale Variablen hat, würde beispielsweise so aussehen:

SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1

Es kann beliebig oft aufgerufen werden, da sich der gesamte temporäre Speicherplatz auf dem Stapel befindet.

Der 8080 , der auf TRS-80 und einer Vielzahl von CP / M-basierten Mikrocomputern verwendet wird, könnte etwas Ähnliches wie der 6800 tun, indem er SP auf den Stapel drückt und ihn dann in sein indirektes Register HL kopiert.

Dies ist eine sehr gebräuchliche Methode, um Dinge zu implementieren, und sie wurde auf moderneren Prozessoren noch mehr unterstützt, und zwar mit dem Basiszeiger, mit dem alle lokalen Variablen vor der Rückkehr einfach gesichert werden können.

Das Problem ist, wie Sie etwas zurückgeben ? Prozessorregister waren zu Beginn nicht sehr zahlreich, und man musste oft einige von ihnen verwenden, um herauszufinden, welcher Teil des Speichers adressiert werden sollte. Das Zurückgeben von Dingen auf den Stack wäre kompliziert: Sie müssten alles platzieren, den PC speichern, die Rückgabeparameter (die in der Zwischenzeit wo gespeichert würden?) Drücken, dann den PC erneut drücken und zurückkehren.

Normalerweise wurde also ein Register für den Rückgabewert reserviert . Der aufrufende Code wusste, dass sich der Rückgabewert in einem bestimmten Register befinden würde, das beibehalten werden musste, bis es gespeichert oder verwendet werden konnte.

Schauen wir uns eine Sprache an, die mehrere Rückgabewerte zulässt: Forth. Forth behält einen separaten Rückgabestapel (RP) und einen separaten Datenstapel (SP) bei, sodass eine Funktion nur alle ihre Parameter abrufen und die Rückgabewerte im Stapel belassen musste. Da der Rückgabestapel getrennt war, störte er nicht.

Als jemand, der Assembler und Forth in den ersten sechs Monaten Erfahrung mit Computern gelernt hat, sind mehrere Rückgabewerte für mich völlig normal. Operatoren wie Forths /mod, die die Ganzzahldivision und den Rest zurückgeben, scheinen offensichtlich zu sein. Andererseits kann ich leicht erkennen, wie jemand, dessen erste Erfahrung C mind war, dieses Konzept seltsam findet: Es widerspricht seinen tief verwurzelten Erwartungen, was eine "Funktion" ist.

Was Mathematik angeht ... nun, ich habe Computer schon lange programmiert, bevor ich überhaupt in Mathematikunterricht kam. Es gibt eine ganze Sektion von CS und Programmiersprachen, die von Mathematik beeinflusst wird, aber es gibt auch eine ganze Sektion, die dies nicht ist.

Wir haben also einen Zusammenfluss von Faktoren, bei denen die Mathematik das frühe Sprachdesign beeinflusste, bei denen Hardwareeinschränkungen vorschrieben, was leicht zu implementieren war, und bei denen die populären Sprachen die Entwicklung der Hardware beeinflussten (die Maschinenprozessoren Lisp und Forth waren in diesem Prozess Roadkills).

Daniel C. Sobral
quelle
@gnat Die Verwendung von "um zu informieren" wie in "um die wesentliche Qualität bereitzustellen" war beabsichtigt.
Daniel C. Sobral
Fühlen Sie sich frei, ein Rollback durchzuführen, wenn Sie diesbezüglich ein starkes Gefühl haben. Mein Leseeinfluss passt hier etwas besser: " Beeinflusst ... in einer wichtigen Weise"
Mücke
1
+1 Wie bereits erwähnt, kann die geringe Registerzahl früherer CPUs im Vergleich zu einer großen Registerzahl aktueller CPUs (die auch in vielen ABIs verwendet wird, z. B. x64 abi) ein Spielewechsel sein und die ehemals zwingenden Gründe, nur 1 Wert zurückzugeben könnte heutzutage nur ein historischer Grund sein.
BitTickler
Ich bin nicht davon überzeugt, dass frühe 8-Bit-Mikroprozessoren einen großen Einfluss auf das Sprachdesign haben und davon ausgehen, welche Dinge in C- oder Fortran-Aufrufkonventionen für jede Architektur erforderlich sind. Fortran geht davon aus, dass Sie Array-Argumente (im Wesentlichen Zeiger) übergeben können. Sie haben bereits große Implementierungsprobleme für normalen Fortran auf Computern wie 6502, da es keine Adressierungsmodi für Zeiger + Index gibt, wie in Ihrer Antwort und in Warum erzeugen C-zu-Z80-Compiler schlechten Code? auf retrocomputing.SE.
Peter Cordes
Fortran geht wie C auch davon aus, dass Sie eine beliebige Anzahl von Argumenten übergeben können und zufälligen Zugriff auf diese und eine beliebige Anzahl von Einheimischen haben, oder? Wie Sie gerade erklärt haben, können Sie dies unter 6502 nicht einfach tun, da die stapelbezogene Adressierung keine Rolle spielt, es sei denn, Sie geben die Wiedereintrittsberechtigung auf. Sie können sie in den statischen Speicher kopieren. Wenn Sie eine beliebige Argumentliste übergeben können, können Sie zusätzliche ausgeblendete Parameter für Rückgabewerte (z. B. jenseits der ersten) hinzufügen, die nicht in Register passen.
Peter Cordes
7

Die mir bekannten funktionalen Sprachen können durch die Verwendung von Tupeln problemlos mehrere Werte zurückgeben (in dynamisch typisierten Sprachen können Sie sogar Listen verwenden). Tuples werden auch in anderen Sprachen unterstützt:

f :: Int -> (Int, Int)
f x = (x - 1, x + 1)

// Even C++ have tuples - see Boost.Graph for use
std::pair<int, int> f(int x) {
  return std::make_pair(x - 1, x + 1);
}

Im obigen Beispiel fgibt eine Funktion 2 Zoll zurück.

In ähnlicher Weise können ML, Haskell, F # usw. auch Datenstrukturen zurückgeben (Zeiger sind für die meisten Sprachen zu niedrig). Ich habe noch nie von einer modernen GP-Sprache mit einer solchen Einschränkung gehört:

data MyValue = MyValue Int Int

g :: Int -> MyValue
g x = MyValue (x - 1, x + 1)

Schließlich können outParameter auch in funktionalen Sprachen von emuliert werden IORef. Es gibt mehrere Gründe, warum es in den meisten Sprachen keine native Unterstützung für Out-Variablen gibt:

  • Unklare Semantik : Gibt die folgende Funktion 0 oder 1 aus? Ich kenne Sprachen, die 0 und 1 ausgeben würden. Beide haben Vorteile (sowohl in Bezug auf die Leistung als auch in Bezug auf das mentale Modell des Programmierers):

    int x;
    
    int f(out int y) {
      x = 0;
      y = 1;
      printf("%d\n", x);
    }
    f(out x);
    
  • Nicht lokalisierte Effekte : Wie im obigen Beispiel können Sie feststellen, dass Sie eine lange Kette haben können und die innerste Funktion den globalen Status beeinflusst. Im Allgemeinen wird es schwieriger, die Anforderungen der Funktion zu ermitteln und festzustellen, ob die Änderung zulässig ist. Angesichts der Tatsache, dass die meisten modernen Paradigmen versuchen, die Effekte zu lokalisieren (Kapselung in OOP) oder die Nebenwirkungen zu eliminieren (funktionale Programmierung), widerspricht sie diesen Paradigmen.

  • Redundanz : Wenn Sie Tupel haben, haben Sie 99% der Funktionalität von outParametern und 100% der idiomatischen Verwendung. Wenn Sie dem Mix Zeiger hinzufügen, decken Sie die restlichen 1% ab.

Ich habe Probleme, eine Sprache zu benennen, die mithilfe eines Tupels, einer Klasse oder eines outParameters nicht mehrere Werte zurückgeben kann (und in den meisten Fällen sind 2 oder mehr dieser Methoden zulässig).

Maciej Piechotka
quelle
+1 Um zu erwähnen, wie funktionale Sprachen auf elegante und schmerzlose Weise damit umgehen.
Andres F.
1
Technisch gesehen geben Sie immer noch einen einzelnen Wert zurück: D (Es ist nur so, dass es trivial ist, diesen einzelnen Wert in mehrere Werte zu zerlegen.)
Thomas Eding
1
Ich würde sagen, dass sich ein Parameter mit echter "out" -Semantik als temporärer Compiler verhalten sollte, der beim normalen Beenden einer Methode an das Ziel kopiert wird. Eine mit "inout" -Semantikvariable sollte sich wie ein temporärer Compiler verhalten, der bei der Eingabe aus der übergebenen Variablen geladen und beim Beenden zurückgeschrieben wird. Eine mit "ref" -Semantik sollte sich als Alias ​​verhalten. Die sogenannten "out" -Parameter von C # sind wirklich "ref" -Parameter und verhalten sich so.
Supercat
1
Das Tupel "Workaround" ist ebenfalls nicht kostenlos. Es blockiert Optimierungsmöglichkeiten. Wenn eine ABI vorhanden wäre, mit der N Rückgabewerte in CPU-Registern zurückgegeben werden können, könnte der Compiler tatsächlich optimieren, anstatt eine Tupelinstanz zu erstellen und diese zu erstellen.
BitTickler
1
@BitTickler Es gibt nichts, was die Rückgabe der ersten n Strukturfelder verhindert, die von den Registern übergeben werden, wenn Sie ABI steuern.
Maciej Piechotka
6

Ich denke, es liegt an Ausdrücken wie (a + b[i]) * c.

Ausdrücke bestehen aus "singulären" Werten. Eine Funktion, die einen singulären Wert zurückgibt, kann somit anstelle einer der vier oben gezeigten Variablen direkt in einem Ausdruck verwendet werden. Eine Funktion mit mehreren Ausgängen ist in einem Ausdruck zumindest etwas umständlich.

Ich persönlich finde , dass dies ist die Sache , die über einen singulären Rückgabewert Besonderes. Sie können dies umgehen, indem Sie eine Syntax hinzufügen, mit der Sie angeben, welcher der mehreren Rückgabewerte in einem Ausdruck verwendet werden soll. Diese Syntax ist jedoch umständlicher als die gute alte mathematische Notation, die jeder kennt und kurz fasst.

Roman Starkov
quelle
4

Dies verkompliziert die Syntax ein wenig, aber es gibt keinen guten Grund auf Implementierungsebene, dies nicht zuzulassen. Im Gegensatz zu einigen anderen Antworten führt die Rückgabe mehrerer Werte, sofern verfügbar, zu einem klareren und effizienteren Code. Ich kann nicht zählen, wie oft ich mir gewünscht habe, ein X und ein Y oder einen "Erfolg" -Booleschen Wert und einen nützlichen Wert zurückgeben zu können.

ddyer
quelle
3
Können Sie ein Beispiel nennen, bei dem mehrere Retouren einen klareren und / oder effizienteren Code liefern?
Steven Evers
3
Beispielsweise haben in der C ++ COM-Programmierung viele Funktionen einen [out]Parameter, aber praktisch alle geben einen HRESULT(Fehlercode) zurück. Es wäre sehr praktisch, dort ein Paar zu finden. In Sprachen, die Tupel gut unterstützen, wie z. B. Python, wird dies in vielen Codes verwendet, die ich gesehen habe.
Felix Dombek
In einigen Sprachen würden Sie einen Vektor mit der X- und Y-Koordinate zurückgeben, und die Rückgabe eines nützlichen Werts würde als "Erfolg" gewertet, mit Ausnahmen, die möglicherweise diesen nützlichen Wert enthalten und für Fehler verwendet werden.
Doppelgreener
3
In vielen Fällen verschlüsseln Sie Informationen auf nicht offensichtliche Weise in den Rückgabewert. negative Werte sind Fehlercodes, positive Werte sind Ergebnisse. Yuk. Wenn Sie auf eine Hash-Tabelle zugreifen, ist es immer unübersichtlich, anzuzeigen, ob der Artikel gefunden wurde, und den Artikel auch zurückzugeben.
Ddyer
@SteveEvers Die Matlab - sortFunktion normalerweise ein Array sortiert: sorted_array = sort(array). Manchmal brauche ich auch die entsprechende Indizes: [sorted_array, indices] = sort(array). Manchmal möchte ich nur, dass die Indizes: [~, indices]= sort (array) . The function sort` tatsächlich angeben können, wie viele Ausgabeargumente benötigt werden. Wenn also für 2 Ausgaben im Vergleich zu 1 zusätzliche Arbeit erforderlich ist, können diese Ausgaben nur bei Bedarf berechnet werden.
Gerrit
2

In den meisten Sprachen, in denen Funktionen unterstützt werden, können Sie einen Funktionsaufruf überall dort verwenden, wo eine Variable dieses Typs verwendet werden kann:

x = n + sqrt(y);

Wenn die Funktion mehr als einen Wert zurückgibt, funktioniert dies nicht. Dynamisch getippte Sprachen wie Python ermöglichen dies. In den meisten Fällen wird jedoch ein Laufzeitfehler ausgegeben, es sei denn, es kann etwas Sinnvolles mit einem Tupel in der Mitte einer Gleichung herausgearbeitet werden.

James Anderson
quelle
5
Verwenden Sie keine unangemessenen Funktionen. Dies unterscheidet sich nicht von dem "Problem", das durch Funktionen verursacht wird, die keine oder nicht numerische Werte zurückgeben.
Ddyer
3
In Sprachen, die ich verwendet habe und die mehrere Rückgabewerte bieten (z. B. SciLab), ist der erste Rückgabewert privilegiert und wird in Fällen verwendet, in denen nur ein Wert benötigt wird. Also kein echtes Problem da.
Das Photon
Und selbst wenn dies nicht der Fall ist, wie beim Auspacken des Python-Tupels, können Sie das gewünschte auswählen:foo()[0]
Izkata,
Wenn eine Funktion genau 2 Werte zurückgibt, ist ihr Rückgabetyp 2 Werte und kein einzelner Wert. Die Programmiersprache sollte Ihre Gedanken nicht lesen.
Mark E. Haase
1

Ich möchte nur auf Harveys Antwort aufbauen. Ich fand diese Frage ursprünglich auf einer Nachrichtentechnologieseite (arstechnica) und fand eine erstaunliche Erklärung dafür, dass ich der Meinung bin, dass sie den Kern dieser Frage wirklich beantwortet und allen anderen Antworten (außer denen von Harvey) fehlt:

Der Ursprung der Einzelrückgabe von Funktionen liegt im Maschinencode. Auf der Maschinencodeebene kann eine Funktion einen Wert im A-Register (Akkumulator) zurückgeben. Alle anderen Rückgabewerte befinden sich auf dem Stapel.

Eine Sprache, die zwei Rückgabewerte unterstützt, kompiliert diesen als Maschinencode, der einen zurückgibt, und legt den zweiten auf den Stapel. Mit anderen Worten, der zweite Rückgabewert würde ohnehin als out-Parameter enden.

Es ist, als würde man fragen, warum die Zuweisung jeweils eine Variable ist. Sie könnten beispielsweise eine Sprache haben, die a, b = 1, 2 zulässt. Aber es würde auf der Maschinencodeebene enden, die a = 1 gefolgt von b = 2 ist.

Es gibt einige Gründe dafür, dass Konstrukte von Programmiersprachen einen gewissen Einfluss darauf haben, was tatsächlich passiert, wenn der Code kompiliert und ausgeführt wird.

user2202911
quelle
Wenn Low-Level-Sprachen wie C als erstklassiges Feature mehrere Rückgabewerte unterstützen, würden C-Aufrufkonventionen mehrere Rückgabewertregister enthalten, so wie bis zu 6 Register für die Übergabe von Integer- / Pointer-Funktionsargumenten im x86-Format verwendet werden. 64 System V ABI. (In der Tat gibt x86-64 SysV Strukturen mit bis zu 16 Bytes zurück, die in das RDX: RAX-Registerpaar gepackt sind. Dies ist gut, wenn die Struktur gerade geladen wurde und gespeichert wird, kostet aber zusätzliches Entpacken im Vergleich dazu, dass separate Mitglieder sogar in separaten Registern enthalten sind wenn sie schmaler als 64 Bits sind.)
Peter Cordes
Die offensichtliche Konvention wäre RAX, dann die Arg-Passing-Regs. (RDI, RSI, RDX, RCX, R8, R9). Oder in der Windows x64-Konvention RCX, RDX, R8, R9. Da C jedoch von Haus aus nicht über mehrere Rückgabewerte verfügt, geben C-ABI- / Aufrufkonventionen nur mehrere Rückgaberegister für breite ganze Zahlen und einige Strukturen an. Unter Vorgehensweise Standard für die ARM-Architektur aufrufen: 2 separate, aber zusammengehörige Rückgabewerte finden Sie ein Beispiel für die Verwendung eines breiten Int, damit der Compiler 2 Rückgabewerte in ARM effizient empfängt.
Peter Cordes
-1

Es begann mit Mathe. FORTRAN, benannt nach "Formula Translation", war der erste Compiler. FORTRAN war und ist auf Physik / Mathematik / Technik ausgerichtet.

COBOL, fast so alt, hatte keinen expliziten Rückgabewert; Es gab kaum Unterprogramme. Seitdem ist es größtenteils Trägheit.

Go hat beispielsweise mehrere Rückgabewerte, und das Ergebnis ist klarer und weniger mehrdeutig als die Verwendung von "out" -Parametern. Nach ein wenig Gebrauch ist es sehr natürlich und effizient. Ich empfehle, für alle neuen Sprachen mehrere Rückgabewerte zu berücksichtigen. Vielleicht auch für alte Sprachen.

RickyS
quelle
4
diese beantwortet nicht die Frage gestellt
gnat
@gnat wie für mich antwortet es. OTOH man muss bereits einige Hintergrundinformationen benötigen, um es zu verstehen, und diese Person wird wahrscheinlich nicht die Frage stellen ...
Netch
@Netch man braucht kaum Hintergrundwissen, um zu verstehen, dass Aussagen wie "FORTRAN ... war der erste Compiler" ein totales Durcheinander sind. Es ist nicht einmal falsch. Genau wie der Rest dieser "Antwort"
Mücke
Link sagt, es habe frühere Versuche mit Compilern gegeben, aber "Dem von John Backus bei IBM angeführten FORTRAN-Team wird allgemein zugeschrieben, den ersten vollständigen Compiler im Jahr 1957 eingeführt zu haben". Die Frage war, warum nur einer? Wie ich sagte. Es war hauptsächlich Mathematik und Trägheit. Die mathematische Definition des Begriffs "Funktion" erfordert genau einen Ergebniswert. Es war also eine vertraute Form.
Rickys
-2

Dies hat wahrscheinlich mehr damit zu tun, wie Funktionsaufrufe in Prozessormaschinenanweisungen vorgenommen werden und dass alle Programmiersprachen vom Maschinencode abgeleitet sind, z. B. C -> Assembly -> Machine.

Wie Prozessoren Funktionsaufrufe ausführen

Die ersten Programme wurden in Maschinencode geschrieben und später zusammengesetzt. Die Prozessoren unterstützten Funktionsaufrufe, indem sie eine Kopie aller aktuellen Register in den Stapel schoben. Wenn Sie von der Funktion zurückkehren, wird der gespeicherte Registersatz vom Stapel gelöscht. Normalerweise wurde ein Register unberührt gelassen, damit die Rückgabefunktion einen Wert zurückgeben konnte.

Nun, warum die Prozessoren so entworfen wurden ... war es wahrscheinlich eine Frage der Ressourcenbeschränkungen.

Harvey
quelle