Kontext und Escape-Prozedur Kontext und Escape
Transcription
Kontext und Escape-Prozedur Kontext und Escape
KONTROLLABSTRAKTION Vorgehen: Kontext und Escape-Prozedur “Control” ←→ Ablaufsteuerung “Kontrollstruktur”: Beschreibung bzw. Festlegung der Ausführungsreihenfolge von Anweisungen oder Programmeinheiten. Ziel: Semantisch wohldefinierte und transparente funktionale Verallgemeinerung der Sprunganweisung (befehls-orientierte Programmiersprachen). Kontrollabstraktion in Analogie zur Datenabstraktion: Abstraktion über bestimmte Anweisungen oder Programmeinheiten. Es wird nur betrachtet, in welcher Weise sie miteinander verknüpft werden. Kompositionalitätsprinzip bleibt gewahrt: Ein Term wird ausgewertet, indem man seine Teile auswertet und dann die Auswertungsresultate miteinander verknüpft. • Kontrollstrukturen auf der Ebene der Anweisungen: Ordnung der Aktivierung einzelner Anweisungen oder Befehle, Wenn ein bestimmter Teilausdruck ausgewertet wird, kann man die jeweils noch ausstehenden Teilauswertungen sowie den Kombinationsschritt als “Fortsetzung” zusammengefaßt denken. • Kontrollstrukturen auf der Ebene der Programmeinheiten: Ordnung der Aktivierung von Programmeinheiten (abgeschlossener Folgen von Anweisungen). G. Görz, FAU, Inf.8 10–1 G. Görz, FAU, Inf.8 Continuation 10–3 Kontext und Escape-Prozedur (2) • Mit dem Begriff des Kontexts wird die Erzeugung einer Prozedur im Hinblick auf die Auswertung eines bestimmten Teilausdrucks eines Terms formalisiert. zentrales Konzept der Kontrollabstraktion Dynamische Sichtweise auf Berechnungsprozesse: Wir betrachten einen gerade aktiven Berechnungsprozess als Folge zeitlich geordneter Verarbeitungsschritte. • Escape-Prozeduren sind Funktionen, die, wenn aufgerufen, nicht zum Punkt ihres Aufrufs zurückkehren, d.h. die Kontrolle nicht an die aufrufende Funktion zurückgeben. Vom Standpunkt eines bestimmten gerade in Ausführung befindlichen “aktuellen” Verarbeitungsschritts soll über alle zukünftigen Verarbeitungsschritte abstrahiert werden. ⇒ Continuations als Kontexte verstehen, die zu einer Escape-Funktion gemacht wurden. In der funktionalen Programmierung: Einführung dieser sog. Continuation (“Fortsetzung”) als Funktionsobjekt. Standardprozedur call-with-current-continuation (abgekürzt: call/cc) macht in einem Programm Continuations explizit verfügbar — unmittelbare Schnittstelle zum Kontrollregime des Systems. Damit kann der Programmierer die “Zukunft” einer Berechnung, d.h. ihren Berechnungskontext, “einfangen”, um sie später zu benutzen! G. Görz, FAU, Inf.8 G. Görz, FAU, Inf.8 Das so gewonnene abstrakte Objekt repräsentiert die Zukunft der Berechnung. 10–2 10–4 ist die Closure Kontexte (1) Ein Kontext ist eine einstellige Prozedur; zur Unterscheidung von anderen Prozeduren wird die Argument-Variable mit notiert. Kontexte sind relativ zu Unterausdrücken eines Ausdrucks definiert: Betrachten wir Teilausdruck e eines Ausdrucks E. Unter dem Kontext von e in E versteht man diejenige einstellige Prozedur c, die — Abwesenheit von Seiteneffekten vorausgesetzt — auf den Wert von e angewandt, den Wert von E liefert. (lambda () (begin (writeln (* (+ 5) 2))) n)) Die Kontext-Prozedur muss die Bindung der freien Variablen n aufbewahren. Die Auswertung kann also über let und if so weit fortschreiten, bis erreicht ist, muss aber n bewahren, das in ihrer Umgebung die Bindung des let-Ausdrucks, also den Wert 1 hat. Was ist zu tun, wenn Seiteneffekte ins Spiel kommen? Sei E Term (+ 3 (* 4 (+ 5 6))) und (+ 5 6) der Teilterm e. Der Kontext c von e in E ist die Prozedur (lambda () (+ 3 (* 4 ))) Wert von E: (c 11) = 47. G. Görz, FAU, Inf.8 10–5 Kontexte (2) 10–7 Kontexte (3): Erweiterung bei Seiteneffekten werden in drei Schritten gebildet: (begin (writeln 0) (let ((n 1)) (if (zero? n) (writeln (* 5 6)) (writeln (* (+ (* 3 4) 5) 2))) (set! n (+ n 2)) ; Seiteneffekt mit n n)) 1. Ersetzung des Referenzobjektes durch , 2. Auswertung des Gesamtausdruckes bis zum Vorkommen von 3. Einbettung des Ergebnisses in einen lambda-Ausdruck. Im zweiten Schritt wird zunächst (writeln 0) ausgeführt, bevor der Kontext als diejenige Prozedur bestimmt wird, die der Wert ist von Beispiel: Kontext von (* 3 4) in (let ((n 1)) (if (zero? n) (writeln (* 5 6)) (writeln (* (+ (* 3 4) 5) 2))) n) G. Görz, FAU, Inf.8 G. Görz, FAU, Inf.8 (lambda () (writeln (* (+ 5) 2)) (set! n (+ n 2)) n)) 10–6 G. Görz, FAU, Inf.8 10–8 Die Bindung von n ist die zum Zeitpunkt des Betretens des let-Ausdrucks, der Wert von n ist 1. Bei jeder Aktivierung des Kontexts wird also der Wert derselben Variablen n um 2 erhöht. Kontexte sind i.a. Prozeduren, die einen Zustand haben! Escape-Prozeduren: Illustration Annahme: Es gäbe einstellige Prozedur escaper, die aus der ihr als Argument übergebenen Prozedur p die entsprechende Escape-Prozedur berechnet und als Wert zurückgibt: > (+ ((escaper *) 5 2) 3) 10 > (+ ((escaper (lambda (x) (- (* x 3) 7))) 5) 4) 8 G. Görz, FAU, Inf.8 10–9 Escape-Prozeduren Sei e eine Escape-Prozedur und f eine beliebige andere Prozedur, dann gilt: (compose f e) ≡ e D.h., für alle Ausdrücke <expr> ist ≡ 10–11 > (+ ((escaper (lambda (x) ((escaper -) ((escaper *) x 3) 7))) 5) 4) 15 liefern einen Wert, geben diesen aber nicht an den wartenden Kontext zurück. Stattdessen: Berechnungskontext wird aufgegeben und der Wert der Escape-Prozedur ist zugleich Wert der Gesamtberechnung. (f (e <expr>)) G. Görz, FAU, Inf.8 (e <expr>) Der Kontext von (e <expr>) in (f (e <expr>)) ist (lambda () (f )) ≡ f und wird beim Aufruf von e aufgegeben! G. Görz, FAU, Inf.8 10–10 G. Görz, FAU, Inf.8 10–12 Continuations als Kontexte in Form von Escape-Prozeduren Bildung der Continuation: Beispiel (+ 3 (* 4 (call/cc (lambda (kont) 6)))) Einführung der fiktiven Prozedur escaper, um die Standardprozedur call-with-current-continuation (abgekürzt: call/cc) zu erklären, durch die Continuations explizit verfügbar gemacht werden können. call/cc ist eine einstellige Prozedur, deren Argument Empfänger (engl. receiver ) heißt und das selbst auch eine einstellige Prozedur ist. Das Argument des Empfängers heißt aktuelle Continuation und ist wiederum eine einstellige Prozedur, so dass gilt Der Kontext von (call/cc r) mit r = (lambda (kont) 6) ist diejenige Prozedur, die Wert von (lambda () (+ 3 (* 4 ))) ist. Somit hat der Beispielausdruck dieselbe Bedeutung wie (+ 3 (* 4 ((lambda (kont) 6) (escaper (lambda () (+ 3 (* 4 )))) ))) (call/cc <receiver>) ≡ (<receiver> <continuation>) G. Görz, FAU, Inf.8 10–13 G. Görz, FAU, Inf.8 Bildung der Continuation 10–15 Ablaufgeschehen Sei ein Ausdruck E gegeben, innerhalb dessen (call/cc r) mit dem Empfänger r vorkommt. Nachdem der Kontext von (call/cc r) gebildet wurde, wird er als Escape-Prozedur an r übergeben (normaler Prozeduraufruf). Wird bei der Auswertung von E dieser call/cc-Term erreicht, wird dessen Kontext c bestimmt. Wert von Dann kann (call/cc r) unter Beibehaltung der Ablaufsemantik von E ersetzt werden durch (r (escaper c)). Das durch (escaper c) bestimmte Prozedurobjekt ist die Continuation von (call/cc r) in E. ((lambda (kont) 6) (escaper (lambda () (+ 3 (* 4 ))))) ist 6, denn der Wert des Parameters kont, die Continuation, wird im Rumpf von (lambda (kont) 6) gar nicht benutzt — er besteht nur aus der Konstanten 6! Gesamtergebnis: 27 G. Görz, FAU, Inf.8 10–14 G. Görz, FAU, Inf.8 10–16 Ersetzen wir die Konstante 6 im Rumpf durch (kont 6), wird 6 an die (einstellige) Continuation als Aktualparameter übergeben: Vereinfachungsregeln ((escaper (lambda () (+ 3 (* 4 )))) 6) 1. Ein Aufruf der Continuation auf der obersten Ebene im Empfänger kann weggelassen werden: Gesamtergebnis: 27 (call/cc (lambda (k) (k <body>))) ≡ (call/cc (lambda (k) <body>)) 2. Wenn in (call/cc (escaper (lambda (k) (k <body>)))) bei der Auswertung von <body> die Continuation k oder eine andere Escape-Prozedur aktiviert wird, gilt: G. Görz, FAU, Inf.8 10–17 Ablaufgeschehen (2) Betten wir den expliziten Aufruf der Continuation in einen anderen Term ein, z.B. lambda-Ausdruck (lambda (kont) (+ 2 (kont 6))) resultiert ebenfalls 27 (nicht etwa 35 !) Der Aufruf der Continuation bewirkt den Aufruf der Escape-Prozedur, die ihren Kontext (escaper (lambda () (+ 3 (* 4 (+ 2 ))))) verwirft. Damit wurde aus einer an sich noch anstehenden Berechnung “herausgesprungen”. G. Görz, FAU, Inf.8 10–18 G. Görz, FAU, Inf.8 10–19 (call/cc (escaper (lambda (k) (k <body>)))) ≡ (call/cc (lambda (k) <body>)) Der Kontext eines call/cc-Aufrufs wird also in eine Escape-Prozedur überführt. Da Prozeduren in Scheme Datentypen erster Klasse sind, also gleichberechtigt zu anderen Werten behandelt werden, und die Escape-Prozedur eine Prozedur ist, ist es möglich, dieselbe Continuation auch mehrfach aufzurufen. Sie wird ihren Wert nicht an die zur Aufrufzeit aktuelle Continuation, sondern an die durch call/cc festgehaltene Continuation weitergeben. Nachdem die aktuelle Continuation mit Hilfe des aktuellen Kontexts gebildet wird und Kontexte Zustandsinformation enthalten dürfen, können auch Continuations einen lokalen Zustand verwalten. Auf diese Weise hat man einen sehr mächtigen Mechanismus an der Hand, mit dem man komplexe Kontrollstrukturen realisieren kann. G. Görz, FAU, Inf.8 10–20 Zur Illustration Ausnahmebehandlung Frage: Was geschieht bei (* 2 (call/cc (lambda (k) (+ 3 (k 4))))) Häufig tritt bei Berechnungen die Situation ein, dass ein Wert erreicht wird, aufgrund dessen es nicht mehr sinnvoll ist, die Berechnung fortzusetzen. ?? Wird k mit einem Wert v (hier: 4) während der Auswertung des Ausdrucks aufgerufen, so wird v unmittelbar als Wert der ganzen call/ccAuswertung zurückgegeben. Resultat: 8 Die Addition wird nicht ausgeführt! (call/cc (lambda (k) (cons ’a ’()))) ==> (a) G. Görz, FAU, Inf.8 10–21 (call/cc (lambda (k) (cons ’a (k 3)))) ==> 3 (define (product lyst) (call/cc (lambda (exit-on-zero) (letrec ((product1 (lambda (l) (cond ((null? l) 1) ((zero? (car l)) G. Görz, FAU, Inf.8 10–23 (exit-on-zero 0)) (else (* (car l) (product1 (cdr l)))) )) )) (product1 lyst))) )) Was ergibt (call/cc (lambda (k) (cons (k 3) (k 4))) ? ==> undefiniert (Reihenfolge der Argument-Auswertung!) G. Görz, FAU, Inf.8 Beispiel: Abbruch der Multiplikation einer Liste von Zahlen bei Erreichen von 0: 10–22 G. Görz, FAU, Inf.8 10–24 Beispiel: Länge echter Listen Verwendung einer Continuation nach Rückkehr aus call/cc Für Paare, die keine Listen sind, soll #f zurückgegeben werden: > (let ((x (call/cc (lambda (k) k)) )) (x (lambda (ignore) "hi"))) "hi" (define (list-length obj) (call/cc (lambda (return) (letrec ((r (lambda (obj) (cond ((null? obj) 0) ((pair? obj) (+ (r (cdr obj)) 1)) (else (return #f))) ))) (r obj))) )) G. Görz, FAU, Inf.8 Was ist die durch den Aufruf von call/cc gewonnene Continuation? let-Ausdruck: “Nimm den Wert, binde ihn an x, und wende den Wert von x auf (lambda (ignore) ‘‘hi’’) an”. Da (lambda (k) k) sein Argument k als Wert zurückgibt, wird x an die Continuation selbst gebunden. Diese Continuation wird auf die Prozedur angewandt, die aus der Berechnung von (lambda (ignore) ‘‘hi’’) resultiert. Dadurch wird erneute Bindung von x an diese Prozedur und deren Anwendung auf sich selbst bewirkt. Die Prozedur ignoriert — im Rumpf — ihr Argument und liefert den Wert ‘‘hi’’. 10–25 G. Görz, FAU, Inf.8 10–27 “Hineinspringen” in Berechnungen Es gilt > (list-length ’(1 2 3 4)) 4 > (list-length ’(a . (b . c))) #f > (length ’(a . (b . c))) 2 Beispiel: Ausführung von Schleifen ohne funktionale Rekursion und ohne expliziten Kontrollmechanismus in einer imperativen Version der product-Prozedur: In den letzten Beispielen haben wir eine Form der Ausnahmebehandlung (“exception handling”) benutzt, bei der im Programm gezielt eine Ausnahmesituation “abgefangen” wurde. Escape-Prozeduren ermöglichen dem Programmierer, flexibel auf solche Situationen z.B. durch Rückgabe eines geeigneten Werts zu reagieren. Demgegenüber stellt ein normaler Fehlerausgang durch die Standardprozedur error eine weniger flexible Form der Ausnahmebehandlung dar. error ist eine Escape-Prozedur, die den aktuellen Berechnungskontext mit Ausgabe einer Fehlermeldung aufgibt. (define (product lyst) (call/cc (lambda (return) (let ((loop ’any-value) (p 1)) (call/cc ; Auswertung evaluiert (lambda (k) ; die (im let) (set! loop k))) ; folgenden Terme (if (null? lyst) (return p)) (if (zero? (car lyst)) (return 0)) G. Görz, FAU, Inf.8 G. Görz, FAU, Inf.8 10–26 10–28 Berechnungskontext retour: break (2) (set! p (* p (car lyst))) (set! lyst (cdr lyst)) (loop ’any-value))) )) Beispiel: (define flatten-list (letrec ((flatten (lambda (l) (cond ((null? l) ’()) ((number? l) (list (break l))) (else (append (flatten (car l)) (flatten (cdr l)))))))) (lambda (l) (flatten l)))) Hier: loop wirkt wie ein goto label Rückwärtssprung mit der goto-Anweisung (loop ’any-value). G. Görz, FAU, Inf.8 10–29 G. Görz, FAU, Inf.8 Berechnungskontext retour: break (1) Berechnungskontext retour: break (3) Beispiel (Forts.): Definition von escaper Bedeutung: Debugging (define get-back "any escape procedure") (define break-argument "any value") (define *escape/thunk* "any continuation") (define escaper (lambda (proc) (lambda args (*escape/thunk* (lambda () (apply proc args)))))) (define break (lambda (x) (let ((break-receiver (lambda (continuation) (set! get-back continuation) (set! break-argument x) ((escaper (lambda () x)))))) (call/cc break-receiver)))) G. Görz, FAU, Inf.8 10–31 (define receiver (lambda (cont) (set! *escape/thunk* cont) 10–30 G. Görz, FAU, Inf.8 10–32 “Continuation-Passing Style” (CPS) (*escape/thunk* (lambda () (display "escaper is defined! "))))) Einführung einer neuen Programmiertechnik aufgrund der hinter der Escape-Prozedur stehenden Vorstellung, dass eine Funktion nicht automatisch die Kontrolle an die sie rufende Funktion zurückgibt: An die Stelle der Rückgabe eines Funktionswerts tritt der Aufruf einer als zusätzlicher Parameter gegebenen Funktion, die explizit angibt, wie die Berechnung weitergehen soll. ((call/cc receiver)) ==> escaper is defined! #<unspecified> (flatten-list ’((1 2) 3)) ==> 1 (get-back 1) ==> 2 (get-back 2) ==> 3 (get-back 3) ==> (1 2 3) G. Görz, FAU, Inf.8 Die (rein syntaktische) Transformation einer Prozedur in CPS macht “versteckten” Kontrollfluss explizit; rekursive Prozeduren werden restrekursiv : • Auswertungsordnung ⇒ applikative Ordnung • Zwischenwerte ⇒ Parameter von Continuations • Kontrollfluss ⇒ Rumpf von Continuations 10–33 Zusammenfassend können wir festhalten, dass es durch die “First-Class”-Eigenschaft von Continuations möglich wird, sowohl Continuations zu binden und damit Berechnungszustände “einzufrieren” und wieder — ggf. mehrfach — zu aktivieren, als auch in Berechnungen hineinzuspringen. G. Görz, FAU, Inf.8 10–34 G. Görz, FAU, Inf.8 10–35 CPS: automatischer Optimierungsschritt in der ersten Phase einer Compilation G. Görz, FAU, Inf.8 10–36 CPS: Fakultät CPS: Fakultät (3) “Expansion” im erzeugten Prozess zu n · (n − 1) · · · 1 vs. Abarbeitung über Stack Bei Transformation einer bereits rest-rekursiven Prozedur wie (define (fact-a n p) ; Akkumulatorvariable (if (zero? n) p (fact-a (- n 1) (* n p)))) erhält man (define (fact-a-c n p k) (if (zero? n) (k p) (fact-a-c (- n 1) (* n p) k))) Letzte Zeile: Hier braucht man keine neue Continuation (lambda (v) (k v)) zu erzeugen; man kann die ursprüngliche verwenden. (Voraussetzung: Echte Implementation der Rest-Rekursion) G. Görz, FAU, Inf.8 G. Görz, FAU, Inf.8 (define (fact n) (if (zero? n) 1 (* n (fact (- n 1))))) wird transformiert in (define (fact-c n k) ; zusaetzl. Arg. k (if (zero? n) (k 1) ; sende Wert an Continuation k (fact-c ; rest-rekursiver Aufruf (- n 1) (lambda (v) ; mit neuer Continuation (k (* n v))) ))) 10–37 CPS: append CPS: Fakultät (2) An der Stelle des rekursiven Aufrufs von fact mit einer neuen impliziten Continuation, die das Resultat mit n multipliziert, bevor sie es an die urspr. Continuation sendet, wird die neue Continuation explizit übergeben. Test mit Identitätsfunktion als “äußerster” Continuation: (fact-c 10 (lambda (v) v)) ==> 3628800 Diese Transformation führt stets zu rest-rekursiven Prozeduren, hier durch Erzeugen der neuen Continuation (lambda (v) (k (* n v))) G. Görz, FAU, Inf.8 10–39 10–38 (define (append x y) (if (null? x) y (cons (car x) (append (cdr x) y)))) In CPS: Parameter k für die Fortsetzungsfunktion, an die der Wert von append weitergegeben werden soll. (define (append-cps x y k) (if (null? x) (k y) ; Weitergabe des Werts bei Rekursionsende (append-cps ; restrekursiv (cdr x) y (lambda (l) ; k fuer Rekursionsfall (k (cons (car x) l))) ))) G. Görz, FAU, Inf.8 10–40 CPS: append und Schnittstellenprozedur CPS: REVerse ⇒ Dynamische Variante der Transformation von rekursiven Prozeduren in iterative durch Einführung von Akkumulatorvariablen. Statt die Funktion k zu benutzen, kann man die Liste a übergeben, die ihren Wert darstellt; statt (k ’()) kann man (append ’() a) bzw. a schreiben Um append genauso wie in der rekursiven Version aufrufen zu können, wird eine Schnittstellenprozedur vorgesehen: ⇒ Akkumulatorversion!! (define (rev-a x a) (if (null? x) a (rev-a (cdr x) (cons (car x) a)))) (define (appendc x y) (append-cps x y (lambda (v) v))) oder auch mit call/cc: (define (appendc x y) (call/cc (lambda (k) (append-cps x y k)))) G. Görz, FAU, Inf.8 These: Ein Akkumulator ist i.a. eine Datenstruktur, die eine Continuation (bzw. ihren Wert) repräsentiert. M. Wand: Continuation-Based Program Transform. Strategies. JACM 27 (1980) 164–180 10–41 G. Görz, FAU, Inf.8 CPS: REVerse CPS: Substitutionsprozedur Ersetzt jedes Vorkommen von old im Ausdruck s durch new . (define (rev x) (if (null? x) ’() (append (rev (cdr x)) (list (car x))))) Klassisch rekursiv (define (subst old new s) (letrec ((loop (lambda (s) (if (atom? s) (if (eq? s old) new s) (cons (loop (car s)) (loop (cdr s)) ))))) (loop s))) Transformation in CPS: (define (rev-c x k) (if (null? x) (k ’()) (rev-c (cdr x) (lambda (v) (k (append v (list (car x))))) ))) G. Görz, FAU, Inf.8 10–43 Wegen der unspezifizerten Reihenfolge der Argumentauswertung ist nicht festgelegt, welcher der beiden Aufrufe von loop zuerst ausgeführt wird. 10–42 G. Görz, FAU, Inf.8 10–44 CPS: Substitutionsprozedur (2) “Nicht-lokaler Ausgang” mit CPS (2) CPS-Variante Restrekursive Version (define (subst-cps old new s k) (letrec ((loop (lambda (s k) (if (atom? s) (if (eq? s old) (k new) (k s)) (loop (car s) (lambda (v1) (loop (cdr s) (lambda (v2) (k (cons v1 v2)))))) )))) (loop s k))) 1 gefunden: unmittelbarer Ausgang, aber bis dorthin können schon viele GGTs berechnet sein (define (gcd-it* l) (if (= (car l) 1) 1 (gcd-it*-aux (car l) (cdr l)))) (define (gcd-it*-aux n l) (if (null? l) n (if (= (car l) 1) 1 (gcd-it*-aux (gcd n (car l)) (cdr l))))) loop-Aufrufe sequentialisiert: zuerst geht Rekursion über den Listenkopf. G. Görz, FAU, Inf.8 10–45 G. Görz, FAU, Inf.8 “Nicht-lokaler Ausgang” mit CPS 10–47 “Nicht-lokaler Ausgang” mit CPS (3) Ziel: Berechne die GGT einer Liste von (positiven) natürlichen Zahlen. Ist ein Element = 1, so soll die Schleife terminiert werden. CPS-Version 1 gefunden: GCD wird nie aufgerufen. Stattdessen wird nur die Möglichkeit der Berechnung etabliert und erst dann ausgeführt, wenn die ganze Liste durchmustert und keine 1 gefunden wurde. Rekursive Version 1 gefunden: Rekursion wird beendet, aber alle GGTs berechnet (define (gcd* l) (if (= (car l) 1) 1 (if (null? (cdr l)) (car l) (gcd (car l) (gcd* (cdr l)))))) (define (gcd-cps* l) (gcd-cps*-aux l (lambda (x) x))) (define (gcd-cps*-aux l f) (if (= (car l) 1) 1 (if (null? (cdr l)) (f (car l)) G. Görz, FAU, Inf.8 10–46 G. Görz, FAU, Inf.8 10–48 CPS-Transformation (gcd-cps*-aux (cdr l) (lambda (n) (f (gcd (car l) n))))))) Den “normalen” Prozeduraufruf kann man als Spezialfall des CPS ansehen, in dem die Continuation implizit aktiviert wird. Regeln für die Transformation rekursiver Prozeduren in CPS: 1. Erweitere Parameterliste um eine zusätzliche Variable k, die das (Teil-) Ergebnis aufnimmt. 2. Ersetze Wertrückgaben value durch (k value). 3. Ersetze rekursive Aufrufe durch entsprechende der CPS-Variante, die als letztes Argument eine mit k gebildete Continuation enthalten. Damit wird die Rekursion eliminiert, d.h. durch Iteration (Restrekursion) ersetzt. Die Aktivierung der Continuation ist der Kern des Prozeduraufrufs — im rekursiven Fall entspricht dieser einem einfachen return. G. Görz, FAU, Inf.8 10–49 “Nicht-lokaler Ausgang” mit call/cc G. Görz, FAU, Inf.8 10–51 Anwendung: COROUTINEN (Quasi-parallele Prozesse) (define (gcd-cc* l) (call/cc (lambda (exit) (letrec ((gcd-cc*-aux (lambda (l) (if (= (car l) 1) (exit 1) (if (null? (cdr l)) (car l) (gcd (car l) (gcd-cc*-aux (cdr l)))))))) (gcd-cc*-aux l))))) ROUTINEN (Prozeduren) R1 log. Ende R2 R3 J ] J J J J J J log. J Ende * K A A A A A A I @ @ A @ A @ A @A log. A @ Ende 1. Die aufgerufene Routine (Prozedur) beginnt stets am Anfang. Die Continuation wird nur im Fall der abnormalen Termination angewandt. 2. Die rufende Routine wird bei Rückkehr dort fortgesetzt, wo sie verlassen wurde. G. Görz, FAU, Inf.8 G. Görz, FAU, Inf.8 10–50 10–52 – Sonderfall: Wird als Ergebnis eine lokale (!) Prozedur nach außen bekannt gemacht, so bleibt die Umgebung erhalten. 3. Ruft eine gerufene Routine eine rufende auf (Rekursion), so entsteht eine zweite, von der ersten unabhängige Instanz. R1 log. Ende R2 J ] J J J J J J log. J • Auch wenn die Umgebung zu R2 aus solchem Grund erhalten bleibt, führt ein neuer Aufruf von R2 zu einer neuen Umgebung! (Beispiel: Kontoführung) R3 Ende * J ] J J J AK J A J A J A J A J A A A A A log. A Ende R1 → R2a → R3 → R2b G. Görz, FAU, Inf.8 10–53 G. Görz, FAU, Inf.8 10–55 COROUTINEN (Quasi-parallele Prozesse) • Struktur: lok. Var. • Coroutinen sind eine symmetrische Erweiterung der Routinen: Gleichberechtigung statt Unterordnung. lok. Var. R1 R2 - • Erstmaliger Aufruf führt zu einer neuen Inkarnation (Instanz). • Beim Verlassen bleibt die Umgebung erhalten; beim nächsten Ansprung wird jede Coroutine dort fortgesetzt, wo sie zuletzt verlassen wurde. Coroutinen sind also Prozeduren, deren Ausführung an einer beliebigen Stelle unterbrochen und später in dem zuletzt “festgeschriebenen” Zustand fortgesetzt werden kann. • Beim Aufruf einer Routine wird eine neue Umgebung geschaffen. • Bei Verlassen der gerufenen Prozedur wird diese Umgebung verlassen. – Normalerweise wird die Umgebung beim Verlassen gelöscht. G. Görz, FAU, Inf.8 10–54 • Sie existieren, solange ein Bezug auf sie besteht (“unlimited extent”). • Sie können sich im aktiven, passiven (suspendierten) oder terminierten Zustand befinden; aktiv ist zu jedem Zeitpunkt nur eine Coroutine! G. Görz, FAU, Inf.8 10–56 • Struktur: Prozedur zur Erzeugung von Coroutinen - B coroutine-maker hat die Aufgabe, eine “konventionelle” Prozedur in ihre “Coroutinenversion” zu überführen: ? (coroutine-maker procedure) A (RESUME coroutine2 value) ? erscheint, soll die resultierende Coroutine coroutine1 ihre Arbeit einstellen und die Kontrolle an die (mit dem Argument value) aufgerufene Coroutine coroutine2 übergeben. Wird coroutine1 ihrerseits von einer solchen RESUME-Anweisung aufgerufen, setzt sie ihre Arbeit an der Unterbrechungsstelle fort. Der Wert, der ihr übergeben wird, ist der Wert des RESUME-Terms. • A wird dort fortgesetzt, wo es anfangs verlassen wurde! G. Görz, FAU, Inf.8 10–57 G. Görz, FAU, Inf.8 - Jede Coroutine muss ihre “private” Umgebung mit einer Möglichkeit zur Aufbewahrung der Continuation und mit einer eigenen Funktion RESUME besitzen. B ? ? A coroutine-maker kreiert einen solchen Bindungskontext und erzeugt die eigentliche Coroutine. Da die Funktion, aus der die Coroutine erzeugt werden soll, in der Regel in einer anderen Umgebung generiert wurde, muss ihr der Zugriff auf RESUME via Parameterübergabe explizit ermöglicht werden. Als Konsequenz ergibt sich für solche Funktionen immer folgende Grundstruktur: C ? ? - ? 10–59 Zur Implementation von Coroutinen Beispiel (Fortsetzung) • Nächste Schritte: coroutine1 An jeder Stelle im Rumpf der Prozedur, an der ein Term der Gestalt C ? ==> ? ? (lambda (resume arg) ... (resume ...) ... ) • Anwendungen: Simulation: Erzeuger-Verbraucher-Probleme, . . . ; Spiele G. Görz, FAU, Inf.8 10–58 G. Görz, FAU, Inf.8 10–60 Syntax: Zur Implementation von Coroutinen (2) Der Konstruktor coroutine-maker bekommt als Parameter eine andere Funktion f (“eigentlicher” Algorithmus) und liefert eine einstellige Funktion, die bei späterem Aufruf im geretteten Zustand fortsetzt. Damit die erzeugte Funktion wiederaufsetzen kann, muss bei ihrer Erzeugung eine Wiederaufsetzprozedur (hier: resume) in die Umgebung eingebaut werden. RESUME braucht also zwei Argumente: 1. die zur aktivierende Coroutine 2. den Wert, der dieser Coroutine übergeben wird. 10–61 coroutine-maker Daher: Die von coroutine-maker kreierte Umgebung muss enthalten: G. Görz, FAU, Inf.8 G. Görz, FAU, Inf.8 10–63 Implementation von Coroutinen (Springer/Friedman) Jedesmal, wenn die erzeugte Coroutine aufgerufen wird, empfängt sie einen Wert, der an die saved-continuation geschickt wird. first-time resumer proc saved-continuation update-continuation! (lambda ...) Ergebnis: Coroutine (lambda (value) (cond (first-time (set! first-time #f) (proc resumer value)) (else (saved-continuation value)) )) Initialisierungen: (define first-time #t) (define saved-continuation "undefined") Die RESUME-Funktion muss “privat” (lokal) sein, weil sie den aktuellen Zustand der Coroutine (= ihre Continuation!) festhält. G. Görz, FAU, Inf.8 (define coroutine-maker (lambda (proc) <Initialisierungen> <Rumpf als Ergebnis> )) 1. Aufruf od. Fortsetzung? Wiederaufsetzprozedur eigentlicher Algorithmus vorheriger Zustand Aktualisierung d. Zustands Ergebnis 10–62 (define coroutine-maker (lambda (proc) (let ((saved-continuation "undefined")) (let ((update-continuation! (lambda (v) (set! saved-continuation v)) )) (let ((resumer (resume-maker update-continuation!)) (first-time #t)) (lambda (value) (if first-time (begin (set! first-time #f) (proc resumer value)) (saved-continuation value)))))))) G. Görz, FAU, Inf.8 10–64 (define resume-maker (lambda (update-proc!) (lambda (next-coroutine value) (let ((receiver (lambda (continuation) (update-proc! continuation) (next-coroutine value)) )) (call/cc receiver) )))) (define A (let ((A-proc (lambda (resume v) (writeln "This is A") (writeln "Came from " (resume B "A")) (writeln "Back in A") (writeln "Came from " (resume C "A")) ))) (coroutine-maker A-proc) )) (define B (let ((B-proc (lambda (resume v) (writeln (sp 14) "This is B") (writeln (sp 14) "Came from " (resume C "B")) (writeln (sp 14) "Back in B") (writeln (sp 14) "Came from " G. Görz, FAU, Inf.8 10–65 Coroutinen: Beispiel 10–67 (resume A "B")) ))) (coroutine-maker B-proc) )) Die Struktur der drei Coroutinen A, B, C sei gleich: (define C (let ((C-proc (lambda (resume v) (writeln (sp 28) "This is C") (writeln (sp 28) "Came from " (resume A "C")) (writeln (sp 28) "Back in C") (writeln (sp 28) "Came from " (resume B "C")) ))) (coroutine-maker C-proc) )) "This is X" jump to Y "Came from Y" "Back in X" jump to Z "Came from Z" Implementation: (define (sp n) (make-string n #\space)) (A ’something) (define (writeln . args) (map display args) (newline) ) G. Görz, FAU, Inf.8 G. Görz, FAU, Inf.8 Ablaufprotokoll: 10–66 G. Görz, FAU, Inf.8 10–68 This is A This is B This is C Came from C Back in A Came from A Back in C Came from C Back in B Came from B G. Görz, FAU, Inf.8 10–69