Ist Rekursion ein Merkmal an und für sich?

116

... oder ist es nur eine Übung?

Ich frage dies aufgrund eines Streits mit meinem Professor: Ich habe die Anerkennung dafür verloren, dass ich eine Funktion rekursiv aufgerufen habe, weil wir die Rekursion im Unterricht nicht behandelt haben, und mein Argument ist, dass wir sie implizit durch Lernen returnund Methoden gelernt haben .

Ich frage hier, weil ich vermute, dass jemand eine endgültige Antwort hat.

Was ist beispielsweise der Unterschied zwischen den folgenden beiden Methoden:

public static void a() {
    return a();
    }

public static void b() {
    return a();
    }

Gibt es außer " afür immer fortfahren" (im eigentlichen Programm wird es korrekt verwendet, um einen Benutzer erneut aufzufordern, wenn eine ungültige Eingabe erfolgt) einen grundlegenden Unterschied zwischen aund b? Wie werden sie für einen nicht optimierten Compiler unterschiedlich behandelt?

Letztlich kommt es durch das Lernen zu, ob nach unten return a()aus , bdass wir dafür auch gelernt return a()aus a. Haben wir?

Áxel Costas Pena
quelle
24
Hervorragende Debatte. Ich frage mich, ob Sie es Ihrem Professor so erklärt haben. Wenn Sie das getan haben, sollte er Ihnen Ihren verlorenen Kredit geben.
Michael Yaworski
57
Rekursion ist nicht einmal ein exklusives Konzept der Informatik. Die Fibonacci-Funktion, der Fakultätsoperator und viele andere Dinge aus der Mathematik (und möglicherweise anderen Bereichen) werden (oder können zumindest) rekursiv ausgedrückt. Verlangt der Professor, dass Sie diese Dinge auch nicht bemerken?
Theodoros Chatzigiannakis
34
Der Professor sollte ihm stattdessen zusätzliche Anerkennung dafür zollen, dass er einen eleganten Weg gefunden hat, um ein Problem zu lösen, oder dass er über den Tellerrand hinaus gedacht hat.
11
Was war die Aufgabe? Dies ist ein Problem, über das ich mich oft gewundert habe, wenn Sie eine Programmieraufgabe einreichen, was markiert wird, Ihre Fähigkeit, ein Problem zu lösen oder Ihre Fähigkeit, das Gelernte zu verwenden. Diese beiden sind nicht unbedingt gleich.
Leon
35
FWIW, wenn Sie zur Eingabe aufgefordert werden, bis es richtig ist, ist dies kein guter Ort, um die Rekursion zu verwenden. Es ist zu einfach, den Stapel zu überlaufen. Für diesen speziellen Fall wäre es besser, so etwas zu verwenden a() { do { good = prompt(); } while (!good); }.
Kevin

Antworten:

113

Um Ihre spezielle Frage zu beantworten: Nein, unter dem Gesichtspunkt des Lernens einer Sprache ist Rekursion keine Funktion. Wenn Ihr Professor Sie wirklich angedockt hat, weil Sie eine "Funktion" verwendet haben, die er noch nicht unterrichtet hat, war das falsch.

Wenn Sie zwischen den Zeilen lesen, besteht eine Möglichkeit darin, dass Sie durch die Verwendung der Rekursion vermieden haben, jemals eine Funktion zu verwenden, die als Lernergebnis für seinen Kurs gedacht war. Zum Beispiel haben Sie möglicherweise überhaupt keine Iteration verwendet, oder Sie haben nur forSchleifen verwendet, anstatt beide forund zu verwenden while. Es ist üblich, dass eine Aufgabe darauf abzielt, Ihre Fähigkeit zu testen, bestimmte Dinge zu tun, und wenn Sie dies vermeiden, kann Ihr Professor Ihnen die für diese Funktion vorgesehenen Noten einfach nicht gewähren. Wenn dies jedoch wirklich die Ursache für Ihre verlorenen Noten war, sollte der Professor dies als eigene Lernerfahrung betrachten. Wenn der Nachweis bestimmter Lernergebnisse eines der Kriterien für eine Aufgabe ist, sollte dies den Studenten klar erklärt werden .

