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

Documents pareils