Skip to content

Commit 5b1f224

Browse files
committed
Tail calls
1 parent 6bed734 commit 5b1f224

1 file changed

Lines changed: 101 additions & 134 deletions

File tree

i1akku.tex

Lines changed: 101 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -334,36 +334,6 @@ \section{Zwischenergebnisse mitführen}
334334
korrekt, sondern auch schnell: Sie benutzt keine Hilfsfunktion und
335335
macht soviele rekursive Aufrufe wie die Eingabeliste Elemente hat, ihre
336336
Laufzeit wächst also \textit{linear}.\index{lineares Wachstum}
337-
Dass das so ist, kannst Du sehen, wenn Du die Auswertung eines Aufrufs
338-
von \lstinline{invert} nachvollziehst:
339-
%
340-
\begin{alltt}\small
341-
(invert \underline{(list 1 2 3 4)})
342-
\(\ldots\evalsto\) (\underline{invert-helper} #<list 1 2 3 4> empty)
343-
\(\ldots\evalsto\) (cond ((empty? #<list 1 2 3 4>) ...) ((cons? #<list 1 2 3 4>) ...))
344-
\(\ldots\evalsto\) (invert-helper (rest #<list 1 2 3 4>) (cons (first #<list 1 2 3 4>) empty))
345-
\(\ldots\evalsto\) (invert-helper #<list 2 3 4> (cons 1 empty))
346-
\(\ldots\evalsto\) (invert-helper #<list 2 3 4> #<list 1>)
347-
\(\ldots\evalsto\) (cond ((empty? #<list 2 3 4>) ...) ((cons? #<list 2 3 4>) ...))
348-
\(\ldots\evalsto\) (invert-helper (rest #<list 2 3 4>) (cons (first #<list 2 3 4>) #<list 1>))
349-
\(\ldots\evalsto\) (invert-helper #<list 3 4> (cons 2 #<list 1>))
350-
\(\ldots\evalsto\) (invert-helper #<list 3 4> #<list 2 1>)
351-
\(\ldots\evalsto\) (cond ((empty? #<list 3 4>) ...) ((cons? #<list 3 4>) ...))
352-
\(\ldots\evalsto\) (invert-helper (rest #<list 3 4>) (cons (first #<list 3 4>) #<list 2 1>))
353-
\(\ldots\evalsto\) (invert-helper #<list 4> (cons 3 #<list 2 1>))
354-
\(\ldots\evalsto\) (invert-helper #<list 4> #<list 3 2 1>)
355-
\(\ldots\evalsto\) (cond ((empty? #<list 4>) ...) ((cons? #<list 4>) ...))
356-
\(\ldots\evalsto\) (invert-helper (rest #<list 4>) (cons (first #<list 4>) empty))
357-
\(\ldots\evalsto\) (invert-helper #<empty-list> (cons 4 #<list 3 2 1>))
358-
\(\ldots\evalsto\) (invert-helper #<empty-list> #<list 4 3 2 1>)
359-
\(\ldots\evalsto\) (cond ((empty? #<empty-list>) #<list 4 3 2 1>) ((cons? #<empty-list>) ...))
360-
\(\evalsto\) #<list 4 3 2 1>
361-
\end{alltt}
362-
%
363-
Die höherere Effizienz hat allerdings auch ihren Preis. Du merkst
364-
vielleicht, dass auffällig viele der Erläuterungen hier in
365-
Anführungszeichen stehen. Das liegt daran, dass wir gar nicht so
366-
einfach erklären können, wie wir die neue Funktion konstruiert haben.
367337
368338
Übrigens: Da die Funktion \lstinline{invert} generell nützlich ist,
369339
ist sie unter dem Namen
@@ -378,7 +348,7 @@ \section{Schablonen für Funktionen mit Akkumulator}
378348
379349
Wir nehmen uns eine Funktion vor, die wir eigentlich schon kennen,
380350
nämlich aus Abschnitt~\ref{sec:list-sum} auf Seite
381-
\pageref{sec:list-sum}:
351+
\pageref{sec:list-sum}:\label{function:list-sum-acc}
382352
%
383353
\begin{lstlisting}
384354
; Summe der Elemente einer Liste von Zahlen berechnen
@@ -557,7 +527,7 @@ \section{Schablonen für Funktionen mit Akkumulator}
557527
(list-sum-helper list0 0)))
558528
\end{lstlisting}
559529
%
560-
Als Nächstes ist der \lstinline{empty}-Zweig dran. Hier ist
530+
Als nächstes ist der \lstinline{empty}-Zweig dran. Hier ist
561531
\lstinline{sum} die Summe aller Elemente vor \lstinline{list}, und,
562532
weil \lstinline{list} leer ist, sind das \emph{alle} Elemente von
563533
\lstinline{list0}. Deswegen ist \lstinline{sum} das gewünschte
@@ -1014,110 +984,107 @@ \section{Aktienkurse analysieren}
1014984
\section{Kontext und Endrekursion}
1015985
\label{sec:iteration}
1016986
1017-
FIXME: "`strukturell rekursiv"'
987+
In diesem Abschnitt werfen wir einen Blick darauf, was eigentlich im
988+
Rechner abläuft bei der Auswertung rekursiver Funktionsaufrufe. Dabei
989+
wird ein wichtiger Unterschied zwischen den Funktionen mit Akkumulator
990+
und den "<normalen"> Funktionen davor sichtbar.
1018991
992+
Als Beispiel betrachten wir ein weiteres Mal \lstinline{list-sum},
993+
zunächst in der Version mit Akkumulator aus
994+
Abschnitt~\ref{function:list-sum-acc} auf
995+
Seite~\pageref{function:list-sum-acc}. Am besten ist, wenn Du Dir
996+
selbst im Stepper die Auswertung von
997+
\begin{lstlisting}
998+
(list-sum (list 1 2 3 4))
999+
\end{lstlisting}
1000+
%
1001+
anschaust. Hier sind die wichtigsten Schritte bei der Auswertung:
1002+
%
1003+
\begin{lstlisting}
1004+
(list-sum #<list 1 2 3 4>)
1005+
|\evalsto| (accumulate #<list 1 2 3 4> 0)
1006+
|\evalsto| (accumulate (rest #<list 1 2 3 4>) (+ (first #<list 1 2 3 4>) 0))
1007+
|\evalsto| (accumulate #<list 2 3 4> 1)
1008+
|\evalsto| (accumulate #<list 3 4> 3)
1009+
|\evalsto| (accumulate #<list 4> 6)
1010+
|\evalsto| (accumulate #<empty-list> 10)
1011+
|\evalsto| 10
1012+
\end{lstlisting}
1013+
%
1014+
Wir haben den Wert des \lstinline{sum}-Parameters immer untereinander
1015+
geschrieben, und man sieht gut, wie sich das Zwischenergebnis von 0
1016+
schrittweise auf das Endergebnis 10 zubewegt.
10191017
1020-
FIXME
1021-
1022-
Ein Vergleich der beiden Versionen der Fakultätsfunktion von
1023-
S.~\pageref{page:factorial} und S.~\pageref{page:factorial-tail} zeigt, dass
1024-
Formulierungen mit und ohne Akkumulator
1025-
unterschiedliche Berechnungsprozesse erzeugen. Hier ein Prozess mit
1026-
Akkumulator:
1027-
%
1028-
\begin{alltt}
1029-
(! 4)
1030-
\(\Longrightarrow\) (!-helper 4 1)
1031-
\(\Longrightarrow\) (if (= 4 0) 1 (!-helper (- 4 1) (* 1 4)))
1032-
\(\Longrightarrow\) (if #f 1 (!-helper (- 4 1) (* 1 4)))
1033-
\(\Longrightarrow\) (!-helper (- 4 1) (* 1 4))
1034-
\(\Longrightarrow\) (!-helper 3 4)
1035-
\(\Longrightarrow\) (if (= 3 0) 4 (!-helper (- 3 1) (* 4 3)))
1036-
\(\Longrightarrow\) (if #f 4 (!-helper (- 3 1) (* 4 3)))
1037-
\(\Longrightarrow\) (!-helper (- 3 1) (* 4 3))
1038-
\(\Longrightarrow\) (!-helper 2 12)
1039-
\(\Longrightarrow\) (if (= 2 0) 12 (!-helper (- 2 1) (* 12 2)))
1040-
\(\Longrightarrow\) (if #f 12 (!-helper (- 2 1) (* 12 2)))
1041-
\(\Longrightarrow\) (!-helper (- 2 1) (* 12 2))
1042-
\(\Longrightarrow\) (!-helper 1 24)
1043-
\(\Longrightarrow\) (if (= 1 0) 24 (!-helper (- 1 1) (* 24 1)))
1044-
\(\Longrightarrow\) (if #f 24 (!-helper (- 1 1) (* 24 1)))
1045-
\(\Longrightarrow\) (!-helper (- 1 1) (* 1 24))
1046-
\(\Longrightarrow\) (!-helper 0 24)
1047-
\(\Longrightarrow\) (if (= 0 0) 24 (!-helper (- 0 1) (* 24 0)))
1048-
\(\Longrightarrow\) (if #t 24 (!-helper (- 0 1) (* 24 0)))
1049-
\(\Longrightarrow\) 24
1050-
\end{alltt}
1051-
%
1052-
Demgegenüber hier der Prozess ohne Akkumulator:
1053-
%
1054-
\begin{alltt}
1055-
(! 4)
1056-
\(\Longrightarrow\) (if (= 4 0) 1 (* 4 (! (- 4 1))))
1057-
\(\Longrightarrow\) (if #f 1 (* 4 (! (- 4 1))))
1058-
\(\Longrightarrow\) (* 4 (! (- 4 1)))
1059-
\(\Longrightarrow\) (* 4 (! 3))
1060-
\(\Longrightarrow\) (* 4 (if (= 3 0) 1 (* 3 (! (- 3 1)))))
1061-
\(\Longrightarrow\) (* 4 (if #f 1 (* 3 (! (- 3 1)))))
1062-
\(\Longrightarrow\) (* 4 (* 3 (! (- 3 1))))
1063-
\(\Longrightarrow\) (* 4 (* 3 (! 2)))
1064-
\(\ldots\)
1065-
\(\Longrightarrow\) (* 4 (* 3 (* 2 (! 1))))
1066-
\(\Longrightarrow\) (* 4 (* 3 (* 2 (if (= 1 0) 1 (* 1 (! (- 1 1)))))))
1067-
\(\Longrightarrow\) (* 4 (* 3 (* 2 (if #f ... (* 1 (! (- 1 1)))))))
1068-
\(\Longrightarrow\) (* 4 (* 3 (* 2 (* 1 (! (- 1 1))))))
1069-
\(\Longrightarrow\) (* 4 (* 3 (* 2 (* 1 (! 0)))))
1070-
\(\Longrightarrow\) (* 4 (* 3 (* 2 (* 1 (if (= 0 0) 1 (* 0 (! (- 0 1))))))))
1071-
\(\Longrightarrow\) (* 4 (* 3 (* 2 (* 1 (if #t 1 (* 0 (! (- 0 1)))))))))
1072-
\(\Longrightarrow\) (* 4 (* 3 (* 2 (* 1 1))))
1073-
\(\Longrightarrow\) (* 4 (* 3 (* 2 1)))
1074-
\(\Longrightarrow\) (* 4 (* 3 2))
1075-
\(\Longrightarrow\) (* 4 6)
1076-
\(\Longrightarrow\) 24
1077-
\end{alltt}
1078-
%
1079-
Es ist deutlich sichtbar, dass die Version ohne Akkumulator alle
1080-
Multiplikationen bis zum Schluss "<aufstaut">. Das heißt aber auch,
1081-
dass im Laufe des Berechnungsprozesses Ausdrücke auftauchen, die desto
1082-
größer werden je größer das Argument von \texttt{!} ist: Bei
1083-
\texttt{(! 100)} werden zum Beispiel 100 Multiplikationen aufgestaut.
1084-
1085-
Die Version mit Akkumulator hingegen scheint in der Größe der
1086-
zwischenzeitlich auftretenden Ausdrücke begrenzt zu sein. Tatsächlich
1087-
stellt sich das Wachstum der Version ohne Akkumulator bei der Version
1088-
mit Akkumulator nicht ein.
1089-
1090-
Der Grund dafür sind die Schablonen: In der Schablone für Funktionen
1091-
ohne Akkumulator steht \texttt{(... (proc (- n 1)) ...)}, das
1092-
heißt, um den rekursiven Aufruf von \texttt{proc} wird noch
1093-
etwas "<herumgewickelt">, oder, anders gesagt, mit dem Ergebnis des
1094-
rekursiven Aufrufs passiert noch etwas. Das, was mit dem Ergebnis
1095-
noch passiert, heißt der \textit{Kontext\index{Kontext}} des Aufrufs.
1096-
Bei \texttt{!} ist der vollständige Ausdruck \texttt{(* n (! (- n
1097-
1)))}. Wenn aus diesem Ausdruck der rekursive Aufruf \texttt{(! (-
1098-
n 1))} herausgenommen wird, bleibt der Kontext \texttt{(* n
1099-
\(\circ\))}, wobei $\circ$ markiert, wo der Aufruf entfernt
1100-
wurde. Tatsächlich wird in der Literatur diese Markierung
1101-
\textit{Loch\index{Loch}} genannt und \texttt{[]} geschrieben. Der
1102-
Kontext \texttt{(* n [])} macht deutlich, dass mit Ergebnis eines
1103-
Aufrufs, der später für \texttt{[]} eingesetzt wird, noch \texttt{n}
1104-
multipliziert wird. Dementsprechend stauen sich in der
1105-
Reduktionsfolge die Multiplikationen mit den verschiedenen Werten von
1106-
\texttt{n}.
1107-
1108-
Bei der Fakultäts-Funktion mit Akkumulator ist der Ausdruck, zu dem
1109-
der Rumpf bei $\texttt{n} \neq 0$ reduziert wird, \texttt{(!-helper (- n 1)
1110-
(* n acc))}. Der Kontext des Aufrufs von \texttt{!-helper} innerhalb
1111-
dieses Ausdrucks ist \texttt{[]}, also \emph{leer}~-- \emph{nichts}
1112-
passiert mehr mit dem Rückgabewert von \texttt{!-helper}, und damit stauen
1113-
sich auch bei der Reduktion keine Kontexte an. Solche Funktionaufrufe
1114-
ohne Kontext heißen \textit{endrekursiv\index{endrekursiv}}~-- eben,
1115-
weil nach dem rekursiven Aufruf "<Ende"> ist.\footnote{Das Konzept des
1116-
Aufrufs ohne Kontext ist nicht auf rekursive Aufrufe beschränkt. Im
1117-
Englischen heißen solche Aufrufe allgemeiner \textit{tail
1118-
calls\index{tail call}} (also ohne "<recursive">).}
1119-
Die Berechnungsprozesse, die von endrekursiven Aufrufen generiert
1120-
werden, heißen auch \textit{iterative\index{Iteration}} Prozesse.
1018+
Wenn Du das "<alte"> \lstlisting{list-sum} in
1019+
Abschnitt~\ref{sec:list-sum} auf Seite~\ref{sec:list-sum} im Stepper
1020+
laufen lässt, sieht das schon optisch ganz anders aus:
1021+
%
1022+
\begin{lstlisting}
1023+
(list-sum #<list 1 2 3 4>)
1024+
|\evalsto| (+ (first #<list 1 2 3 4>) (list-sum (rest #<list 1 2 3 4>)))
1025+
|\evalsto| (+ 1 (list-sum #<list 2 3 4>))
1026+
|\evalsto| (+ 1 (+ 2 (list-sum #<list 3 4>))
1027+
|\evalsto| (+ 1 (+ 2 (+ 3 (list-sum #<list 4>))))
1028+
|\evalsto| (+ 1 (+ 2 (+ 3 (+ 4 (list-sum #<empty-list>)))))
1029+
|\evalsto| (+ 1 (+ 2 (+ 3 (+ 4 0))))
1030+
|\evalsto| (+ 1 (+ 2 (+ 3 4)))
1031+
|\evalsto| (+ 1 (+ 2 7))
1032+
|\evalsto| (+ 1 9)
1033+
|\evalsto| 10
1034+
\end{lstlisting}
1035+
%
1036+
Hier sieht man, dass die rekursiven Aufrufe die einzelnen Additionen
1037+
"<aufstauen">, und die eigentliche Arbeit der Addition erst nach dem
1038+
letzten rekursiven Aufruf stattfindet. Dieses "<Aufstauen"> kommt
1039+
daher, dass der rekursive Aufruf in der alten Version innerhalb des
1040+
Aufrufs von \lstinline{+} steht:
1041+
%
1042+
\begin{lstlisting}
1043+
(+ (first list) (list-sum (rest list)))
1044+
\end{lstlisting}
1045+
%
1046+
Bei der Version mit Akkumulator ist es genau umgekehrt und der
1047+
rekursive Aufruf steht um den Aufruf von \lstinline{+} herum:
1048+
%
1049+
\begin{lstlisting}
1050+
(accumulate (rest list) (+ (first list) sum))
1051+
\end{lstlisting}
1052+
%
1053+
Wenn der Computer in der alten Version einen rekursiven Aufruf
1054+
auswertet, muss er sich merken, dass nach dem rekursiven Aufruf noch
1055+
eine Addition passieren muss. Entsprechend wird die Kette von
1056+
\lstinline{+}-Aufrufen bei jedem rekursiven Aufruf länger. Dieser
1057+
Aufruf von \lstinline{+} heißt der \textit{Kontext}\index{Kontext} des
1058+
rekursiven Aufrufs~-- er ist um ihn herumgewickelt.
1059+
1060+
Bei der Version mit Akkumulator hat der rekursive Aufruf von
1061+
\lstinline{accumulate} keinen Kontext, dementsprechend staut sich bei
1062+
der Auswertung und im Stepper da auch nichts auf. Ein rekursiver
1063+
Aufruf ohne Kontext heißt \textit{endrekursiv}\index{Endrekursion},
1064+
weil nach dem Aufruf nichts mehr passieren muss, der Aufruf also "<am
1065+
Ende"> steht.
1066+
1067+
Der Begriff "<Endrekursion"> ist etwas unglücklich: Ob ein
1068+
Funktionsaufruf einen Kontext hat oder nicht, hat eigentlich gar
1069+
nichts damit zu tun, ob er rekursiv ist oder nicht. Im Englischen
1070+
gibt es den besseren Begriff \textit{tail call}
1071+
\index{tail call@\textit{tail call}}, der sowohl auf rekursive
1072+
als auch nicht-rekursive Aufrufe zutrifft.
1073+
1074+
Dass der Aufruf im alten \lstinline{list-sum} nicht endrekursiv ist,
1075+
legt schon die Schablone fest, in der das Ergebnis des rekursiven
1076+
Aufrufs noch mit dem ersten Element der Liste kombiniert werden muss.
1077+
Bei der Schablone für Funktionen mit Akkumulator ist das nicht so,
1078+
entsprechend haben die entstehenden rekursiven Aufrufe auch keinen
1079+
Kontext.
1080+
1081+
Die Auswertungsprozesse, die von endrekursiven Aufrufen generiert
1082+
werden, gehen in einer geraden Linie voran und heißen auch
1083+
\textit{iterative\index{Iteration}} Prozesse. In anderen
1084+
Programmiersprachen spricht man auch von
1085+
\textit{Schleifen}\index{Schleife}; viele Programmiererinnen und
1086+
Programmierer benutzen deshalb den Namen \lstinline{loop} statt
1087+
\lstinline{accumulate}.
11211088
11221089
\section{Das Phänomen der umgedrehten Liste}
11231090

0 commit comments

Comments
 (0)