Trotzdem stimme ich den meisten anderen Kommentaren und Antworten zu, dass Iteration hier eine bessere Wahl ist als Rekursion. Es gibt mehrere Gründe, und obwohl andere Menschen sie bis zu einem gewissen Grad berührt haben, bin ich mir nicht sicher, ob sie den Gedanken hinter ihnen vollständig erklärt haben.

Stapelüberläufe

Das offensichtlichere ist, dass Sie das Risiko eines Stapelüberlauffehlers haben. Realistisch gesehen ist es sehr unwahrscheinlich, dass die von Ihnen geschriebene Methode tatsächlich zu einer führt, da ein Benutzer viele Male falsche Eingaben machen müsste, um tatsächlich einen Stapelüberlauf auszulösen.

Beachten Sie jedoch, dass sich nicht nur die Methode selbst, sondern auch andere Methoden, die höher oder niedriger in der Aufrufkette sind, auf dem Stapel befinden. Aus diesem Grund ist es für jede Methode ziemlich unhöflich, den verfügbaren Stapelspeicherplatz gelegentlich zu verschlingen. Niemand möchte sich ständig um freien Speicherplatz sorgen müssen, wenn er Code schreibt, da das Risiko besteht, dass anderer Code unnötig viel davon verbraucht hat.

Dies ist Teil eines allgemeineren Prinzips im Software-Design, das als Abstraktion bezeichnet wird. Wenn Sie anrufen DoThing(), müssen Sie sich im Wesentlichen nur darum kümmern, dass die Sache erledigt ist. Sie sollten nicht über die Details der Implementierung zu kümmern, wie es gemacht wird . Die gierige Verwendung des Stapels verstößt jedoch gegen dieses Prinzip, da sich jedes Codebit Gedanken darüber machen muss, wie viel Stapel es sicher annehmen kann, dass es ihm durch Code an anderer Stelle in der Aufrufkette überlassen wurde.

Lesbarkeit

Der andere Grund ist die Lesbarkeit. Das Ideal, das Code anstreben sollte, ist ein für Menschen lesbares Dokument, in dem jede Zeile einfach beschreibt, was es tut. Nehmen Sie diese beiden Ansätze:

private int getInput() {
    int input;
    do {
        input = promptForInput();
    } while (!inputIsValid(input))
    return input;
}

gegen

private int getInput() {
    int input = promptForInput();
    if(inputIsValid(input)) {
        return input;
    }
    return getInput();
}

Ja, diese beiden funktionieren, und ja, sie sind beide ziemlich leicht zu verstehen. Aber wie könnten die beiden Ansätze auf Englisch beschrieben werden? Ich denke, es wäre so etwas wie:

Ich werde zur Eingabe auffordern, bis die Eingabe gültig ist, und sie dann zurückgeben

gegen

Ich werde zur Eingabe auffordern. Wenn die Eingabe gültig ist, gebe ich sie zurück. Andernfalls erhalte ich die Eingabe und gebe stattdessen das Ergebnis zurück

Vielleicht können Sie sich für letzteres etwas weniger klobige Formulierungen vorstellen, aber ich denke, Sie werden immer feststellen, dass die erste konzeptionell eine genauere Beschreibung dessen sein wird, was Sie tatsächlich versuchen zu tun. Dies bedeutet nicht, dass die Rekursion immer weniger lesbar ist. In Situationen, in denen es glänzt, wie z. B. beim Durchqueren von Bäumen, können Sie dieselbe Art der Nebeneinanderanalyse zwischen Rekursion und einem anderen Ansatz durchführen, und Sie werden mit ziemlicher Sicherheit feststellen, dass Rekursion Code liefert, der Zeile für Zeile klarer selbstbeschreibend ist.

Für sich genommen sind dies beide kleine Punkte. Es ist sehr unwahrscheinlich, dass dies jemals wirklich zu einem Stapelüberlauf führen würde, und der Gewinn an Lesbarkeit ist gering. Aber jedes Programm wird eine Sammlung vieler dieser kleinen Entscheidungen sein. Selbst wenn sie isoliert nicht viel ausmachen, ist es wichtig, die Prinzipien zu lernen, die dahinter stehen, um sie richtig zu machen.

