Wie verstehe ich diesen Rekursionscode?


12

Ich habe diesen Code im Handbuch gefunden An Introduction to Programming in Emacs Lisp, das die Rekursion mit Hilfe der condFunktion demonstriert , um die Anzahl der Kieselsteine ​​basierend auf der eingegebenen Anzahl der Zeilen zu ermitteln, dh wenn Zeilen = 2, sollten die Kiesel 3 sein, wenn 4 Zeilen, dann sollten es 10 Kieselsteine ​​sein Dort.

(defun triangle-using-cond (number)
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

bewerte bis 10 nachdem du Argument 4 bestanden hast:

(triangle-using-cond 4)

Das Handbuch hat nicht klar erklärt, was bei jedem Schritt in diesem Beispielcode passiert, und ich konnte nicht herausfinden, wie die Rekursion hier funktioniert. Können Sie mir bitte helfen, die Mechanik Schritt für Schritt zu verstehen, was in jedem Fall passiert?


Ich überlasse es jemand anderem, Ihnen mit dem Wort "Rekursion" zu helfen (weil ich es für etwas anderes halte als in diesem Kontext) oder besser zu erklären, was ich schreiben werde: (a) wenn die Zahl kleiner oder gleich ist auf 0, dann 0; (b) wenn die Zahl gleich 1 ist, dann 1; (c) Wenn number größer als 1 ist, wird number zu dem von der Funktion zurückgegebenen Wert hinzugefügt, triangle-using-condwobei das Argument 1 kleiner ist als die Zahl. Die Bedingungen gehen in der Reihenfolge von a, b und dann c - was zuerst passt, ist, wo der Bock aufhört.
Gesetzesliste

elisp interpreter wertet wie gewohnt von innen nach außen aus. So ist 1-4 = 3. Nun wird der rekursive Aufruf sein (triangle-using-cond 3), aber das wird immer wieder mit demselben rekursiven Aufruf enden, bis er die 1-Bedingung erfüllt, richtig? Was wird als nächstes passieren?
Promotion

Oh, ich verstehe - die Funktion wird in Schritt 3 wieder verwendet - okay, guter Punkt.
Gesetzesliste

Ich frage mich, was das Ergebnis sein würde (triangle-using-cond 3)?
Promotion

2
nb Die Funktion 1-hat einen besonders irreführenden Namen, insbesondere wenn Sie einen Anruf so lesen, als wäre er eine Infixnotation. Es gibt das Argument minus eins zurück. NICHT eins minus das Argument.
Phils

Antworten:


14

Verwendung von "printf debugging"

Sie können sich von Emacs verständigen lassen, indem Sie die Funktionsdefinition ändern:

(defun triangle-using-cond (number)
  (message (format "called with %d" number))
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

Fügen Sie einfach (message ...)irgendwo hinzu, um eine Spur in den *Messages*Puffer zu drucken .

Verwenden von Edebug

Platzieren Sie den Punkt irgendwo in der Funktionsdefinition und drücken Sie, um ihn C-u C-M-xzu "instrumentieren". Bewerten Sie dann die Funktion, indem Sie z. B. einen Punkt nach dem anderen platzieren (triangle-using-cond 3)und auf den anderen schlagen C-x C-e.

Sie befinden sich jetzt im Edebug-Modus. Drücke die Leertaste, um die Funktion zu durchlaufen. Die Zwischenwerte der einzelnen Ausdrücke werden im Echobereich angezeigt. Um den Edebug-Modus zu verlassen, drücken Sie einfach q. Um die Instrumentierung zu entfernen, platzieren Sie den Punkt an einer beliebigen Stelle in der Definition und drücken Sie C-M-x, um die Definition neu zu bewerten.

Verwendung des Standard-Emacs-Debuggers

M-x debug-on-entry triangle-using-condWenn triangle-using-condSie dann aufgerufen werden, werden Sie in den Emacs-Debugger (Puffer *Backtrace*) versetzt.

Durchlaufen Sie die Bewertung mit d(oder c, um uninteressante Bewertungen zu überspringen).

Den Zwischenzustand (variable Werte usw.) können Sie jederzeit eanzeigen. Sie werden aufgefordert, ein Geschlecht für die Bewertung einzugeben, und das Bewertungsergebnis wird gedruckt.

Bewahren Sie während der Verwendung des Debuggers eine Kopie des Quellcodes in einem anderen Frame auf, damit Sie verfolgen können, was vor sich geht.

Sie können auch explizite Aufrufe einfügen, um den Debugger (mehr oder weniger Haltepunkte) an beliebigen Stellen im Quellcode einzugeben. Sie fügen (debug)oder ein (debug nil SOME-SEXP-TO-EVALUATE). Im letzteren Fall wird bei Eingabe des Debuggers SOME-SEXP-TO-EVALUATEausgewertet und das Ergebnis ausgedruckt. (Denken Sie daran, dass Sie diesen Code in den Quellcode einfügen und C-M-xzur Auswertung verwenden können. Machen Sie dies dann rückgängig - Sie müssen die bearbeitete Datei nicht speichern.)

Using DebuggerWeitere Informationen finden Sie im Elisp-Handbuch, Knoten .

Rekursion als Schleife

Stellen Sie sich Rekursion jedenfalls als Schleife vor. Es sind zwei Kündigungsfälle definiert: (<= number 0)und (= number 1). In diesen Fällen gibt die Funktion eine einfache Zahl zurück.

Im rekursiven Fall gibt die Funktion die Summe dieser Zahl und das Ergebnis der Funktion mit zurück number - 1. Eventuell wird die Funktion mit 1einer Zahl kleiner oder gleich Null aufgerufen .

Das rekursive Fallergebnis lautet daher:

(+ number (+ (1- number) (+ (1- (1- number)) ... 1)

Nimm zum Beispiel (triangle-using-cond 4). Lassen Sie uns den endgültigen Ausdruck akkumulieren:

  • In der ersten Iteration numberfolgt 4also die (> number 1)Verzweigung. Wir bauen einen Ausdruck auf (+ 4 ...und rufen die Funktion mit (1- 4), dh auf (triangle-using-cond 3).

  • jetzt numberist 3und das Ergebnis ist (+ 3 (triangle-using-cond 2)). Der Gesamtergebnisausdruck ist (+ 4 (+ 3 (triangle-using-cond 2))).

  • numberist 2jetzt, so ist der Ausdruck(+ 4 (+ 3 (+ 2 (triangle-using-cond 1))))

  • numberist 1jetzt, und wir nehmen den (= number 1)Zweig, was zu einer langweiligen 1. Der ganze Ausdruck ist (+ 4 (+ 3 (+ 2 1))). Beurteilen Sie, dass von innen nach außen und Sie bekommen: (+ 4 (+ 3 3)), (+ 4 6), oder einfach nur 10.


3
Edebug wird noch besser sein. =)
Malabarba

Wie kann man den Trail drucken lassen, wenn man das message (...)trifft, C-x C-ezeigt das Endergebnis (10) nichts anderes? Vermisse ich etwas?
Doktorat

@Malabarba, wie man es Edebugin die Tat umsetzt ?
Promotion

1
@doctorate traf C-u C-M-xmit einem Punkt in der Funktion, um das Problem zu beheben. Dann einfach die Funktion wie gewohnt ausführen.
Malabarba

@doctorate das (message ...)Zeug druckt in den *Message*Puffer.
Rekado

6

Das Substitutionsmodell für die Prozeduranwendung von SICP kann den Algorithmus erklären, um Code wie diesen zu verstehen.

Ich habe auch Code geschrieben, um dies zu erleichtern. lispy-flattenvon lispy package macht das. Hier ist das Ergebnis der Bewerbung lispy-flattenbei (triangle-using-cond 4):

(cond ((<= 4 0)
       0)
      ((= 4 1)
       1)
      ((> 4 1)
       (+ 4 (triangle-using-cond (1- 4)))))

Sie können den obigen Ausdruck folgendermaßen vereinfachen:

(+ 4 (triangle-using-cond 3))

Dann noch einmal abflachen:

(+ 4 (cond ((<= 3 0)
            0)
           ((= 3 1)
            1)
           ((> 3 1)
            (+ 3 (triangle-using-cond (1- 3))))))

Das Endergebnis:

(+ 4 (+ 3 (+ 2 1)))

3

Dies ist nicht spezifisch für Emacs / Elisp, aber wenn Sie einen mathematischen Hintergrund haben, ist eine Rekursion wie eine mathematische Induktion . (Oder wenn Sie nicht: Wenn Sie Induktion lernen, ist es wie eine Rekursion!)

Beginnen wir mit der Definition:

(defun triangle-using-cond (number)
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

Wann numberist 4, gilt keine der beiden ersten Bedingungen, so dass es nach der dritten Bedingung
(triangle-using-cond 4)ausgewertet wird : wird ausgewertet als
(+ number (triangle-using-cond (1- number))), nämlich als
(+ 4 (triangle-using-cond 3)).

Ebenso
(triangle-using-cond 3)wird ausgewertet als
(+ 3 (triangle-using-cond 2)).

Ebenso (triangle-using-cond 2)wird ausgewertet als
(+ 2 (triangle-using-cond 1)).

Aber für (triangle-using-cond 1)gilt die zweite Bedingung, und es wird als ausgewertet 1.

Ein Tipp für alle, die Rekursion lernen: Vermeiden Sie es

Der übliche Anfängerfehler, zu versuchen, darüber nachzudenken, was während des rekursiven Aufrufs geschieht, anstatt zu vertrauen, dass der rekursive Aufruf funktioniert (manchmal auch als rekursiver Glaubenssprung bezeichnet).

Wenn Sie sich selbst davon überzeugen möchten, ob (triangle-using-cond 4)die richtige Antwort zurückgegeben wird, gehen Sie einfach davon aus, dass (triangle-using-cond 3)die richtige Antwort zurückgegeben wird, und überprüfen Sie, ob sie in diesem Fall korrekt ist. Natürlich müssen Sie auch den Basisfall überprüfen.


2

Die Berechnungsschritte für Ihr Beispiel sehen folgendermaßen aus:

(4 +               ;; step 1
   (3 +            ;; step 2
      (2 +         ;; step 3
         (1))))    ;; step 4
=> 10

Die 0-Bedingung ist eigentlich nie erfüllt, da 1 als Eingabe die Rekursion bereits beendet.


(1)ist kein gültiger Ausdruck.
Rekado

1
Es bewertet ganz gut mit M-x calc. :-) Im Ernst, ich wollte die Berechnung zeigen, nicht die Lisp-Bewertung.
Paprika

Oh, ich habe nicht einmal bemerkt, dass es (4 +statt (+ 4in Ihrer Antwort ist ... :)
Rekado

0

Ich denke, es ist ziemlich einfach, dafür braucht man keinen Emacs Lisp, es ist nur Grundschulmathematik.

f (0) = 0

f (1) = 1

f (n) = f (n - 1) + n, wenn n> 1 ist

also ist f (5) = 5 + f (4) = 5 + 4 + f (3) = 5 + 4 + 3 + 2 + 1 + 0

Jetzt ist es offensichtlich.


Bei dieser Funktion wird f (0) jedoch nie aufgerufen.
Rekado
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.