Ben Aaronson
quelle
8
Können Sie Ihre Behauptung erweitern, dass Rekursion keine Funktion ist? Ich habe in meiner Antwort argumentiert, dass dies der Fall ist, da nicht alle Compiler dies unbedingt unterstützen.
Harry Johnston
5
Nicht alle Sprachen unterstützen notwendigerweise die Rekursion, daher ist es nicht unbedingt nur eine Frage der Auswahl des richtigen Compilers - aber Sie können zu Recht sagen, dass "Feature" eine inhärent mehrdeutige Beschreibung ist, also fair genug. Ihr zweiter Punkt ist auch aus der Sicht von jemandem fair, der das Programmieren lernt (wie es jetzt üblich ist), ohne vorher einen Hintergrund in der Programmierung von Maschinencode zu haben. :-)
Harry Johnston
2
Beachten Sie, dass das Problem der Lesbarkeit ein Problem mit der Syntax ist. Rekursion ist von Natur aus nicht "unlesbar". In der Tat ist Induktion der einfachste Weg, induktive Datenstrukturen wie Schleifen und Listen und Sequenzen usw. auszudrücken. Und die meisten Datenstrukturen sind induktiv.
Nomen
6
Ich denke, Sie haben das Deck mit Ihrer Formulierung gestapelt. Sie haben die iterative Version funktional beschrieben und umgekehrt. Ich denke, eine gerechtere zeilenweise Beschreibung von beiden wäre: „Ich werde zur Eingabe auffordern. Wenn die Eingabe nicht gültig ist, werde ich die Eingabeaufforderung so lange wiederholen, bis ich eine gültige Eingabe erhalte. Dann werde ich es zurückgeben. " vs “Ich werde zur Eingabe auffordern. Wenn die Eingabe gültig ist, werde ich sie zurückgeben. Andernfalls werde ich das Ergebnis einer Überarbeitung zurückgeben. “ (Meine Kinder haben das funktionale Konzept eines Do-Over verstanden, als sie in der Vorschule waren, daher denke ich, dass es hier eine legitime englische Zusammenfassung des rekursiven Konzepts ist.)
pjs
2
@HarryJohnston Die fehlende Unterstützung für die Rekursion wäre eher eine Ausnahme von vorhandenen Funktionen als das Fehlen einer neuen Funktion. Insbesondere im Zusammenhang mit dieser Frage ein „neues Feature“ bedeutet „nützlich Verhalten haben wir noch nicht existiert gelehrt“, das nicht wahr Rekursion ist , weil es ist eine logische Erweiterung von Funktionen , die wurden (nämlich gelehrt, dass die Verfahren enthalten Anweisungen und Prozeduraufrufe sind Anweisungen). Es ist, als hätte der Professor einen Studentenzusatz gelehrt und ihn dann beschimpft, weil er denselben Wert mehr als einmal hinzugefügt hat, weil "wir die Multiplikation nicht behandelt haben".
nmclean
48

Um die wörtliche Frage und nicht die Meta-Frage zu beantworten: Rekursion ist ein Merkmal in dem Sinne, dass nicht alle Compiler und / oder Sprachen dies unbedingt zulassen. In der Praxis wird es von allen (gewöhnlichen) modernen Compilern erwartet - und sicherlich von allen Java-Compilern! - aber es ist nicht allgemein wahr.

Als ein erfundenes Beispiel dafür, warum Rekursion möglicherweise nicht unterstützt wird, betrachten Sie einen Compiler, der die Rücksprungadresse für eine Funktion an einem statischen Ort speichert. Dies kann beispielsweise bei einem Compiler für einen Mikroprozessor ohne Stapel der Fall sein.

Für einen solchen Compiler, wenn Sie eine Funktion wie diese aufrufen

a();

es ist implementiert als

move the address of label 1 to variable return_from_a
jump to label function_a
label 1

und die Definition von a (),

function a()
{
   var1 = 5;
   return;
}

wird implementiert als

label function_a
move 5 to variable var1
jump to the address stored in variable return_from_a

Hoffentlich liegt das Problem auf, wenn Sie versuchen, einen a()solchen Compiler rekursiv aufzurufen . Der Compiler weiß nicht mehr, wie er vom äußeren Aufruf zurückkehren soll, da die Rücksprungadresse überschrieben wurde.

Für den Compiler, den ich tatsächlich verwendet habe (Ende der 70er oder Anfang der 80er Jahre, glaube ich), ohne Unterstützung für die Rekursion, war das Problem etwas subtiler: Die Rücksprungadresse würde wie bei modernen Compilern auf dem Stapel gespeichert, lokale Variablen jedoch nicht 't. (Theoretisch sollte dies bedeuten, dass eine Rekursion für Funktionen ohne nicht statische lokale Variablen möglich war, aber ich kann mich nicht erinnern, ob der Compiler dies explizit unterstützt hat oder nicht. Möglicherweise wurden aus irgendeinem Grund implizite lokale Variablen benötigt.)

Mit Blick auf die Zukunft kann ich mir spezielle Szenarien vorstellen - vielleicht stark parallele Systeme -, in denen es vorteilhaft sein könnte, nicht für jeden Thread einen Stapel bereitzustellen, und in denen eine Rekursion daher nur zulässig ist, wenn der Compiler sie in eine Schleife umgestalten kann. (Natürlich waren die primitiven Compiler, die ich oben diskutiere, nicht in der Lage, komplizierte Aufgaben wie das Refactoring von Code auszuführen.)

Harry Johnston
quelle
Beispielsweise unterstützt der C-Präprozessor keine Rekursion in Makros. Makrodefinitionen verhalten sich ähnlich wie Funktionen, können jedoch nicht rekursiv aufgerufen werden.
etw
7
Ihr "erfundenes Beispiel" ist nicht allzu erfunden: Der Fortran 77-Standard erlaubte es Funktionen nicht, sich selbst rekursiv aufzurufen - der Grund dafür ist so ziemlich das, was Sie beschreiben. (Ich glaube, die Adresse, zu der gesprungen werden soll, als die Funktion ausgeführt wurde, wurde am Ende des Funktionscodes selbst gespeichert oder etwas, das dieser Anordnung entspricht.) Hier finden Sie ein wenig dazu.
Alexis
5
Shader-Sprachen oder GPGPU-Sprachen (z. B. GLSL, Cg, OpenCL C) unterstützen beispielsweise keine Rekursion. Insofern ist das Argument "nicht alle Sprachen unterstützen es" sicherlich gültig. Die Rekursion setzt das Äquivalent eines Stapels voraus (es muss nicht unbedingt ein Stapel sein , aber es muss ein Mittel vorhanden sein, um Rücksprungadressen und Funktionsrahmen irgendwie zu speichern ).
Damon
Ein Fortran-Compiler, an dem ich Anfang der 1970er Jahre ein wenig gearbeitet habe, hatte keinen Aufrufstapel. Jede Unterroutine oder Funktion hatte statische Speicherbereiche für die Rücksprungadresse, Parameter und ihre eigenen Variablen.
Patricia Shanahan
2
Sogar einige Versionen von Turbo Pascal haben die Rekursion standardmäßig deaktiviert, und Sie mussten eine Compiler-Direktive festlegen, um sie zu aktivieren.
Dan04
17

Der Lehrer möchte wissen, ob Sie studiert haben oder nicht. Anscheinend haben Sie das Problem nicht so gelöst, wie er es Ihnen beigebracht hat ( der gute Weg ; Iteration), und denken daher, dass Sie es nicht getan haben. Ich bin alle für kreative Lösungen, aber in diesem Fall muss ich Ihrem Lehrer aus einem anderen Grund zustimmen:
Wenn der Benutzer zu oft ungültige Eingaben macht (dh indem Sie die Eingabetaste gedrückt halten), haben Sie eine Stapelüberlauf-Ausnahme und Ihre Lösung wird abstürzen. Darüber hinaus ist die iterative Lösung effizienter und einfacher zu warten. Ich denke, das ist der Grund, warum dein Lehrer dich hätte geben sollen.

Mike
quelle
2
Wir wurden nicht angewiesen, diese Aufgabe auf eine bestimmte Weise auszuführen. und wir lernten Methoden, nicht nur Iteration. Außerdem würde ich verlassen, welches nach persönlichen Vorlieben leichter zu lesen ist: Ich habe ausgewählt, was für mich gut aussieht. Der SO-Fehler ist für mich neu, obwohl die Idee, dass Rekursion ein Merkmal an und für sich ist, immer noch nicht begründet zu sein scheint.
3
"Ich würde gehen, welches leichter nach persönlichen Vorlieben zu lesen ist". Einverstanden. Rekursion ist keine Java-Funktion. Das sind.
Mike
2
@Vality: Tail Call Elimination? Einige JVMs tun dies möglicherweise, aber denken Sie daran, dass für Ausnahmen auch ein Stack-Trace verwaltet werden muss. Wenn die Eliminierung von Tail-Aufrufen möglich ist, kann die naiv generierte Stapelverfolgung ungültig werden, sodass einige JVMs aus diesem Grund keine TCE ausführen.
icktoofay
5
In beiden Fällen ist es eine ziemlich schlechte Form, sich auf die Optimierung zu verlassen, um Ihren fehlerhaften Code weniger fehlerhaft zu machen.
CHao
7
+1, sehen Sie, dass in Ubuntu vor kurzem der Anmeldebildschirm unterbrochen wurde, als der Benutzer die Eingabetaste kontinuierlich drückte, das gleiche geschah mit der XBox
Sebastian
13

Es ist schrecklich, Punkte abzuziehen, weil "wir die Rekursion im Unterricht nicht behandelt haben". Wenn Sie gelernt haben, wie man die Funktion A aufruft, die die Funktion B aufruft, die die Funktion C aufruft, die zu B zurückkehrt, die zu A zurückkehrt, die zum Anrufer zurückkehrt, und der Lehrer Ihnen nicht ausdrücklich gesagt hat, dass dies verschiedene Funktionen sein müssen (welche) Dies wäre beispielsweise in alten FORTRAN-Versionen der Fall. Es gibt keinen Grund, warum A, B und C nicht alle dieselbe Funktion haben können.

Auf der anderen Seite müssten wir den tatsächlichen Code sehen, um zu entscheiden, ob in Ihrem speziellen Fall die Verwendung von Rekursion wirklich das Richtige ist. Es gibt nicht viele Details, aber es klingt falsch.

gnasher729
quelle
10

In Bezug auf die spezifische Frage, die Sie gestellt haben, gibt es viele Gesichtspunkte, aber ich kann sagen, dass Rekursion vom Standpunkt des Lernens einer Sprache keine eigenständige Funktion ist. Wenn Ihr Professor Ihnen wirklich Punkte für die Verwendung eines "Features" angedockt hat, das er noch nicht gelehrt hat, war das falsch, aber wie gesagt, es gibt andere Gesichtspunkte, die hier berücksichtigt werden müssen, damit der Professor beim Abziehen von Punkten tatsächlich Recht hat.

Aus dem, was ich aus Ihrer Frage ableiten kann, ist die Verwendung einer rekursiven Funktion, um bei einem Eingabefehler nach einer Eingabe zu fragen, keine gute Praxis, da der Aufruf jeder rekursiven Funktion auf den Stapel übertragen wird. Da diese Rekursion durch Benutzereingaben gesteuert wird, ist es möglich, eine unendliche rekursive Funktion zu haben, was zu einem StackOverflow führt.

Es gibt keinen Unterschied zwischen diesen beiden Beispielen, die Sie in Ihrer Frage im Sinne ihrer Funktionsweise erwähnt haben (aber auf andere Weise unterscheiden). In beiden Fällen werden eine Absenderadresse und alle Methodeninformationen in den Stapel geladen. In einem Rekursionsfall ist die Rücksprungadresse einfach die Zeile direkt nach dem Methodenaufruf (natürlich ist es nicht genau das, was Sie im Code selbst sehen, sondern im Code, den der Compiler erstellt hat). In Java, C und Python ist die Rekursion im Vergleich zur Iteration (im Allgemeinen) ziemlich teuer, da ein neuer Stapelrahmen zugewiesen werden muss. Ganz zu schweigen davon, dass Sie eine Stapelüberlaufausnahme erhalten können, wenn die Eingabe nicht zu oft gültig ist.

Ich glaube, der Professor hat Punkte abgezogen, da Rekursion als eigenständiges Thema betrachtet wird und es unwahrscheinlich ist, dass jemand ohne Programmiererfahrung an Rekursion denkt. (Natürlich heißt das nicht, dass sie es nicht tun, aber es ist unwahrscheinlich).

IMHO, ich denke, der Professor hat Recht, indem er Ihnen die Punkte abzieht. Sie hätten den Validierungsteil leicht auf eine andere Methode übertragen und wie folgt verwenden können:

public bool foo() 
{
  validInput = GetInput();
  while(!validInput)
  {
    MessageBox.Show("Wrong Input, please try again!");
    validInput = GetInput();
  }
  return hasWon(x, y, piece);
}

Wenn das, was Sie getan haben, tatsächlich auf diese Weise gelöst werden kann, war das, was Sie getan haben, eine schlechte Praxis und sollte vermieden werden.

Yonatan Nir
quelle
Der Zweck der Methode selbst besteht darin, die Eingabe zu validieren, dann das Ergebnis einer anderen Methode aufzurufen und zurückzugeben (deshalb gibt sie sich selbst zurück). Um genau zu sein, wird geprüft, ob der Zug in einem Tic-Tac-Toe-Spiel gültig ist, und dann zurückgegeben hasWon(x, y, piece)(um nur die betroffenen Zeilen und Spalten zu überprüfen).
Sie könnten einfach NUR den Validierungsteil nehmen und ihn in eine andere Methode namens "GetInput" einfügen und ihn dann so verwenden, wie ich es in meiner Antwort geschrieben habe. Ich habe meine Antwort so bearbeitet, wie sie aussehen soll. Natürlich können Sie GetInput einen Typ zurückgeben lassen, der die benötigten Informationen enthält.
Yonatan Nir
1
Yonatan Nir: Wann war Rekursion eine schlechte Praxis? Vielleicht wird JVM explodieren, weil die Hotspot-VM aufgrund der Sicherheit des Bytecodes nicht optimiert werden konnte, und so wäre ein gutes Argument. Wie unterscheidet sich Ihr Code von einem anderen Ansatz?
1
Rekursion ist nicht immer eine schlechte Praxis, aber wenn sie vermieden werden kann und der Code sauber und leicht zu pflegen ist, sollte sie vermieden werden. In Java, C und Python ist die Rekursion im Vergleich zur Iteration (im Allgemeinen) ziemlich teuer, da ein neuer Stapelrahmen zugewiesen werden muss. In einigen C-Compilern kann man ein Compiler-Flag verwenden, um diesen Overhead zu beseitigen, der bestimmte Arten von Rekursionen (tatsächlich bestimmte Arten von Tail-Aufrufen) in Sprünge anstelle von Funktionsaufrufen umwandelt.
Yonatan Nir
1
Es ist nicht klar, aber wenn Sie eine Schleife durch eine unbestimmte Anzahl von Iterationen durch Rekursion ersetzt haben, ist das schlecht. Java garantiert keine Tail-Call-Optimierung, sodass Ihnen möglicherweise der Stapelspeicherplatz ausgeht. Verwenden Sie in Java keine Rekursion, es sei denn, Sie haben garantiert eine begrenzte Anzahl von Iterationen (normalerweise logarithmisch im Vergleich zur Gesamtgröße der Daten).
Hyde
6

Vielleicht hat Ihr Professor es noch nicht gelehrt, aber es scheint, als wären Sie bereit, die Vor- und Nachteile der Rekursion zu lernen.

Der Hauptvorteil der Rekursion besteht darin, dass rekursive Algorithmen oft viel einfacher und schneller zu schreiben sind.

Der Hauptnachteil der Rekursion besteht darin, dass rekursive Algorithmen Stapelüberläufe verursachen können, da für jede Rekursionsstufe ein zusätzlicher Stapelrahmen zum Stapel hinzugefügt werden muss.

Bei Produktionscode, bei dem die Skalierung in der Produktion zu viel mehr Rekursionsstufen führen kann als bei den Komponententests des Programmierers, überwiegt der Nachteil normalerweise den Vorteil, und rekursiver Code wird häufig vermieden, wenn dies praktikabel ist.

Warren Dew
quelle
1
Jeder potenziell riskante rekursive Algorithmus kann immer trivial umgeschrieben werden, um einen expliziten Stapel zu verwenden - der Aufrufstapel ist schließlich nur ein Stapel. In diesem Fall würde es lächerlich aussehen, wenn Sie die Lösung für die Verwendung eines Stapels umschreiben - ein weiterer Beweis dafür, dass die rekursive Antwort nicht sehr gut ist.
Aaronaught
1
Wenn Stapelüberläufe ein Problem darstellen, sollten Sie eine Sprache / Laufzeit verwenden, die die Tail-Call-Optimierung unterstützt, z. B. .NET 4.0 oder eine andere funktionale Programmiersprache
Sebastian,
Nicht alle Rekursionen sind Tail Calls.
Warren Dew
6

In Bezug auf die spezifische Frage ist Rekursion ein Merkmal, ich neige dazu, ja zu sagen, aber nachdem ich die Frage neu interpretiert habe. Es gibt gängige Entwurfsoptionen für Sprachen und Compiler, die eine Rekursion ermöglichen, und es gibt Turing-vollständige Sprachen , die eine Rekursion überhaupt nicht zulassen . Mit anderen Worten, Rekursion ist eine Fähigkeit, die durch bestimmte Auswahlmöglichkeiten im Sprach- / Compiler-Design ermöglicht wird.

  • Die Unterstützung erstklassiger Funktionen ermöglicht eine Rekursion unter sehr geringen Annahmen. Ein Beispiel finden Sie unter Schreiben von Schleifen in Unlambda oder in diesem stumpfen Python-Ausdruck, der keine Selbstreferenzen, Schleifen oder Zuweisungen enthält:

    >>> map((lambda x: lambda f: x(lambda g: f(lambda v: g(g)(v))))(
    ...   lambda c: c(c))(lambda R: lambda n: 1 if n < 2 else n * R(n - 1)),
    ...   xrange(10))
    [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
    
  • Sprachen / Compiler, die eine späte Bindung verwenden oder Vorwärtsdeklarationen definieren , ermöglichen eine Rekursion. Während Python beispielsweise den folgenden Code zulässt, ist dies eine Entwurfsauswahl (späte Bindung) und keine Voraussetzung für ein Turing-Complete System. Gegenseitig rekursive Funktionen hängen häufig von der Unterstützung von Vorwärtsdeklarationen ab.

    factorial = lambda n: 1 if n < 2 else n * factorial(n-1)
    
  • Statisch typisierte Sprachen , die rekursiv definierte Typen zulassen , tragen zur Aktivierung der Rekursion bei. Siehe diese Implementierung des Y-Kombinators in Go . Ohne rekursiv definierte Typen wäre es immer noch möglich, die Rekursion in Go zu verwenden, aber ich glaube, dass der Y-Kombinator spezifisch unmöglich wäre.

wberry
quelle
1
Dies ließ meinen Kopf explodieren, besonders die Unlambda +1
John Powell
Festkomma-Kombinatoren sind schwer. Als ich mich entschied, funktionale Programmierung zu lernen, zwang ich mich, den Y-Kombinator zu studieren, bis ich ihn verstanden hatte, und ihn dann anzuwenden, um andere nützliche Funktionen zu schreiben. Hat eine Weile gedauert, hat sich aber gelohnt.
Wberry
5

Nach dem, was ich aus Ihrer Frage ableiten kann, ist es keine gute Praxis, eine rekursive Funktion zu verwenden, um im Falle eines Eingabefehlers nach Eingaben zu fragen. Warum?

Weil jeder Aufruf rekursiver Funktionen auf den Stapel übertragen wird. Da diese Rekursion durch Benutzereingaben gesteuert wird, ist es möglich, eine unendliche rekursive Funktion zu haben, was zu einem StackOverflow führt :-P

Eine nicht rekursive Schleife zu haben, um dies zu tun, ist der richtige Weg.

Remudada
quelle
Der Großteil der fraglichen Methode und der Zweck der Methode selbst besteht darin, die Eingabe durch verschiedene Überprüfungen zu validieren. Der Vorgang beginnt von vorne, wenn die Eingabe ungültig ist, bis die Eingabe korrekt ist (wie angewiesen).
4
@fay Wenn die Eingabe jedoch zu oft ungültig ist, erhalten Sie einen StackOverflowError. Rekursion ist eleganter, aber in meinen Augen eher ein Problem als eine normale Schleife (aufgrund von Stapeln).
Michael Yaworski
1
Das ist also ein interessanter und guter Punkt. Ich hatte diesen Fehler nicht berücksichtigt. Kann der gleiche Effekt durch while(true)Aufrufen derselben Methode erzielt werden? Wenn ja, würde ich nicht sagen, dass dies einen Unterschied zwischen Rekursion unterstützt, gut zu wissen, wie es ist.
1
@fay while(true)ist eine Endlosschleife. Wenn Sie keine breakAussage haben, sehe ich keinen Sinn darin, es sei denn, Sie versuchen, Ihr Programm zum Absturz zu bringen lol. Mein Punkt ist, wenn Sie dieselbe Methode aufrufen (das ist Rekursion), erhalten Sie manchmal einen StackOverflowError , aber wenn Sie eine whileoder- forSchleife verwenden, wird dies nicht der Fall sein . Das Problem besteht bei einer regulären Schleife einfach nicht. Vielleicht habe ich dich missverstanden, aber meine Antwort an dich ist nein.
Michael Yaworski
4
Dies scheint mir ehrlich gesagt wahrscheinlich der wahre Grund zu sein, warum der Professor die Noten abgenommen hat =) Er hat es vielleicht nicht sehr gut erklärt, aber es ist eine berechtigte Beschwerde zu sagen, dass Sie es auf eine Weise verwendet haben, die als sehr schlechter Stil angesehen würde, wenn nicht in fehlerhafterem Code völlig fehlerhaft.
Commander Coriander Salamander
3

Rekursion ist ein Programmierkonzept , ein Merkmal (wie Iteration) und eine Praxis . Wie Sie dem Link entnehmen können, gibt es einen großen Forschungsbereich, der sich diesem Thema widmet. Vielleicht müssen wir nicht so tief in das Thema eintauchen, um diese Punkte zu verstehen.

Rekursion als Feature

Im Klartext unterstützt Java dies implizit, da es einer Methode (die im Grunde eine spezielle Funktion ist) ermöglicht, "Wissen" über sich selbst und andere Methoden zu haben, aus denen die Klasse besteht, zu der sie gehört. Stellen Sie sich eine Sprache vor, in der dies nicht der Fall ist: Sie könnten den Hauptteil dieser Methode schreiben a, aber Sie könnten keinen Aufruf an asie aufnehmen. Die einzige Lösung wäre, Iteration zu verwenden, um das gleiche Ergebnis zu erhalten. In einer solchen Sprache müssten Sie zwischen Funktionen unterscheiden, die sich ihrer eigenen Existenz bewusst sind (indem Sie ein bestimmtes Syntax-Token verwenden), und solchen, die dies nicht tun! Tatsächlich macht eine ganze Gruppe von Sprachen diese Unterscheidung (siehe Lisp und Lambdas ), um sich rekursiv zu nennen (wiederum mit einer dedizierten Syntax). zum Beispiel die Familien ML ). Interessanterweise erlaubt Perl sogar anonyme Funktionen (sogenannte)

keine Rekursion?

Für Sprachen, die nicht einmal die Möglichkeit einer Rekursion unterstützen, gibt es häufig eine andere Lösung in Form des Festkomma-Kombinators. Die Sprache muss jedoch weiterhin Funktionen als sogenannte erstklassige Objekte unterstützen (dh Objekte, die möglicherweise vorhanden sind) innerhalb der Sprache selbst manipuliert).

Rekursion als Praxis

Wenn diese Funktion in einer Sprache verfügbar ist, bedeutet dies nicht, dass sie idiomatisch ist. In Java 8 wurden Lambda-Ausdrücke aufgenommen, sodass es möglicherweise einfacher wird, einen funktionalen Ansatz für die Programmierung zu verwenden. Es gibt jedoch praktische Überlegungen:

  • Die Syntax ist immer noch nicht sehr rekursionsfreundlich
  • Compiler sind möglicherweise nicht in der Lage, diese Praxis zu erkennen und zu optimieren

Das Endergebnis

Glücklicherweise (oder genauer gesagt, um die Verwendung zu vereinfachen) lässt Java Methoden standardmäßig auf sich aufmerksam machen und unterstützt daher die Rekursion. Dies ist also kein wirklich praktisches Problem, aber es bleibt immer noch ein theoretisches, und das nehme ich an Ihr Lehrer wollte es speziell ansprechen. Außerdem könnte es angesichts der jüngsten Entwicklung der Sprache in Zukunft zu etwas Wichtigem werden.

didierc
quelle