Kontextuelle Gleichheit, Programmtransformation, Korrektheit

Werbung
Kapitel 5
Kontextuelle Gleichheit,
Programmtransformation,
Korrektheit
In diesem Kapitel soll die Gleichheit von Programmen bzw. Programmfragmenten von Haskell behandelt werden. Wir führen das auf die Gleichheit von KFPProgrammen zurück. Diese Methode baut darauf auf, dass Haskell-Programme
in KFP-Programme übersetzt werden können.
Weiterhin benötigt diese Gleichheit die operationalen Semantik in KFP, d.h.
Kontexte und die Normalordnungsreduktion in KFP. Wichtigstes Unterscheidungsmerkmal zwischen Ausdrücken bzw. theoretische Beobachtung wird die
Terminierung / Nichtterminierung sein. Es kann dann geschlossen werden, dass
Gleichungen die in KFP gelten, auch rückübersetzt werden können. Aber man
kann nicht alle Gleichheiten in Haskell damit erfassen, da es in Haskell weniger
Kontexte als in KFP gibt.
5.1
Die Kernsprache KFP
In diesem Kapitel führen wir die funktionale Kernsprache KFP ein, auf die man
die höheren Konstrukte einer funktionalen Programmiersprache zurückführen
kann, insbesondere KFPT und Haskellprogramme, Diese Kernsprache ist für
Grundlagenbetrachtungen für nicht-strikte (und strikte) funktionale Programmiersprachen geeignet, da sie eine sehr einfache Syntax und wenig Reduktionsregeln hat.
5.1.1
Syntax der funktionalen Kernsprache KFP
Die Kernsprache KFP ist angelehnt an Kernsprachen der Compiler von funktionalen Programmiersprachen und hat möglichst einfache Syntax, und möglichst
1
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
2
wenig vordefinierte Funktionen, Namen und Konstrukte. KFP hat ein schwaches
Typsystem (Programme mit Typfehlern sind möglich).
Syntax: Es gibt Konstantensymbole, die jeweils eine feste Stelligkeit haben.
Diese nennen wir Konstruktoren. Die Anzahl der Konstruktoren sei N , die Konstruktoren seien mit ci , i = 1, . . . , N bezeichnet.
Wir nehmen an, dass es eine Möglichkeit gibt, alle Konstruktoren mit Stelligkeit
anzugeben, ohne dass wir diese Methode näher spezifizieren. Zum Beispiel als
eine Auflistung aller Konstruktoren mit Stelligkeit.
Definition 5.1.1 Eine einfache kontextfreie Grammatik für KFP-Ausdrücke
(Terme, Expressions EXP ) ist:
EXP ::= V
V sind Variablen
| λV . EXP
wobei V eine Variable ist.
| (EXP EXP )
| (c EXP1 . . . EXPn )
wobei n = ar (c)
| (case EXP {P at1 → Exp1 ; . . . ; P atN +1 → ExpN +1 })
Hierbei ist P ati Pattern zum Konstruktor i, und
P atN +1 das Pattern lambda.
(P ati → Expi ) heißt auch case-Alternative.
Pat
::=
(c V1 . . . Var (c) ) | lambda
Die Variablen Vi müssen alle verschieden sein.
Wesentlich ist die andere Struktur des jetzt ungetypten case-Konstruktes: Es
gibt nur ein case, und es sind stets alle Konstruktoren als Alternativen vorhanden, ebenso eine weitere Alternative, die mit dem Pattern lambda abgedeckt
wird, und die zum Zuge kommt, wenn der zu untersuchende Ausdruck eine
Abstraktion ist.
Beispiel 5.1.2 Die Funktion, die erkennt, ob eine Liste, die mit den Konstruktoren Nil, Cons aufgebaut wurde, leer ist, kann man schreiben als:
\xs . (case xs {Nil -> True;
....
(Cons y ys) -> False;
;lambda -> bot}
Bei Konstruktoranwendungen und Pattern gibt es feste Stelligkeitsregeln, da es
sich um feste syntaktische Strukturen handelt.
Zur Kernsprache STG des Compilers von Haskell gehört auch noch ein sogenanntes letrec. Lässt man letrec zu, so gewinnt man Effizienz beim Übersetzen und kann auch eine abstrakte Maschine besser und effizienter formulieren.
Die formale Behandlung eines letrec in der Kernsprache ist sehr aufwändig
und komplex. Zudem kann man mit einer kleineren Kernsprache KFP für formale Untersuchungen der wesentlichen Aspekten der operationalen Semantik
auskommen. Wir lassen deshalb letrec in der Kernsprache weg.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
3
Der Nachteil ist, dass man die Anzahl Schritte einer Auswertung, oder die Anzahl der Schritte einer abstrakten Maschine, d.h. die Komplexität eines Programms, nicht in dieser let-freien Theorie behandeln kann, da die ReduktionsAnzahlen in KFP und Haskell zu verschieden sind.
5.1.2
Übersetzung von Haskell und KFPT nach KFP
Wir gehen kurz auf die erweiterte Kernsprache KFPT ein und zeigen, wie man
diese in KFP kodiert.
Man muss nur noch spezifizieren, wie man den typisierten case-Ausdruck nach
KFP übersetzt:
Man übersetzt ein caseT yp einfach als ein KFP-case, wobei die vorhandenen
Alternativen übernommen werden und für die anderen Konstruktoren extraPattern hinzugefügt werden: (pat → bot), ebenso am Ende (lambda → bot).
Hierbei ist bot ein nichtterminierender Ausdruck.
Diese Übersetzung ist natürlich rekursiv auch für die Unterterme durchzuführen.
Die Verwendung von bot statt eines gezielteren Fehlerausgangs ist absichtlich,
denn in KFP werden Fehler, Abbrüche und Nichtterminierung in einen Topf
geworfen und als gleich behandelt. Der Hintergrund ist, dass KFP einen Begriff
der Gleichheit von Programmen und Ausdrücken bereitstellen wird.
Übersetzung von Haskell nach KFP
Die meisten Konstrukte von Haskell kann man dadurch nach KFP übersetzen,
indem man diese zuerst nach KFPT und dann nach KFP übersetzt. Es fehlen
die Übersetzung rekursiv definierter Superkombinatoren nach KFPT und die
Übersetzung von seq und sstrict. Die rekursiven Superkombinatoren werden
wir noch nachholen.
Die Funktionen seq, strict in Haskell müssen direkt nach KFP übersetzt werden:
seq
s t = case s
{p_1 -> t; ... p_N -> t}
wobei die Pattern neue Variablen enthalten sollen. Die Übersetzung von strict
kann einfach mittels seq gemacht werden, und von seq weiß man, wie es nach
KFP übersetzt wird.
strict
= \f x -> seq x (f x)
5.1.3
Baumdarstellung von Termen
Die Darstellung von Ausdrücken als markierte geordnete Bäume (Syntaxbäume)
wird oft verwendet, wenn auch nur implizit: Als Beispiel betrachte f (f a b) c
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
4
c
f
b
a
f
Der erste Term in einer Liste von Termen ist die Funktion, der Rest sind die
Argumente:
Man kann eine kompaktere Darstellung wählen:
f
f
c
a
den
b
wir
auch
Bei
Termen
als
Bäumen
werAdressierungsmethode in Bäumen verwenden:
die
f
1
1;1
a
ε
f
c
b
2
1;2
Die Wurzel (mit Markierung f) hat Adresse ε (leeres Wort) Der Knoten mit
Markierung c hat Adresse 2. Die Blätter mit den Markierungen a, und b haben
die Adressen 1;1 und 1;2.
Die Darstellung mit expliziten Anwendungsknoten (mit @ markiert) ist folgendermaßen.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
5
@
@
f
c
@
@
b
f
a
5.1.4
Auswertungsregeln
Wir geben der Vollständigkeit halber die komplette operationale Semantik von
KFP an, auch wenn sich diese nur leicht von der operationalen Semantik von
KFPT unterscheidet.
Definition 5.1.3 Ein Wert bzw. eine WHNF (weak head normal form, schwache Kopfnormalform) in KFP ist ein Ausdruck entweder von der Form
1. (c t1 . . . tn ), wobei n = arity(c) und c ein Konstruktor ist (CWHNF), oder
2. eine Abstraktion: λx . e (FWHNF)
Dazu definieren wir zunächst, was Programmkontexte sind:
Definition 5.1.4 Ein Programmkontext (ein Kontext) ist analog zu einem
Ausdruck, der an einer Stelle ein “Loch“ hat, an dem ein Ausdruck eingesetzt
werden kann. Formal definieren wir: Ein Kontext kann sein:
C
::= [] | (e C) | (C e) | (λx . C)
| (c e1 . . . C . . . ear (c) )
| (case C {p1 → t1 ; . . . ; pn → tn })
| (case t {p1 → t1 ; . . . ; pi → C; . . . ; pn → tn })
wobei e ein Ausdruck ist.
Allerdings gilt in Kontexten die freie Umbenennbarkeit von Variablen nur für
die Unterausdrücke, die das Loch nicht enthalten.
Wenn das Loch in einem Gültigkeitsbereich einer Variablen ist, dann wird der
Kontext als syntaktisch gegebenes Programm angesehen, in dem man an der
Loch-stelle andere Ausdrücke einsetzen kann, auch solche, die freie Variablen
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
6
haben, und diese freien Variablen dann nach dem Einsetzen gebunden sind, d.h.
eingefangen wurden. D.h. es tritt der Effekt auf, dass die Namen von gebundenen Variablen eine Rolle spielen, sofern das Loch innerhalb des Rumpfs einer
Abstraktion ist. Z.B. ist der Kontext λx . [] vom Kontext λu . [] verschieden,
da beim Einsetzen von x jeweils andere Ausdrücke entstehen.
Beispiel 5.1.5 Damit sind als Kontexte z.B. definiert:
• [] : der leere Kontext. Setzt man einen Ausdruck e in den leeren Kontext
ein, so ergibt sich e selbst.
• (x (· y)). Einsetzen von t in den Kontext ergibt (x (t y))
• (case (x ·) {p1 → t1 ; . . . ; pn → tn }). Einsetzen von e in den Kontext
ergibt (case (x e) {p1 → t1 ; . . . ; pn → tn })
Beachte, dass innerhalb eines Pattern kein Loch sein kann, ebenso ist der Kontext λ · .e nicht möglich.
Wir werden den Begriff der Gleichheit und Ungleichheit von Ausdrücken noch
formaler spezifizieren.
Definition 5.1.6 Auswertungsregeln (Reduktionsregeln), wobei (Case) zwei
Regeln hat.
Beta
((λx.t) s)
t[s/x]
Case
(case (c t1 . . . tn ) {. . . ; c x1 . . . xn → s; . . .})
s[t1 /x1 , . . . , tn /xn ]
(case (λx.t) {. . . ; lambda → s})
s
Wir nennen das die unmittelbare Reduktion von Ausdrücken. Wir unterscheiden im folgenden zwischen allgemeiner Reduktion (oder kurz: Reduktion) und
unmittelbarer Reduktion, wobei die allgemeine Reduktion auch die unmittelbare
Reduktion eines Unterausdrucks umfasst.
Die Auswertungsregeln sollen in allen Programmkontexten verwendet werden
dürfen. D.h. Wenn s → t in einem Schritt reduziert, dann gilt auch C[s] → C[t]
für jeden Programmkontext C.
Die Reduktionsrelation schreiben wir als s → t, wenn die (allgemeine) Reduktion
in einem Schritt erfolgt. Die transitive bzw. reflexiv-transitive Hülle schreiben
+
∗
wir als s −→ t bzw. s −→ t.
Der Begriff des Redex (reducible expression) kann jetzt definiert werden: Wenn
in C[s] das s unmittelbar reduziert werden kann, dann ist s (zusammen mit
seiner Position) ein Redex in C[s].
Man definiert die Normalordnungs-Reduktion (normal-order-Reduktion,
Standard-Reduktion), wozu man Reduktionskontexte braucht.
7
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Definition 5.1.7 Reduktionskontexte sind:
R
::= [] | (R e) | (case R {p1 → t1 ; . . . ; pn → tn }
Jetzt ist die Normalordnungs-Reduktion diejenige, die immer in einem Reduktionskontext den Ausdruck unmittelbar reduziert:
Definition 5.1.8 Sei R[s] ein Ausdruck, so dass R ein Reduktionskontext ist,
wobei s keine WHNF ist, und s reduziert unmittelbar zu t:
Dann ist R[s] → R[t] die Ein-Schritt-Normalordnungsreduktion (Standardreduktion, normal order reduction) von R[s].
Der Unterterm s zusammen mit seiner Position wird auch Normalordnungsredex genannt. Die Reduktionsrelation wird mit dem Marker n versehen. Analog
n,+
wie oben bezeichnen wir die transitive und reflexiv-transitive Hülle mit −→ bzw.
n,∗
−→.
n,∗
Die Relation −→ bezeichnet man auch als Normalordnungsrelation oder als Auswertung eines Terms.
Wenn ein geschlossener Term t unter Normalordnung zu einer WHNF reduziert,
dann sagen wir, t terminiert und bezeichnen dies mit t⇓. Anderenfalls und falls t
geschlossen ist, divergiert t, bezeichnet mit t⇑. Wenn t offen ist, dann definieren
wir t: divergiert (t⇑), falls t eine Normalordnungsreduktion hat, die unendlich
ist, oder die Normalordnung endet mit einem Term der Form R[t0 t00 ], wobei
t0 eine Konstruktoranwendung ist und R ein Reduktionskontext. Der Fall R[x]
bleibt dabei offen.
Wir sagen, t hat eine WHNF, wenn t zu einer WHNF reduziert. Die WHNF, zu
der t unter Normalordnung reduziert, ist eindeutig, aber es gibt i.a. viele WHNFs
zu denen ein Term t reduzieren kann. Z.B. t ≡ (cons ((λx.x) True)) Nil) ist
selbst in WHNF, aber (cons True Nil) ist ebenfalls eine WHNF zu t.
Damit haben wir alle zur Auswertung von Ausdrücken notwendigen Begriffe
definiert und können jetzt Ausdrücke auswerten.
n
Beispiel 5.1.9 ((λx.x) Nil) −→ Nil
n
n
((λx.λy.x) s t) −→ ((λy.s) t) −→ s.
Lemma 5.1.10 Es gilt auch in KFP:
• Jede unmittelbare Reduktion in einem Reduktionskontext ist eine Normalordnungsreduktion.
• Der Normalordnungsredex und die Normalordnungsreduktion sind eindeutig.
• Eine WHNF hat keinen Normalordnungsredex und erlaubt keine Normalordnungsreduktion.
Beweis. Das erkennt man an der Definition der Reduktionskontexts und der
Normalordnungsreduktion.
2
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
5.2
8
Kontextuelle Gleichheit von Ausdrücken in
KFP
Gleichheit von Ausdrücken s, t kann man annehmen, wenn s und t sich gleich
verhalten (Verhaltensgleichheit). Da die Erkennung des Verhaltens ein formales Beobachten erfordert, benutzen wir das Kriterium der Terminierung bzw.
Nichtterminierung als Ergebnis der theoretischen Beobachtung. Natürlich kann
man Nichtterminierung nicht praktisch bzw. effektiv beobachten, insofern ist
das Beobachtungskriterium nicht effektiv. Die formale Definition hilft aber beim
Erkennen und Nachweisen der Korrektheit von Programmtransformationen, wie
wir sehen werden.
Man sieht (offene und geschlossene) Ausdrücke s, t als gleich an, wenn Programme ihr Terminierungsverhalten niemals ändern, wenn man s durch t ersetzt.
Dieses Kriterium erscheint zunächst schwach, aber da man alle Programme und
alle Stellen innerhalb der Programme ausprobieren kann, ergibt sich ein starkes
Kriterium.
Wenn man Beobachten im engeren Sinne auslegt, könnte man auch die Effizienz
eines Programmes als Kriterium nehmen (Anzahl der Schritte). Das wäre eine
stärkere Forderung, die wir aber hier nicht betrachten, da die kontextuelle Ordnung darauf nicht zugeschnitten ist, und sich dann fast nichts mehr nachweisen
lässt.
Man definiert zunächst eine Approximationsordnung s ≤c t, die bedeutet, dass
t besser terminiert als s, oder, dass s weniger Information liefert als t.
Definition 5.2.1
(Kontextuelle Approximation) Seien s, t KFP-Ausdrücke
und C[.] ein Kontext.
s ≤c t gdw. ∀C[] : C[s]⇓ ⇒ C[t]⇓
s ∼c t gdw s ≤c t ∧ t ≤c s
Die Ordnung ≤c nennen wir kontextuelle Approximation, und ∼c kontextuelle
Gleichheit.
Die Gleichheitsrelation kann man auch interpretieren als:
Terme sind gleich, wenn man sie mit keinem Experiment unterscheiden kann.
Das Experiment ist: Einsetzen in einen Kontext und Terminierung beobachten.
Ein großer Vorteil dieser Definition ist, dass man sich ganz auf die definierte
Sprache und die Auswertung zurückziehen kann, ohne die Hilfe einer externen
Semantik in Anspruch zu nehmen.
Ein kleiner Nachteil ist, dass diese Definition der Gleichheit von Ausdrücken
vom Sprachumfang abhängt. Die Gleichheit kann sich ändern, wenn man die
Sprache verkleinert oder erweitert.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
9
Beispiel 5.2.2 Mit
∼c
kann
man
z.B.
(S
0)
von
True
unterscheiden,
indem
man
den
Kontext
case .
{True -> True; False -> True: ... ;(S x) -> bot: ...}
verwendet.
case (S 0) {True -> True; False -> True; ... ;(S x) -> bot; ...}
terminiert nicht; aber case True {True -> True; False -> True; ...}
ist reduzierbar zu True.
Eine wichtige und (im Gegensatz zu anderen Definitionen) leicht zu zeigende
Eigenschaft ist, dass sich durch Ersetzung eines Unterausdrucks e durch einen
kontextuell gleichen Ausdruck e0 ein Programm in ein kontextuell äquivalentes
verwandelt.
Aussage 5.2.3 ≤c und ∼c sind stabil gegen Einsetzung in Kontexte und ≤c ist
eine Präordnung, während ∼c eine Äquivalenzrelation ist. Zusammengefasst:
≤c ist eine Präkongruenz und ∼c ist eine Kongruenzrelation auf Ausdrücken.
Beweis. Offensichtlich ist ≤c transitiv und ∼c ist eine Äquivalenzrelation. Wir
zeigen, dass ≤c stabil gegen Einsetzung in Kontexte ist. Sei dazu s ≤c t. Wir
zeigen dass für einen beliebigen Kontext C: C[s] ≤c C[t] gilt. Sei D ein Kontext
und sei D0 := D[C[]]. Aus s ≤c t folgt dann dass D[C[s]]⇓ ⇒ D[C[t]]⇓. Da das
für alle Kontexte D gilt, folgt die Behauptung.
2
5.3
KFP: Invarianz der Terminierung, Standardisierungssatz
Wir behandeln jetzt die Sprache KFP etwas formaler. Das Ziel dieses Kapitels
ist der Nachweis, dass Terminierung von Ausdrücken erhalten bleibt, wenn man
reduziert. Genauer, dass im Falle t → t0 die Terminierung sich nicht verändert;
d.h. t⇓ ⇔ t0 ⇓. Diese erfordert die Betrachtung beider Implikationen t⇓ ⇒ t0 ⇓
und t⇓ ⇐ t0 ⇓, bei deren Beweis jeweils verschiedene Methoden verwendet werden.
Eine verwandte, aber doch leicht andere Aussage (Standardisierungssatz), ist der
Nachweis, dass jeder Ausdruck, der sich mit irgendeiner Folge von Reduktion
mit den Kalkülregeln auf eine WHNF reduzieren lässt, auch eine terminierende
no-Reduktion hat.
Das ganze ist technisch etwas aufwendig, aber doch elementar. Da diese Aussagen die Basis für die Korrektheit von Programmtransformationen sind, lohnt es
sich auch, diese etwas genauer durchzugehen.
Wir benötigen dafür die sogenannte 1-Reduktion (siehe [Bar84]), deren Bedeutung die parallele 1-Schritt-Reduktion ist. D.h. zwei Terme s, t stehen in der
1-Relation, wenn s parallel auf t reduzierbar ist. Hierbei ist gemeint, dass man
irgendeine Untermenge der Redexe von s gleichzeitig (parallel) reduziert.
Dieser Parallelitätsbegriff muss formal definiert werden, da es nicht ganz offensichtlich ist, wie er gemeint ist. Betrachte z.B.
10
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Beispiel 5.3.1 (λx.x x)((λy . y)a). Es gibt zwei Redexe die man unabhängig
voneinander reduzieren kann. Z.B. ist dieser Term parallel reduzierbar zu: a a.
Aber: der Term (λx.x x)(λy . y) ist nur 1-reduzierbar zu (λy . y) (λy 0 . y 0 ), und
nicht zu (λy 0 . y 0 ). Diese Reduktionen sind sequentiell, denn der zweite Redex
entsteht erst durch die Reduktion.
Definition 5.3.2 Sei →1 die folgende Relation auf Ausdrücken:
• s →1 s
• Wenn s →1 s1 und t →1 t1 , dann (s t) →1 (s1 t1 ).
• Wenn s →1 s1 , dann (λ x . s) →1 (λ x . s1 ).
• Wenn si →1 s0i und t →1 t0 , dann
case t {(c x1,1 . . . x1,n1 ) → s1 ; . . . ; (c xm,1 . . . xm,nm ) → sm }
→1 case t0 {c x1,1 . . . x1,n1 → s01 ; . . . ; (c xm,1 . . . xm,nm ) → s0m }.
• Wenn s →1 s1 , t →1 t1 , dann (((λ x . s)) t) →1 s1 [t1 /x]
• Wenn si →1 s0i , t →1 t0 , dann
(case (c s1 . . . sn ){. . . ; (c x1 . . . xn ) → t; . . .}
→1 t0 [s01 /x1 , . . . , s0n /xn ].
• Wenn t →1 t0 , dann
(case (λx.s){. . . ; lambda → t; . . .}
→1 t0 .
Die 1-Relation kann aufgelöst werden in eine Folge von normalen Reduktionen:
∗
Lemma 5.3.3 Wenn s →1 t gilt, dann auch s −→ t.
Beweis. Das kann man mit Induktion nach der Struktur des Terms s zeigen.
Als Beispiel zeigen wir wie das im Fall der Beta-reduktion geht: Angenommen,
s = ((λx.s1 ) s2 ) und s1 →1 t1 , s2 →1 t2 und t = t1 [t2 /x]. Dann gilt mit
∗
∗
∗
∗
Induktion, dass s1 −→ t1 , s2 −→ t2 , also auch ((λx.s1 ) s2 ) −→ ((λx.s1 ) t2 ) −→
∗
2
((λx.t1 ) t2 ) → t1 [t2 /x], also insgesamt s −→ t.
no
Lemma 5.3.4 Sei t ein (evtl. offener) Ausdruck. Wenn t −→ s und t →1 t0 ,
dann ist entweder s →1 t0 , oder es existiert ein Term s0 , so dass s →1 s0 , und
no
t0 −→ s0 .
11
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
t
t'
1
t'
t
no
1
no
no
1
s
s
1
s'
Beweis. Dies zeigt man mit Induktion über die Struktur des Ausdrucks: Dazu
braucht man eine Fallunterscheidung:
• Sei t ≡ t1 t2 und t1 ist keine Abstraktion. Dann ändert die Normalordnung
den Ausdruck t2 nicht. Für den Ausdruck t1 ist die Tiefe des Normalordno
no
nungsredex kleiner, also gilt mit t1 −→ s1 , auch: t1 t2 −→ s1 t2 . Für
t1 t2 →1 t01 t02 gilt t1 →1 t01 und t2 →1 t02 . Mit Induktion gibt es jetzt zwei
Fälle:
– s1 →1 t01 . Dann gilt auch s1 t2 →1 t01 t02 .
no
– Es gibt ein s01 mit s1 →1 s01 und t01 −→ s01 .
no
Zusammengesetzt ergibt das: s1 t2 →1 s01 t02 und t01 t02 −→ s01 t02 .
1
t1 t2
t1' t2'
no
no
1
s1 t2
s1' t2'
• t ≡ t1 t2 und t1 ist eine Abstraktion λx . t3 . Dann ist die Normalordno
nungsreduktion gerade t1 t2 −→ t3 [t2 /x], wobei wir die Konvention über
disjunkte (gebundene) Variablennamen annehmen. Es gibt zwei Fälle für
die 1-Relation:
Wenn t1 t2 →1 t01 t02 , dann können wir auch schreiben (λx . t3 ) t2 →1
(λx . t03 ) t02 mit t3 →1 t03 , t2 →1 t02 . Damit gilt t3 [t2 /x] →1 t03 [t02 /x] und
no
(λx . t03 ) t02 −→ t03 [t02 /x].
Wenn (λx . t3 ) t2 →1 t03 [t02 /x], dann gilt t3 [t2 /x] →1 t03 [t02 /x], und wir
haben die Behauptung gezeigt.
• Wenn t ≡ case t1 {alt1 . . . altn }, und dieser Ausdruck ist kein Normalordnungsredex, dann kann man Induktion über die Struktur wie im ersten
Fall machen.
• Wenn t ≡ case t1 {alt1 ; . . . ; altn } und der Ausdruck ist ein
Normalordnungsredex,
dann
ist
die
Normalordnungsreduktion
no
case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) → tn+1 ; . . .} −→ tn+1 [t1 /x1 , . . . tn /xn ].
12
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Die 1-Reduktion kann sein: case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) →
tn+1 ; . . .} →1 case (c t01 . . . t0n ) {. . . ; (c x1 . . . xn ) → t0n+1 ; . . .}. In diesem
Fall ist die Vertauschbarkeit analog zu den anderen Fällen.
Wenn die 1-Reduktion direkt den case-Ausdruck reduziert:
case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) → tn+1 ; . . .} →1 t0n+1 [t01 /x1 , . . . , t0n /xn ],
dann ist auch: tn+1 [t1 /x1 , . . . , tn /xn ], →1 t0n+1 [t01 /x1 , . . . , t0n /xn ].
Analog sind die Fälle, in denen der Ausdruck von der Form
(case (λx.t) {. . .}) ist.
2
no,k
Aussage 5.3.5 Sei t ein (evtl. offener) Ausdruck. Wenn t −→ t2 und t2 ist
eine WHNF, und t →1 t0 . Dann terminiert die no-Reduktion von t0 und es gilt
no,k0
t0 −→ t02 , t2 →1 t02 , wobei k ≥ k 0 und t02 eine WHNF ist.
Beweis. Induktion nach k, der Anzahl der Normalordnungsreduktionen. Im Basisfall muss man nur noch beachten, dass aus t →1 t0 und t ist WHNF folgt,
dass t0 ebenfalls eine WHNF ist.
no,k−1
no
Im Falle k > 0 zerlegen wir die no-Reduktion in t −→ t1 −→ t2 . Nach Lemma
5.3.4 gibt es zwei Fälle:
• t1 →1 t0 . Induktion zeigt die Existenz einer WHNF t02 mit t2 →1 t02 und
no,k0
t0 −→ t02 und k − 1 ≥ k 0 . Dann gilt auch k ≥ k 0 und die Behauptung des
Lemmas ist gezeigt.
no
• Es gibt ein t01 mit t0 −→ t01 und t1 →1 t01 . Induktion nach k zeigt dann die
no,k0 −1
Existenz einer WHNF t02 mit t01 −→
t02 und t2 →1 t02 und k − 1 ≥ k 0 − 1.
no,k0
Zusammensetzen der Diagramme ergibt t0 −→ t02 und t2 →1 t02 und k ≥ k 0 .
t
1
no
1
t'
no,k'
t1
no,k-1 1
t2
t
1
no
t2'
t1
t'
no
1
no,k-1
t1'
no,k'-1
t2
1
t2'
2
Da jede 1-Schritt-Reduktion auch als 1-Relation darstellbar ist, folgt unmittelbar:
13
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
no,∗
Satz 5.3.6 Sei t ein (evtl. offener) Ausdruck. Wenn t⇓, d.h. t −→ t1 und t1 ist
eine WHNF, und t → t0 mit einer anderen Reduktion. Dann gilt t0 ⇓.
Jetzt fehlt noch die Richtung: (t → t0 ∧ t0 ⇓) ⇒ t⇓. Die Idee hier ist, eine
Reduktion zur WHNF, die aus Normalordnungsreduktionen und 1-Reduktionen
besteht, in eine Normalordnungsreduktion zu verwandeln.
Definition 5.3.7 Sei t ein (evtl. offener) Term. Die Reduktion t →1 s ist intern, d.h. ohne anteilige Normalordnungsreduktion, wenn sie nicht von einer der
Formen ist:
• R[((λx . t1 ) t2 )] →1 R0 [t01 [t02 /x]] wobei R ein Reduktionskontext ist, t1 →1
t01 , t2 →1 t02 , R →1 R0 , wobei man →1 für Kontexte wie für einen Term
mit einer extra Konstanten definiert.
• t = R[(case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) → tn+1 ; . . .})] →1
R0 [t0n+1 [t01 /x1 , . . . , t0n /xn ]] wobei R ein Reduktionskontext ist und ti →1
t0i , R →1 R0 .
• t = R[(case (λx.r) {. . . ; (lambda → t)}] →1 R0 [t0 ] wobei R ein Reduktionskontext ist und t →1 t0 , R →1 R0 .
2
Man kann die Anzahl der notwendigen Einzelreduktionen in der parallelen Reduktion r1 →1 r2 durch Markieren der Unterterme des Terms r1 mit dieser
Anzahl festhalten. Diese Anzahl entspricht der Anzahl Reduktionen, wenn man
diese “von oben“ her abarbeitet. Diese Anzahl kann man berechnen anhand der
syntaktischen Form von r1 und der Änderungen, die die 1-Reduktion bewirkt.
Sei ϕ diese Markierung, geschrieben als Abbildung von Untertermen auf die
Anzahl. Eigentlich ist das eine Abbildung der ganzen Reduktion.
• Wenn (s t) →1 s0 t0 , dann markiere (s t) mit ϕ(s) + ϕ(t)
• Wenn (λx . s) →1 (λx . s0 ), dann markiere (λx . s) mit ϕ(s).
• Wenn (case s {p1 → t1 ; . . . ; pn → tn }) →1 (case s0 {p1 → t01 ; . . . ; pn →
t0n }), dann markiere mit ϕ(s) + Σ ϕ(ti ).
• Wenn (λx . s) t →1 s0 [t0 /x], dann markiere mit 1 + ϕ(s) + ϕ(t) ∗ (#(x, s0 ))
wobei #(x, s) die Anzahl der freien Vorkommen von x in s ist.
• Wenn (case (c s1 . . . sn ) {. . . ; (c x1 . . . xn )
→
t; . . .})
→1
t[s1 /x1 , . . . , sn /xn ], dann markiere mit 1 + ϕ(t) + (Σ (ϕ(s0i ) ∗ (#(xi , t)))).
Lemma 5.3.8 Sei t ein (evtl. offener) Ausdruck. Wenn t →1 s, dann kann
no,∗
man die Reduktion zerlegen in: t −→ u →1 s, wobei die Reduktion u →1 s
intern ist.
14
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
no
Beweis. Dass man eine nicht-interne Reduktion t →1 s in t −→ u1 →1 s zerlegen
kann, folgt aus der Definition der Reduktion und aus der Definition von intern.
Dies kann man iterieren, bis die Reduktion u1 →1 s intern ist. Allerdings ist
nicht ganz offensichtlich, ob es terminiert. Dazu muss man die Anstrengung etwas erhöhen: Die Idee ist, dass die Anzahl ϕ der notwendigen Einzelreduktionen
vermindert wird: Es gibt zwei Fälle (Beta und Case):
no
• Wenn die erste Zerlegung so aussieht: R[(λx . s) t] −→ R[s[t/x]] →1
R0 [s0 [t0 /x]], dann ist die Zahl der Reduktionen (ϕ) in der Reduktion
R[s[t/x]] →1 R0 [s0 [t0 /x]] um mindestens 1 vermindert.
• Wenn die Zerlegung so aussieht: R[(case (c s1 . . . sn ) {. . . ; (c x1 . . . xn ) →
no
t; . . .})] −→ R[t[s1 /x1 , . . . , sn /xn ]] →1 R0 [t0 [s01 /x1 , . . . , s0n /xn ]], dann folgt
ebenfalls aus der obigen Definition, dass die Anzahl der Reduktionen mindestens um 1 vermindert ist. Die Verminderung kann größer sein, denn
eine case-Reduktion kann ganze Unterterme löschen.
Insgesamt heißt das, dass das Abspalten von no-Reduktionen terminiert.
2
no
Lemma 5.3.9 Sei t ein (evtl. offener) Ausdruck. Wenn t →1 u −→ s, dann
no,+
kann man die Reduktionen vertauschen: ∃u0 : t −→ u0 →1 s, so dass die Reduktion u0 →1 s intern ist.
no,∗
no
Beweis. Die Reduktionsfolge t →1 u −→ s kann man zerlegen in: t −→ t0 →1
no
u −→ s, so dass t0 →1 u intern ist. Beachte, dass die 1-Relation auch trivial sein
no
kann. Es gibt nur die folgenden zwei Fälle für die Reduktion t0 →1 u −→ s:
no
1. Der erste Fall ist, dass R[(λx . t1 ) t2 ] →1 R0 [(λx . t01 ) t02 ] −→ R0 [t01 [t02 /x]],
wobei R Reduktionskontext, t1 →1 t01 , t2 →1 t02 , R →1 R0 , wobei man →1
für Kontexte wie für einen Term mit einer extra Konstanten definiert (eigentlich braucht man dafür Induktion nach der Termstruktur). In diesem
no
Fall gilt: R[(λx . t1 ) t2 ] −→ R[t1 [t2 /x]] →1 R0 [t01 [t02 /x]].
2. Der zweite Fall ist t = R[(case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) →
no
tn+1 ; . . .})] →1 R0 [(case (c t01 . . . t0n ) {. . . ; (c x1 . . . xn ) → t0n+1 ; . . .})] −→
R0 [t0n+1 [t01 /x1 , . . . , t0n /xn ]] wobei R Reduktionskontext, ti →1 t0i , R →1 R0 .
no
In diesem Fall gilt wie im ersten t −→ R[tn+1 [t1 /x1 , . . . , tn /xn ]] →1
R0 [t0n+1 [t01 /x1 , . . . , t0n /xn ]].
no,∗
D.h. in diesem Fall ist die Reduktion vertauschbar, und wir erhalten: t −→
no
t1 −→ t2 →1 s. Wenn die letzte Reduktion nicht intern ist, dann kann man mit
Lemma 5.3.8 den Normalordnungsanteil abspalten.
2
15
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
no
Lemma 5.3.10 Sei s ein (evtl. offener) Ausdruck. Eine Folge s →1 s2 −→
no
. . . −→ sn wobei sn eine WHNF ist, kann man umändern in eine Reduktion, die
nur Normalordnungs-Reduktionen verwendet, um zu einer WHNF zu kommen.
no,∗
Genauer: in eine Reduktion s −→ s0n−1 →1 sn , bei der s0n−1 eine WHNF ist.
Beweis. Das Lemma 5.3.9 kann
man no
zum Verschieben der 1-Reduktion nach
no
rechts verwenden. s →1 s2 −→ . . . −→ sn wird damit im ersten Schritt zu
no,∗
no
s −→ s02 →1 s3 . . . −→ sn . Schiebt man die 1-Reduktion weiter nach rechts, so
no,∗
no,∗
erhält man schließlich: s −→ s02 −→ s3 . . . s0n−1 →1 sn . Die letzte Reduktion ist
intern, also ist s0n−1 eine WHNF.
2
∗
Satz 5.3.11 (Standardisierung). Sei t ein (evtl. offener) Ausdruck. Wenn t −→
t1 mit beliebigen (Beta) und (case)-Reduktionen, wobei t1 eine WHNF ist, dann
no,∗
∗
existiert eine WHNF tN F , so dass t −→ tN F , und tN F −→ t1 .
Beweis. Jede Reduktion ist auch eine Folge von 1-Reduktionen. Danach kann
man Lemma 5.3.10 mehrfach anwenden.
2
Satz 5.3.12 (Invarianz der Terminierung) Seien t, t0 (evtl. offene) Ausdrücke
mit t → t0 . Dann gilt t⇓ ⇔ t0 ⇓
Beweis. Das folgt aus Satz 5.3.11 (Standardisierung) und Satz 5.3.6.
2
Der nächste Satz ist der erste Satz über Korrektheit von Programmtransformationen. Er sagt aus, dass man die Reduktionsregeln Beta und Case an beliebiger
Stelle (vorwärts und rückwärts) in einem Programm verwenden kann, um das
Programm (zur Compilezeit) zur verändern, bzw. zu optimieren, ohne dass man
einen Fehler macht. Dies nennt man auch partielle Auswertung.
Satz 5.3.13 Seien t, t0 Terme, so dass t → t0 mit einer KFP-Reduktion. Dann
gilt t ∼c t0 .
Beweis. Sei C irgendein Kontext. Dann gilt auch C[t] → C[t0 ], da Reduktionen
überall erlaubt sind. Wenn C[t]⇓, dann auch C[t0 ]⇓ nach Satz 5.3.12. Da das für
alle Kontexte gilt. schließen wir t ≤c t0 .
Wenn C[t0 ]⇓, dann auch C[t]⇓, ebenfalls wegen Satz 5.3.12. Deshalb gilt t0 ≤c t.
Zusammen ergibt sich t0 ∼c t.
2
Damit haben wir schon eine große Klasse von kontextuellen Gleichheiten im
Griff: alle die sich mittels Reduktion ineinander überführen lassen. Allerdings
sind das nicht alle interessanten Gleichungen, wie wir noch sehen werden.
16
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Definition 5.3.14 Die durch Reduktion erzeugt Gleichheit ist definiert als:
∗
s ←→ t gdw. es eine Folge von Reduktionen (→ und ←) zwischen s und t gibt
Diese Relation sieht Terme, die sich nur um Umbenennung von gebundenen
Variablen unterscheiden, als gleich an.
∗
Es gilt, dass ←→ eine Kongruenz in KFP ist.
∗
Satz 5.3.15 Für alle s, t gilt: s ←→ t ⇒ s ∼c t
In KFP ist die Umkehrung falsch: Es gibt Ausdrücke s, t mit s ∼c t, aber
∗
s ←→
6
t.
Beispiel 5.3.16 Wir geben Kombinatoren an:
Y
= \f-> (\x->
rep1 y = 1 : rep1 a
rep2 y = 1 : rep2 b
rp1
rp2
= Y (\r1
= Y (\r2
f (x x))
(\x->
f (x x))
y -> 1 : r1 a)
y -> 1 : r2 b)
Die Ausdrücke rp1 und rp2 sind KFP-Ausdrücke, denn die Rekursion ist direkt
∗
kodiert. Mit Induktion kann man zeigen, dass alle Ausdrücke t1 mit t1 ←→ rp1
ein a enthalten, aber kein b enthalten, während das für alle Ausdrücke t2 mit
∗
∗
t2 ←→ rp2 genau umgekehrt ist. Damit gilt nicht: rp1 longlef trightarrow rp2.
Beide Ausdrücke erzeugen aber die unendliche Liste 1 : 1 : . . .. Wir werden
später sehen, dass diese Ausdrücke dann kontextuell äquivalent sein müssen.
5.3.1
Exkurs: verallgemeinerte Induktion
Eine irreflexive partielle Ordnung < auf einer Menge M ist fundiert (wellfounded), gdw. jede absteigende Kette endlich ist.
Wenn man eine fundierte Ordnung < auf einer Menge M mit Menge der kleinsten Elemente Mmin hat, dann kann man folgendes Induktionsschema verwenden, um eine Aussage P (.) für alle Elemente der Menge M zu zeigen:
((∀m ∈ Mmin : P (m))
∧ (∀x : (∀y.y < x ⇒ P (y)) ⇒ P (x)))
⇒ ∀x.P (x)
Basis
Induktionsschluss
Es gibt Standardmethoden, um aus (linearen) fundierten Ordnungen weitere
(lineare)fundierte Ordnungen zu erzeugen.
Eine fundierte Ordnung, die wir verwenden wollen, ist die lexikographische Ordnung auf Tupeln, deren Komponenten eine fundierte Ordnung haben. Es gilt:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
17
Wenn die Ordnung < auf M fundiert ist, dann auch die lexikographische Ordnung auf den Tupeln M n , wobei (a1 , . . . , an ) < (b1 , . . . , bn )
gdw. für einen Index i : a1 = b1 , . . . , ai−1 = bi−1 , ai < bi
Die Ordnung < auf den natürlichen Zahlen ist fundiert und linear. Also ist auch
die lexikographische Ordnung auf m-Tupeln über natürlichen Zahlen für festes
m eine lineare und fundierte Ordnung. Eine nicht-lineare fundierte Ordnung ist
die echte-Teilmengen“ -Beziehung auf der Menge der endlichen Teilmengen von
”
IN.
5.4
Kontextuelle Gleichheit von Ausdrücken in
KFP
Wir betrachten und zeigen jetzt Eigenschaften der kontextuellen Approximation
und der kontextuellen Gleichheit. Nochmal zur Erinnerung die Definition:
Seien s, t KFP-Ausdrücke und C[.] ein Kontext.
s ≤c t gdw. ∀C[] : C[s]⇓ ⇒ C[t]⇓
s ∼c t gdw s ≤c t ∧ t ≤c s
Wir erinnern uns auch, dass ≤c eine Präkongruenz und ∼c eine Kongruenzrelation auf Ausdrücken ist.
Ein erstes Lemma ist:
Lemma 5.4.1
• Für verschiedene Variablen x, y gilt x 6≤c y und y 6≤c x
• Für verschiedene Konstanten c, d gilt c 6≤c d und d 6≤c c.
Beweis. Für die Variablen betrachte den Kontext C = (λy . (λx . [])) bot Nil.
Dann reduziert C[x] zu Nil, während C[y] nicht terminiert.
Für
die
Konstanten
betrachte
die
Kontexte
C
=case . {c -> bot;d -> Nil; ...}
und
C 0 =case . {d -> bot;c -> Nil; ...}.
2
Man kann auch fordern, dass die Kontexte die Terme s, t schließen:
Aussage 5.4.2 In KFP gilt: s ≤c t gdw. ∀C : wenn C[s], C[t] geschlossen, dann
C[s]⇓ ⇒ C[t]⇓.
Beweis. Die eine Richtung ist klar.
Es gelte ∀C : wenn C[s], C[t] geschlossen, dann C[s]⇓ ⇒ C[t]⇓. Sei C irgendein
Kontext, so dass C[s]⇓, aber nicht C[t]⇓. Dann erweitern wir den Kontext C
so dass alle freien Variablen in C[s], C[t] eingefangen werden, indem wir für
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
18
die freien Variablen x1 , . . . xn den Kontext D[] := (λx1 , . . . , xn .C[])bot . . . bot
nehmen.
Dann gilt weiterhin D[s]⇓, denn eine WHNF ist unbeeinflusst von den Einsetzungen in die freien Variablen. Es gilt auch noch D[t]⇑, denn entweder ist die
Normalordnungsreduktion nicht terminierend, oder ergibt einen Typfehler, was
nicht durch die Einsetzung beeinflusst ist, oder bei der Reduktion von C[t] gerät
eine freie Variable in einen Reduktionskontext. Diese ist aber durch bot ersetzt,
somit gilt D[t]⇑, ein Widerspruch.
2
Wir können für Ausdrücke, die kontextuell gleich sind, mehr über die Struktur
der zugehörigen WHNF sagen.
Aussage 5.4.3 Seien s, t zwei Terme mit s ≤c t. Dann gilt einer der drei Fälle:
1. s⇑.
∗
∗
2. s −→ (c s1 . . . sn ) und t −→ (c t1 . . . tn ) für einen Konstruktor c und
si ≤c ti für alle i.
∗
∗
3. s −→ λx.s0 und t −→ λx.t0 und für alle r: ((λx.s0 ) r) ≤c ((λx.t0 ) r).
Beweis. Wir nehmen an, dass s⇓, so dass wir nur zeigen müssen, dass dann der
zweite oder dritte Fall eintritt.
Mit Kontexten der Form (case [] {alts}) ist sofort zu zeigen, dass aus s ≤c t
folgt, dass die WHNF für t den gleichen Topkonstruktor wie s haben muss, bzw.
sowohl s als auch t Abstraktionen sind.
∗
∗
Angenommen, s −→ (c s1 . . . sn ). Dann gilt t −→ (c t1 . . . tn ). Um
für ein i die Beziehung si ≤c t zu zeigen, nehme den Kontext Ci =
case [] {... ; (c x_1 ... x_i ... x_k) -> x_i; ... } . Da aus s ≤c
t auch C[s] ≤c C[t] folgt, und man auch reduzieren darf wegen Satz 5.3.15, folgt
sofort: si ≤c ti .
∗
∗
Angenommen, s −→ λx.s0 . Dann gilt t −→ λx.t0 . Um ((λx.s0 ) r) ≤c ((λx.t0 ) r)
für alle r zu zeigen, nehme den Kontext Cr := ([] r).
2
Aussage 5.4.4 Seien s, t zwei Terme mit s ∼c t.
Dann gilt einer der drei Fälle:
1. s⇑ und t⇑.
∗
∗
2. s −→ (c s1 . . . sn ) und t −→ (c t1 . . . tn ) für einen Konstruktor c und
si ∼c ti für alle i.
∗
∗
3. s −→ λx.s0 und t −→ λx.t0 und für alle r: ((λx.s0 ) r) ∼c ((λx.t0 ) r).
Definition 5.4.5 Ein Konstruktorterm ist ein Ausdruck, der nur aus Konstruktoren besteht.
19
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Beachte, dass damit endliche Konstruktorterme“
”
drücke sind immer endlich.
gemeint sind, denn Aus-
Satz 5.4.6 Erhält man für einen Ausdruck mittels zweier verschiedener Auswertungen jeweils Konstruktorterme, so sind diese gleich.
∗
∗
Formal: s −→ s1 , s −→ s2 wobei s1 , s2 Konstruktorterme sind, dann gilt s1 = s2 .
Beweis. Folgt aus der Aussage 5.4.4 mit Induktion über die Tiefe des Konstruktorausdrucks.
2
Jetzt können wir auch noch zeigen, dass man für Ausdrücke mit freien Variablen
eingeschränktere Kriterien für die kontextuelle Äquivalenz hat:
Lemma 5.4.7 Seien s, t KFP-Ausdrücke mit den freien Variablen x1 , . . . , xn
und sei ρ eine Umbenennung der Variablen xi . D.h. eine Injektion mit ρ(x) = yi .
Dann gilt s ≤c t gdw. sρ ≤c tρ.
Aussage 5.4.8 Seien s, t KFP-Ausdrücke mit den freien Variablen x1 , . . . , xn .
Dann gilt s ≤c t gdw. λx1 , . . . , xn .s ≤c λx1 , . . . , xn .t.
Beweis. Die eine Richtung ist klar.
Sei λx1 , . . . , xn s ≤c λx1 , . . . , xn t, und sei C ein Kontext, so dass C[s], C[t] geschlossen sind, und sei C[s]⇓.
Bilde den Kontext C 0 := C[(λx1 , . . . , xn .[])x1 . . . xn ). Dann reduziert C 0 [s] nach
einigen Beta-Reduktionsschritten zu C[s], also gilt C 0 [s]⇓ nach Satz 5.3.15. Da
C 0 [s] = C[(λx1 , . . . , xn .s)x1 . . . xn ], erhält man wegen der Voraussetzung auch
C[(λx1 , . . . , xn .t)x1 . . . xn ]⇓. Da dies zu C[t] reduziert, erhält man C[t]⇓, und
damit gilt die Behauptung.
2
5.4.1
Simulation und Bisimulation
Zum Erkennen der kontextuellen Gleichheit von zwei Ausdrücken ist es hilfreich,
wenn man diese nicht in allen (unendlich) vielen Kontexten ausprobieren muss,
sondern wenn es ausreicht, die Terme selbst (d.h. im leeren Kontext) zu reduzieren, und dies evtl. zu iterieren. Dies leistet die sogenannte Simulation bzw.
Bisimulation.
Definition 5.4.9 Seien s, t zwei Ausdrücke. Wir definieren ≤b als den größten
Fixpunkt des folgenden Operators [.] auf Relationen R:
s [R] t wenn folgendes gilt:
Wenn s⇓(c s1 . . . sn ), dann t⇓(c t1 . . . tn ) und si R ti für alle i.
und
wenn
s⇓(λx.s0 ),
dann
t⇓(λx.t0 )
und
für
alle
r:
0
0
((λx.s ) r) R ((λx.t ) r)
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
20
Man definiert s ∼b t, gdw s ≤b t und t ≤b s.
Die Relation ≤b nennt man auch Simulation und ∼b Bisimulation.
Die Relation s ∼b t gilt, wenn man s, t mittels WHNF-Reduktion und dann
entweder durch Anwenden auf gleiche Argumente, oder Betrachten von Untertermen nicht unterscheiden kann. Diese Betrachtungsweise wird durch die
Definition mittels größtem Fixpunkt erreicht.
Da man die Relationen als Mengen von Paaren ansehen kann, hat man einen
vollständigen Verband von Relationen, und man kann den Fixpunktsatz von
Knaster-Tarski verwenden. Wir definieren:
a ist ein Fixpunkt einer Funktion f , gdw. f (a) = a.
a ist ein Post-Fixpunkt einer Funktion f , gdw. a ≤ f (a).
Wir formulieren den Satz auf Mengen mit Vereinigung und Untermenge:
Satz 5.4.10 Sei (L, ⊆) ein (vollständiger) Mengen-Verband und sei f : L → L
eine monotone Funktion. Dann ist
[
m = {R ∈ L | R ⊆ f (R)}
ein größter Fixpunkt, und auch ein größter Post-Fixpunkt,
Da [·] monoton auf Relationen ist, kann man diesen Satz auf Relationen und die
Funktion [·] anwenden.
Den größten Fixpunkt von [·] kann man auch definieren über eine Folge von
Relationen ≤b,i mit folgender Eigenschaft:
s ≤b,0 t für alle s, t.
s ≤b,i+1 t wenn folgendes gilt:
Wenn s⇓(c s1 . . . sn ), dann t⇓(c t1 . . . tn ) und sj ≤b,i tj für alle
j = 1, . . . , n;
und
wenn
s⇓(λx.s0 ),
dann
t⇓(λx.t0 )
und
für
alle
r:
0
0
((λx.s ) r) ≤b,i ((λx.t ) r)
Die Relation s ≤b t gilt gdw. ∀i : s ≤b,i t.
Es gilt folgende Aussage zum Zusammenhang zwischen ≤b und ≤c :
Satz 5.4.11 (siehe Howe-1989, 1996). Es gelten die Aussagen:
≤b = ≤c
∼b = ∼c
Es folgt sofort, dass ⊥ ≤c t ist für alle t, denn ∀t : ⊥ ≤b t ist offensichtlich.
Leider können wir das Theorem 5.4.11 nicht im Rahmen des Skriptes zeigen,
da man dafür spezifische Methoden verwenden muss. Was man allerdings mit
unseren Mitteln zeigen kann, ist eine Richtung:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Aussage 5.4.12 Es gilt ≤c
⊆
21
≤b
Beweis. Es genügt zu zeigen, dass ≤c ein PostFixpunkt des Operators [.] ist,
d.h. dass ≤c ⊆ [≤c ], denn nach dem Satz von Knaster-Tarski ist ≤b ein größter
PostFixpunkt von [·]. Das ist aber gerade der Inhalt von Aussage 5.4.3, denn
aus s ≤c t folgt einer der drei Fälle in 5.4.3, und daraus folgt, dass s [≤c ] t. 2
Durch die Definition von ≤b als größter Fixpunkt (und größter Postfix-Punkt)
und durch die Gleichheit von ≤b und ≤c gilt folgendes Induktionsprinzip:
Lemma 5.4.13 Wenn für eine Relation R gilt: R ⊆ [R], dann gilt R ⊆ ≤c .
Lemma 5.4.14 Wenn für zwei Terme s, t und alle i gilt: s ≤b,i t ⇒ s ≤b,i+1 t,
dann gilt s ≤c t.
Beweis. Gilt, da ≤c und ≤b übereinstimmen, da s ≤b,0 t stets gilt, und s ≤b
t ⇔ ∀i : s ≤b,i t.
2
Diese Aussage hilft, die Gleichheit von Ausdrücken zu prüfen:
Beispiel 5.4.15 s = λx, y.if x then (Cons x y) else (Cons True y)
t = λx, y.if x then (Cons True y) else (Cons True y)
Zum Feststellen der Gleichheit muss man alle Ausdrücke r1 , r2 prüfen, auf die
man s, t anwenden kann und die Gleichheit von s r1 r2 und t r1 r2 prüfen.
Für r1 = True bzw. r1 = False ergibt sich der gleiche Term. Aber für r1 = bot
ergibt sich ebenfalls das gleiche: s bot r2 = bot und t bot r2 = bot.
Beachte aber, dass die Terme nicht kontextuell äquivalent zu λx, y.(Cons True y)
sind.
Im folgenden Beispiel gehen wir davon aus, dass repeat und map in KFP geeignet definiert sind.
Wenn s = (repeat 1) und t = map (λx.1) (repeat 2), gilt dann s ∼c t?
Hierzu kann man s und t reduzieren und erhält jeweils eine WHNF:
1 : (repeat 1) und 1 : map (λx.1) (repeat 2). Die rekursive Frage ist dann wieder die gleiche, nämlich ob s ∼c t. Hier kann man das Lemma 5.4.14 anwenden.
Da das in beiden Richtungen anwendbar ist, folgt auch t ≤c s, und damit s ∼c t
Diese Methode werden wir noch erweitern in einem der folgenden Kapitel.
5.4.2
Das Kontextlemma
Ein wichtiges Lemma zur kontextuellen Approximation ist die Einschränkung
des Kriteriums “für alle Kontexte“ auf Reduktionskontexte. Das kann in anderen Kalkülen als gleichwertiger oder überlegener Weg zu den Aussagen wie
Korrektheit von Programmtransformationen und Standardisierungslemma verwendet werden. Wir erwähnen das Kontextlemma wegen seiner herausragenden
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
22
Rolle in anderen Reduktionskalkülen; es lässt sich auch in KFP unabhängig
nachweisen, allerdings scheint es nicht der richtige Weg zu sein, die Korrektheit der KFP-Kalkülregeln als Programmtransformationen nachzuweisen. Der
Grund könnte darin liegen, dass KFP-Reduktionskontexte offene Terme nicht
abschließen können, was in anderen Kalkülen (mit let) möglich ist.
Für das Kontextlemma benötigen wir den technischen Begriff des Multikontexts.
Das ist eine Verallgemeinerung des Begriffs Kontext auf mehrere Löcher. Dazu
kann man z.B. neue Konstanten nehmen, bezeichnet als ·1 , . . . , ·n und Terme
bauen, die diese Konstanten enthalten. Wir nehmen zusätzlich an, dass diese
Konstanten genau einmal vorkommen, wenn sie erwähnt sind, d.h. C[·1 , . . . , ·n ]
ist ein Ausdruck, der jede Konstante ·i genau einmal enthält.
Lemma 5.4.16
(Kontext-Lemma) Seien s, t geschlossene Ausdrücke. Wenn
für alle (geschlossenen) Reduktionskontexte R: R[s]⇓ ⇒ R[t]⇓ gilt, dann gilt
auch s ≤c t.
Beweis. Der hier gegebene Beweis ist unabhängig von den Beweisen mittels der
1-Relation. Beachte im folgenden, dass R[s], R[t] geschlossene Terme sind.
Wir beweisen die allgemeinere Behauptung:
Wenn für die geschlossenen Terme si , ti : ∀i, R : R[si ]⇓ ⇒
R[ti ]⇓ erfüllen, dann gilt für alle Multikontexte C[·1 , . . . , ·m ]:
Wenn C[s1 , . . . , sn ], C[t1 , . . . , tn ] geschlossen sind, dann gilt
C[s1 , . . . , sn ]⇓ ⇒ C[t1 , . . . , tn ]⇓.
Wie nehmen an, dies sei falsch, Dann gibt es ein Gegenbeispiel, d.h. geschlossene Terme si , ti mit ∀i, R : R[si ]⇓ ⇒ R[ti ]⇓, aber es gibt einen Multikontext
C[·1 , . . . , ·n ], so dass C[s1 , . . . , sn ]⇓, aber C[t1 , . . . , tn ]⇑. Wir wählen den Multikontext so, dass er minimal ist bzgl. der lexikographischen Ordnung mit den
Komponenten
1. Die Anzahl der Normalordnungsreduktionen von C[s1 , . . . , sn ],
2. Die Anzahl der Löcher von C.
Wenn ein Loch von C[·1 , . . . , ·n ] in einem Reduktionskontext ist, d.h., einer der
Kontexte C[·, t2 , . . . , tn ], C[t1 , ·, t3 , . . . , tn ], . . . , C[t1 , . . . , tn−1 , ·] ist ein Reduktionskontext, dann nehmen wir der Einfachheit halber an, das erste Loch sei
in einem Reduktionskontext. Dann ist C[·, t2 , . . . , tn ] ein Reduktionskontext.
Sei C 0 := C[s1 , ·2 , . . . , ·n ]. Da C 0 [s2 , . . . , sn ] ≡ C[s1 , . . . , sn ], haben beide dieselbe Normalordnungsreduktion. Die Anzahl der Löcher von C 0 ist kleiner als
die von C, also gilt C 0 [t2 , . . . , tn ]⇓. Das bedeutet, dass C[s1 , t2 , . . . , tn ]⇓. Da
C[·, t2 , . . . , tn ] ein Reduktionskontext ist, folgt aus den Bedingungen des Lemmas : C[t1 , t2 , . . . , tn ]⇓, was ein Widerspruch ist.
Wenn keiner der Kontexte C[·, t2 , . . . , tn ], C[t1 , ·, t3 , . . . , tn ], . . . ,
C[t1 , . . . , tn−1 , ·] ein Reduktionskontext ist, dann ist entweder C[s1 , . . . , sn ]
eine WHNF, oder die zwei Terme C[s1 , . . . , sn ] und C[t1 , . . . , tn ] werden mit
derselben Normalordnungsreduktion reduziert. Das liegt daran, dass das Finden
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
23
des Normalordnungsredex von oben nach unten geschieht und in diesem Falle
unabhängig von den Ausdrücken in den Löchern ist.
Falls C[s1 , . . . , sn ] eine WHNF ist, dann terminiert die Suche nach einem Normalordnungsredex innerhalb des Kontexts C, somit ist auch C[t1 , . . . , tn ] eine
WHNF.
Wir betrachten den Fall dass C[s1 , . . . , sn ] keine WHNF ist, aber C[s1 , . . . , sn ]⇓.
Die Normalordnungsreduktion produziert aus C[s1 , . . . , sn ] einen Ausdruck
C 0 [u1 , . . . , um ] wobei ui = sφ(i) für eine Abbildung φ : [1..m] → [1..n].
Es ist möglich dass m > n. Die Anzahl der Normalordnungsschritte von
C 0 [u1 , . . . , um ] ist um 1 kleiner als die von C[s1 , . . . , sn ]. Jetzt zeigt die Annahme der Minimalität, dass dann auch C 0 [v1 , . . . , vm ]⇓ mit vi = tφ(i) . Da der
Ausdruck C[t1 , t2 , . . . , tn ] in Normalordnung auf C 0 [v1 , . . . , vm ] reduziert, gilt
auch C[t1 , t2 , . . . , tn ]⇓.
2
Beispiel 5.4.17 Dieses Beispiel zeigt, dass der Beweis des Kontextlemma nicht
verallgemeinerbar ist: Nehme den Multikontext C = (λx.·1 )·2 . Dann reduziert
C[[x], 1] zu [1], aber weder ist eines der Löcher in einem Reduktionskontext,
noch kann man die eingesetzten Ausdrücke unverändert im Redukt wiederfinden. D.h. der Beweisschritt, der Normalordnungsreduktion anwendet, und dann
das Resultat als neuen Multikontext mit eingesetzten Termen beschreibt, ist in
diesem Fall nicht mehr zu begründen.
Leider gibt das Kontext-Lemma keine Information über die kontextuelle Äquivalenz von offenen Ausdrücken. In Zusammenarbeit mit folgender Aussage kann
man das Kontextlemma auch in allgemeineren Zusammenhängen verwenden.
Aussage 5.4.18 Seien s, t Ausdrücke mit den freien Variablen x1 , . . . , xn .
Dann:
s ≤c t gdw. für alle geschlossenen ti gilt: s[t1 /x1 , . . . , tn /xn ] ≤c
t[t1 /x1 , . . . , tn /xn ]
Beweis. Die nichttriviale Richtung der Implikation ist: Wenn für alle geschlossenen ti gilt: s[t1 /x1 , . . . , tn /xn ] ≤c t[t1 /x1 , . . . , tn /xn ], dann gilt s ≤c t.
Induktion nach der Anzahl der freien Variablen in s, t.
Es genügt, für s0 := λx1 , . . . xn .s und t0 := λx1 , . . . xn .t zu zeigen, dass s0 ≤c t0 .
Jetzt kann man das Kontextlemma anwenden.
Sei R ein Reduktionskontext. Dann ist das Loch entweder in einem case.
Dann sind die Reduktionen von R[s0 ], R[t0 ] beide mit Typfehler, oder das Loch
wird angewendet, d.h. R[] = R0 ([] r), wobei r geschlossen ist. Dann gilt wegen der Induktionsannahme λx2 , . . . , xn .s[r/x1 ] ≤c λx2 , . . . , xn .t[r/x1 ]. D.h.
R[s0 ] ∼c R0 ([s0 ] r) ∼c R0 (λx2 , . . . , xn .s[r/x1 ]). Somit gilt: R[s0 ]⇓ ⇒ R[t0 ]⇓. Das
Kontextlemma zeigt jetzt s0 ≤c t0 , und somit auch s ≤c t.
2
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
5.4.3
24
Programmtransformationen
Programmtransformationen sind zunächst mal alle Veränderungen eines Programms P resultierend in einem Programm P 0 . Natürlich soll diese Transformation nicht das Programm verfälschen, z.B. aus einem Programm, das Zahlen
testet, ob sie gerade sind, ein Programm machen, das Zahlen testet, ob sie ungerade sind. Dies wird durch den Begriff der kontextuellen Gleichheit erfasst.
Definition 5.4.19 Eine Programmtransformation ist eine Relation R auf Ausdrücken. Man sagt zu e1 R e2 , dass R den Ausdruck e1 zu einem Ausdruck e2
transformiert.
Die Programmtransformation R ist korrekt, wenn aus e1 R e2 stets e1 ∼c e2
folgt.
Die Programmtransformation R ist modular, wenn für jeden Kontext C aus
e1 R e2 auch C[e1 ] R C[e2 ] folgt.
Modularität einer Programmtransformation bedeutet, dass man sie auf Unterterme anwenden kann ohne den Rest des Programms zu kennen.
Beachte, dass die Korrektheit von Programmtransformationen zunächst nur bedeutet, das diese während des Kompilierens verwendet werden dürfen, während
des Programmlaufs, d.h. in eine Normalordnungsreduktion eingeschoben nur
dann, wenn man darauf achtet, dass sich die Terminierungseigenschaften nicht
ändern. Eine veränderte Normalordnung könnte auch eine gewünschte Verbesserung der Auswertung sein, aber dazu sind andere Betrachtungen notwendig:
Man muss dazu verschiedene Reduktionsstrategien vergleichen, was wir an anderer Stelle machen werden.
Beispiel 5.4.20 Ein triviales Beispiel für eine veränderte Normalordnungsreduktion, die aus korrekten Programmtransformation während der Normalordnungsreduktion besteht, aber ein falsches Terminierungsverhalten hat, ist:
Mache einen Normalordnungsreduktionsschritt, und dann mache diesen wieder
rückgängig. Diese “Reduktion“ ändert die Terme nicht, kann also i.a. nicht terminieren.
Korollar 5.4.21 Aus dem Satz 5.3.15 oben folgt, dass die Beta- und die caseReduktion an beliebigen Stellen eines Programms während des Kompilierens als
Programmtransformationen verwendet werden dürfen. Das gilt auch für die umgekehrten Regeln. D.h. Beta-Expansion und Case-Expansion.
Die meisten KFP-Programmtransformationen sind eine Kombination von
Kalkülregeln. Es gibt auch solche, die keine Kombination von Kalkülregeln sind.
(z.B. Assoziativität von append (++))
Die sogenannte Lambda-lifting-Transformation ist im ungetypten Lambdakalkül, d.h. ohne case und Konstruktoren, eine bekannte Methode, um Superkombinatorprogramme herzustellen.
Definition 5.4.22 Sei λx.e ein Ausdruck, so dass e eine freie Variable y
enthält. Dann ist die Lambda-lifting Transformation:
ll
λx.e −→ (λz.λx.e[z/y]) y, wobei z eine neue Variable ist.
25
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Aussage 5.4.23 Lambda-Lifting ist in KFP eine korrekte Programmtransformation.
Beweis. Das folgt daraus, dass sich (λz.λx.e[z/y]) y mittels Beta-Reduktion
wieder zu λx.(e[z/y])[y/z] = λx.e reduzieren läßt, und dass Beta eine korrekte
Programmtransformation ist.
2
Die sogenannte η-Transformation ist ebenfalls im ungetypten Lambda-kalkül
bekannt. Man kann diese zum Lambda-Kalkül hinzunehmen oder nicht, was
natürlich verschiedene Reduktions-Gleichheiten ergibt.
Definition 5.4.24 Sei e ein Ausdruck, der die Variable x nicht frei enthält.
Dann ist die η-Transformation definiert als:
η
λx.(e x) −→ e
Lemma 5.4.25 Die η-Transformation ist nicht korrekt in KFP.
Beweis Dazu genügt es einen Ausdruck anzugeben, für den das falsch ist.
Der Ausdruck (λx.bot x) hat im leeren Kontext eine WHNF, aber es gilt
η
2
λx.bot x −→ bot, und bot hat keine WHNF.
Die folgende Variante der η-Reduktion ist korrekt:
Definition 5.4.26 Sei e ein Ausdruck, der die Variable x nicht frei enthält, und
der eine WHNF hat, die eine Abstraktion ist. Dann ist die η 0 -Transformation
definiert als:
η0
λx.(e x) −→ e
Aussage 5.4.27 Die η 0 -Transformation ist korrekt.
Beweis. Hierzu genügt es, den Satz zur Bisimulation zur verwenden: Es genügt,
beide Ausdrücke auf allen Argumenten r zu testen: ((λx.(e x)) r) reduziert zu
(e r).
2
Man kann diese Reduktion noch etwas allgemeiner fassen: Die η-Reduktion ist
η 00
korrekt, wenn man sie im richtigen Kontext verwendet: ((λx.(e x)) t) −→ (e t) .
Diese Reduktion ist ebenfalls mittels des Bisimulationssatzes leicht als korrekt
zu erkennen.
5.4.4
Kleinste und Größte Elemente bzgl. ≤c
Was wir auch zeigen können, ist die kontextuelle Äquivalenz aller nichtterminierenden Ausdrücke und aller ungetypten Ausdrücke.
Offene Ausdrücke, die ungetypt sind, oder die eine nichtterminierende Normalordnungsreduktion haben, behalten diese Eigenschaft auch nach Einsetzung von
Termen für die freien Variablen:
26
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
n
1. Sei t −→ t0 , so dass t0 direkt ungetypt ist. (wir haben das dynamisch
ungetypt genannt). Dann ist für jede Einsetzung ρ auch der Ausdruck t ρ
dynamisch ungetypt.
Das liegt daran, dass die Normalordnungsreduktion freie Variablen nicht
unmittelbar anfasst, sondern nur mitschleppt.
∞,n
∞,n
2. Sei t −→. Dann gilt für jede Einsetzung ρ auch t ρ −→. Auch hier ist die
Normalordnungsreduktion die gleiche.
Wir zeigen mit den Mitteln des Skriptes (nicht über den Bisimulationssatz),
dass bot das kleinste Element ist:
Lemma 5.4.28 Sei s ein geschlossener Ausdruck mit s⇑ und t ein weiterer
geschlossener Ausdruck. Dann gilt s ≤c t.
Beweis. Sei R ein Reduktionskontext. Jede Normalordnungsreduktion von R[s]
muss zuerst s in WHNF reduzieren, was nach Voraussetzung nicht geht. Damit
gilt R[s]⇓ ⇒ R[t]⇓ und somit folgt aus dem Kontextlemma, dass s ≤c t
2
Lemma 5.4.29 Sei s ein (evtl. offener) Ausdruck, so dass für alle Einsetzungen ρ: (s ρ)⇑, und sei t ein weiterer Ausdruck. Dann gilt s ≤c t.
Beweis. Das Lemma oben zeigt, dass für alle Einsetzungen ρ, die alle freien
Variablen von s, t mit geschlossenen Ausdrücken ersetzen, dass sρ⇑, und damit
sρ ≤c tρ. Lemma 5.4.18 zeigt dann auch s ≤c t.
2
Damit gibt es ein kleinstes Element in der ≤c -Ordnung in der Menge der geschlossenen Ausdrücke, nämlich bot, auch als ⊥ geschrieben.
Aussage 5.4.30 Seien s, t zwei (evtl. offene) Ausdrücke, die entweder eine
nicht terminierende Normalordnungsreduktion haben oder direkt nicht wohlgetypt sind. Dann gilt s ∼c t.
Beweis. Folgt aus obigem Lemma, da wegen s⇑ und t⇑ sowohl s ≤c t als auch
t ≤c s gilt.
2
Beachte, dass es auch offene Terme gibt, die eine unendliche Normalordnungsreduktion haben: Z.B. bot x. Für diese gilt ebenfalls bot x ∼c bot.
Aussage 5.4.31 Wenn es mindestens einen Konstruktor gibt, dann gibt es kein
größtes Element in der ≤c -Ordnung
Beweis. Sei c ein Konstruktor der Stelligkeit n: Ein größtes Element s muss eine
WHNF haben. Dafür gibt es zwei Möglichkeiten:
• s = λx . s0 . In dem Fall ist c |c .{z
. . c} 6≤c s: Nehme den Kontext
n
case [] {(c x1 . . . xn ) → c; lambda → ⊥; . . .}.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
27
• s = d s1 . . . sm wobei d ein Konstruktor der Stelligkeit m ist. In dem Fall
ist λx.x 6≤c s: Nehme den Kontext case [] {(d x1 . . . xn ) → bot; lambda →
d; . . .}.
2
Diese Aussage zeigt, dass eine Einbettung von KFP im Lambda-Kalkül nicht 1-1
sein kann, denn der Lambda-Kalkül ohne Konstruktoren mit der kontextuellen
Ordnung wie in “The lazy Lambda-Calculus“ von Abramsky beschrieben, hat
ein eindeutiges größtes Element (modulo der kontextuellen Ordnung): (Y K)1 ,
das ist ein Lambda-ausdruck, der beliebig viele Argumente schluckt, ohne sich
zu verändern: (Y K) t ∼c (Y K), wobei ∼c hier die kontextuelle Ordnung im
lazy Lambda-Kalkül ist.
5.4.5
Auswertungsreihenfolgen
Aus dem Satz über die Korrektheit der Anwendbarkeit von Reduktionen an
beliebigen Stellen in einem Ausdruck folgt, dass man Ausdrücke in einem Programm in beliebiger Reihenfolge auswerten darf, solange man die Reduktionsregeln verwendet. Allerdings gilt das nur, wenn die Auswertungen auch terminieren.
Definition 5.4.32 Eine Auswertungsstrategie S ist eine Relation t →S t0 auf
Termen, wobei zumindest t →S t0 ⇒ t ∼c t0 gelten soll. Diese Relation muss
effektiv sein, d.h es muss einen Algorithmus geben, der bei Eingabe des Terms
t das Redukt t0 berechnet und ausgibt, oder sagt: keine Reduktion möglich, oder:
ist eine S-Normalform. Die transitive, reflexive Hülle der Reduktion bezeichnen
∗
∗
wir mit −→S . Falls t −→S t0 , wobei t0 eine S-Normalform, dann schreiben wir
t⇓S .
• Eine Strategie S ist korrekt, gdw. für alle Terme t: t⇓ ⇔ t⇓S . Da man
sinnvollerweise annimmt, dass S-Normalformen auch WHNFs sind, gilt
die Korrektheit immer.
• Seien S1 , S2 zwei Strategien. Die Strategie S1 ist strikter als S2 , gdw. für
alle Terme t: t⇓S1 ⇒ t⇓S2 ,
Folgende Normalform für strikte Auswertung entspricht der Auswertung der
Argumente einer Anwendung vor dem Einsetzen der Argument.
Definition 5.4.33 Eine strikte Normalform ist definiert als:
• Eine Abstraktion ist eine strikte Normalform.
• Wenn s1 , . . . , sn strikte Normalformen sind, dann auch c s1 . . . sn für
einen Konstruktor c mit ar(c) ≥ n.
1 Y ist der Fixpunktkombinator wie in Abschnitt 5.6.2 beschrieben und K ist definiert als
K xy=x
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
28
Zwei Strategien bzw. Methoden der Reduktion haben eigene Namen:
Normalordnung (normale Reihenfolge, normal-order, nicht-strikte Auswertung), die wir bereits kennen.
Anwendungsordnung (applikative Reihenfolge, applikative Ordnung, strikte Auswertung). Hier wird verlangt, dass man die Beta-Reduktion nur
verwenden darf, wenn das Argument ausgewertet ist, d.h., eine strikte
Normalform ist.
Die Anwendungsordnung ist die Strategie, die in Lisp, und ML verwendet wird.
Es gibt auch andere Varianten der Auswertung, in denen Normalform durch
etwas anderes (z.B. HNF) ersetzt ist.
Aus Satz 5.4.6 folgt, dass die verschiedenen Methoden die gleichen Resultate
liefern, wenn die Auswertung mit Konstruktortermen terminiert.
Allerdings kann das Verhalten bzgl. Terminierung verschieden sein:
Beispiel 5.4.34 Sei K = λx.(λy . x).
Dann wertet die Anwendungsordnung den Ausdruck K Nil bot so aus, dass
zuerst versucht wird, bot in Normalform zu bringen. Das gelingt nicht, also
terminiert das nicht.
Die Normalordnung wertet den Ausdruck K Nil bot so aus, dass zuerst der Ausdruck ((λy . Nil) bot) entsteht. Danach wird bot eingesetzt, und der Ausdruck
reduziert zu Nil. D.h. in diesem Fall terminiert die Auswertung.
Es gilt folgender Satz (Church-Rosser) der in der Literatur unter Existenz einer
Standardreduktion für den Lambda-Kalkül bekannt ist. Wir formulieren ihn
leicht verallgemeinert.
Satz 5.4.35 Sei s ein geschlossener Ausdruck. Wenn es einen Ausdruck t in
WHNF gibt mit s ∼c t, dann gilt s⇓.
Beweis. Unsere Definitionen sind auf diesen Fall zugeschnitten: Nehme den leeren Kontext. Dann folgt aus s ∼c t und t⇓ dass auch s⇓
2
Insbesondere heißt das:
Aussage 5.4.36 Wenn für einen Term t die Reduktion in Anwendungsordnung
terminiert, dann auch die Reduktion in Normalordnung. D.h. Die Anwendungsordnung ist strikter als die Normalordnung.
Beweis. Mit Induktion über die Tiefe der entstehenden strikten Normalform. 2
Aussage 5.4.37 Die Normalordnungsreduktion ist eine maximale Strategie bzgl
der Relation ist strikter als“ , wobei wir die striktere Strategie als kleiner an”
sehen.
Beweis. Das folgt aus dem Standardisierungssatz.
29
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Beispiel 5.4.38 Zu verschiedenen Reihenfolgen der Auswertung von Ausdrücken.
Wertet man (quadrat(3 ∗ 3)) aus, so kann man das auf zwei Weisen machen:
(quadrat(3 ∗ 3)) → quadrat 9 → 81
oder
(quadrat(3 ∗ 3)) → ((3 ∗ 3) ∗ (3 ∗ 3)) → (9 ∗ (3 ∗ 3)) → (9 ∗ 9) → 81
Bemerkung: Funktionale Programmiersprachen, die die Anwendungsordnung als
Auswertungsstrategie haben, nennt man strikte FPS, solche die die Normalordnung benutzen, nennt man auch nicht-strikte FPS. Der Begriff kommt eigentlich
aus der denotationalen Semantik der Programmiersprachen, wird aber auch für
die passende operationale Semantik verwendet.
5.4.6
Relation zur Konfluenz
Im Lambdakalkül verwendet man meist den Begriff der Reduktionsgleichheit
∗
←→, bzw. αβ-Gleichheit, um Gleichheit von Ausdrücken zu spezifizieren: Zur
Erinnerung:
∗
zwei KFP-Ausdrücke s, t sind reduktionsgleich (αβ-gleich) (s ←→ t,
wenn es eine Folge von (Beta und Case-) Reduktionen (vorwärts
oder rückwärts) und Umbenennungen gibt, die s in t transformiert.
∗
Es gilt: s ←→ t ⇒ s =c t.
Die Konfluenz ist eine Eigenschaft, die syntaktisch analog zur Standardreduktion ist:
∗
∗
∗
∗
Wenn s −→ s1 , s −→ s2 , dann existiert t1 =α t2 mit: s1 −→ t1 , s2 −→
t2 .
Diese Konfluenzeigenschaft gilt ebenfalls in KFP für die Reduktion. Sie lässt
sich ganz analog mit den Mitteln zeigen, die wir bereitgestellt haben: mit der
1-Relation. Allerdings ist die Konfluenz nicht die wesentliche Eigenschaft des
Kalküls. Wesentlich ist, dass alle Reduktionen korrekte Programmtransformationen sind. Aus der Konfluenz selbst folgt diese Eigenschaft nicht, da die Konfluenz nichts darüber aussagt, von welcher Art die schließenden Reduktionen
sind (normal order oder nicht).
5.5
Übersetzungen von Haskell nach KFPTS
nach KFPT nach KFP
Wir betrachten jetzt die Übersetzungen
Haskell → KFPTS → KFPT → KFP
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
30
und deren Eigenschaften. Da wir die Sprachen, die Reduktion und die entsprechenden WHNFs definiert haben, haben wir auch einen jeweiligen Begriff von
Terminierung, kontextueller Approximation und kontextueller Äquivalenz. Die
Notationen der Begriffe versehen wir mit den jeweiligen Indizes KFPTS, KFPT,
KFP wobei wir KFP weglassen.
Definition 5.5.1 Sei τ eine Übersetzung von P1 nach P2 . Dann betrachten wir
folgende Eigenschaften:
• τ erhält Terminierung, gdw. für alle s, t in P1 gilt:
s⇓1 ⇔ τ (s)⇓2
• τ erhält kontextuelle Ordnung, gdw für alle s, t in P1 gilt:
s ≤1 t ⇔ τ (s) ≤2 τ (t)
• τ ist vollständig bzgl der kontextuellen Ordnung, gdw für alle s, t in P1
gilt:
s ≤1 t ⇒ τ (s) ≤2 τ (t)
• τ ist korrekt bzgl. der kontextuellen Ordnung, gdw für alle s, t in P1 gilt:
s ≤1 t ⇐ τ (s) ≤2 τ (t)
Die theoretisch sauberste Übersetzung / Implementierung ist eine, die die kontextuelle Ordnung erhält. Normalerweise hat man aber diese Eigenschaft nicht,
sondern nur die Erhaltung der Terminierung und die Korrektheit bzgl. der kontextuellen Ordnung.
Die Korrektheit der Übersetzung bzgl. der kontextuellen Ordnung ist ausreichend, um Überlegungen zur Gleichheit von Ausdrücken bzw. Korrektheit von
Programmtransformationen in die Zielsprache (meist die Kernsprache) zu verlagern. Damit kann man zwar einige Gleichungen verlieren, aber hat ein gutes
Werkzeug, um solche Gleichungen in einer einfacheren Sprache zu zeigen.
Aussage 5.5.2 Ist die Übersetzung von P1 nach P2 korrekt bzgl. kontextueller
Ordnung, dann gilt folgendes:
Seien s, t P1 -Terme, und gilt nach Übersetzung τ (s) ∼2 τ (t), dann gilt auch
s ∼1 t.
5.5.1
KFPT nach KFP
Erstes Beispiel einer Übersetzung ist die Kodierung τ von KFPT-Ausdrücken in
KFP. Hierbei wurden nur die case-Ausdrücke verändert:
case s {p1 -> τ (t1 ); pn ->τ (tn )}.
wird unter τ zu
case τ (s) {p1 -> τ (t1 ); pn ->τ (tn );
->bot}.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
31
Aussage 5.5.3 Die Übersetzung von KFPT nach KFP erhält die Terminierung, und ist korrekt bzgl. kontextueller Ordnung.
Beweis. Die Erhaltung der Terminierung ist einfach zu sehen, da man Normalordnungsreduktionen direkt übertragen kann.
Um die Korrektheit bzgl. kontextueller Ordnung zu sehen, muss man folgendermaßen vorgehen:
Seien s, t KFPT-Terme und sei τ (s) ≤c,KF P τ (t). Um s ≤c,KF P T t zu zeigen,
sei C ein KFPT-Kontext und sei C[s]⇓KF P T . Der Kontext kann in einen KFPKontext C 0 übersetzt werden, so dass folgendes gilt: τ (C[s]) = C 0 [τ (s)] und
somit C 0 [τ (s)]⇓KF P . Daraus folgt wegen der Voraussetzung τ (s) ≤c,KF P τ (t),
dass C 0 [τ (t)]⇓KF P , und jetzt wieder mit der Erhaltung der Terminierung, dass
C[t]⇓KF P T .
2
Überraschend ist, dass der relative kleine Unterschied zwischen KFP und KFPT
schon dazu führt, dass die Übersetzung nicht mehr vollständig bzgl kontextueller
Ordnung ist:
Aussage 5.5.4 Die Übersetzung von KFPT nach KFP ist nicht vollständig
bzgl. der kontextuellen Ordnung.
Beweis. Betrachte den Kontext D[] = λf . caseBool (f []) {True →
True; False → True} und die beiden Ausdrücke
s0 = D[(λx . ⊥)] und t0 = D[⊥].
Behauptung: In KFPT sind diese Ausdrücke nicht unterscheidbar. D.h.
s0 ∼c,KF P T t0 . Sei dazu C ein beliebiger KFPT-Kontext. Wir argumentieren,
dass beide Ausdrücke C[s0 ], C[t0 ] dasselbe Resultat bzgl. Terminierung ergeben. Wir machen eine Reduktion von C[s0 ] und C[t0 ] mit Markierungen, wobei
sowohl der case-Ausdruck in D markiert ist als auch der Ausdruck λx . bot und
bot.
Es genügt die Überlegung, dass für alle r die Resultate der Normalordnungsreduktion von s0 r und t0 r immer gleich sind. Es wird zunächst r (λx . bot)
reduziert, parallel dazu r (bot). Falls die Normalordnungsreduktion den markierten Term (λx . bot) oder (bot) auswertet, ist das Ergebnis Nichtterminierung,
also äquivalent zu bot, da es entweder in einem KFPT-caseals erstes Argument
vorkommt, oder in Funktionsposition in einer Applikation. Wenn die Ausdrücke
nicht ausgewertet werden, dann ist das Ergebnis auf beiden Seiten vergleichbar. Da danach ein caseBool auf das Ergebnis gemacht wird, ist das entweder
ebenfalls bot, oder falls es einer der Werte True oder False ist, das Ergebnis
True.
D.h. die Ausdrücke s0 , t0 sind in KFPT nicht unterscheidbar.
Behauptung: s0 , t0 sind unterscheidbar in KFP.
Dazu sei strict der in KFP definierbare Operator. Als Kontext nehme
([] (strict(λx . True))).
Das ergibt für s0 das Resultat True, da λx . bot in WHNF.
Für t0 ergibt sich bot, da obige Funktion den Versuch startet, bot auszuwerten.
2
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
32
Diese Ungenauigkeit ließe sich beheben durch Veränderung der Sprache KFPT
insbesondere ein anderes case. Diese case-Veränderung machen wir jedoch
nicht, da das eher eine künstliche Reparatur wäre, und nicht zum Verständnis
beiträgt. Wir werden aber, um die Übersetzungskette von Haskell nach KFP
der Praxis anzupassen, auch die Übersetzungskette
Haskell → KFPTS+seq → KFPT+seq → KFP
betrachten
5.5.2
KFPT+seq nach KFP
Wenn wir seq zu KFPT hinzunehmen, gilt immer noch die Korrektheit der
Übersetzung τ bzgl kontextueller Äquivalenz, aber obiges Beispiel wird ungültig.
Beispiel 5.5.5 Trotzdem ist (das angepasste) τ nicht vollständig bei Übersetzung von KFPT+seq nach KFP:
Definiere:
s0 = \f -> if (f True) then (if (f Nil) then bot else True)
else bot
t0 = \f -> if (f Nil)
then (if (f True) then bot else False)
else bot
Diese Funktionen s0 , t0 sind in KFPT+seq nicht unterscheidbar, da (f True)
und (f Nil) nicht verschiedene (terminierende) Boolesche Werte liefern
können. Wenn die Funktion f unterschiedliche Werte liefern soll, dann muss
sie den Wert mittels eines case sich anschauen. Dann muss aber eins der Ergebnisse bot sein, denn das case ist getypt.
In KFP sind sie unterscheidbar, da man ein solches f leicht definieren kann:
das case in KFP kann bei True den Wert True zurückgeben, und bei Nil den
Wert False.
Das bedeutet, dass die Definitionsmöglichkeit für den Operator strictbzw. seq
nicht alles ist, sondern die Typisierung macht auch noch einen Unterschied aus:
Durch die Typisierung hat man weniger Kontexte in KFPT.
5.6
Übersetzung KFPTS nach KFPT
Wir untersuchen in diesem Kapitel den Zusammenhang der Reduktion zwischen
KFPT und KFPTS.
Es gibt folgende wesentlichen Unterschiede:
• In KFPT kann man Beta-Reduktion bereits bei einem Argument anwenden, in KFPTS die SK-Beta-Reduktion erst, wenn ausreichend viele Argumente vorhanden sind.
• In KFPTS gibt es allgemeine Rekursion. In KFPT gibt es keine (direkte)
Rekursion.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
33
Wir geben zwei Übersetzungen τ und σ an:
• τ : KF P T S → KF P T .
• σ : KF P T → KF P T S wobei lambda-freie Superkombinatorausdrücke
entstehen.
Um die Betrachtungen zur Übersetzungsfunktion zu vereinfachen, nehmen wir
an, dass die Typen und Konstruktoren im KFPT und im KFPTS-Programm
jeweils dieselben sind.
5.6.1
Von KFPT nach KFPTS: Lambda-Lifting
Bei der Übersetzung schießen wir etwas über das Ziel hinaus: wir zeigen, wie
man ein lambda-freies KFPTS-Programm aus einem KFPT-Programm erhalten
kann.
Erster Schritt der Übersetzung ist die Elimination von lokalen Abstraktionen,
die lokal freie Variablen enthalten. Dies wäre nicht direkt in lambda-freies
KFPTS übersetzbar.
Definition 5.6.1 Sprechweisen in der Sprache KFPT (im Lambda-Kalkül):
• Ein Kombinator ist ein geschlossener Ausdruck.
• Eine maximale Abstraktion t0 in einem Ausdruck t ist entweder t selbst,
wenn t eine Abstraktion ist, oder ein Unterausdruck (eine Abstraktion)
dessen direkter Oberterm keine Abstraktion ist.
• Ein Superkombinator t ist ein geschlossener KFP-Ausdruck e, so dass alle
maximalen Abstraktionen in e, die echte Unterterme von t sind, ebenfalls
Superkombinatoren sind.
• Ein Ausdruck, dessen sämtliche maximale Abstraktionen Superkombinatoren sind, heißt Superkombinatorausdruck.
Beispiel 5.6.2
• λx1 , x2 , . . . xn . xi ist ein Superkombinator.
• λx . (x λy . x) ist kein Superkombinator, da die maximale Abstraktion
λy . x eine freie Variable enthält.
• (λx.λy.(y (λx.x))) ist ein Superkombinator.
• case Nil {(x : xs) → x; Nil → Nil} ist ebenfalls ein Superkombinator.
Allerdings einer, der keine Argumente hat.
Die Regel Lambda-lifting verwenden wir jetzt, um aus jedem geschlossenen KFPT-Ausdruck einen kontextuell äquivalenten KFPTSuperkombinatorausdruck zu machen.
34
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Sei t ≡ C[t0 ] wobei t0 eine maximale Abstraktion in t ist, C nichttrivial, jeder echte Unterterm von t0 , der eine maximale Abstraktion
ist, ein Superkombinator ist, und y frei ist in t0 .
Dann:
ll
C[t0 ] −→ C[((λz . (t0 [z/y])) y)]
wobei z eine neue Variable ist.
Offenbar ist diese Regel korrekt, was direkt aus den Sätzen zur kontextuellen Gleichheit in KFP (KFPT) folgt, da sich diese Transformation mit BetaReduktion wieder rückgängig machen lässt.
Außerdem terminiert die mehrfache Anwendung der Regel und erzeugt einen
Superkombinatorausdruck:
Lemma 5.6.3 Jeder geschlossene KFPT-Ausdruck kann mittels endlich vieler
Lambda-Liftings in einen KFPT-Superkombinatorausdruck transformiert werden, so dass kontextuelle Äquivalenz erhalten bleibt.
Beweis. Die Regel ist immer anwendbar, wenn t kein Superkombinatorausdruck
ll
ist. Also zeigen wir, dass die fortgesetzte Regelanwendung von −→ terminiert.
Am einfachsten ist das folgendermaßen einzusehen: Jeder Superkombinator im
Ausdruck wird durch einen Namen abgekürzt. Das ändert nichts an der Ausführbarkeit und Ausführung der Regel. Eine einfache Anwendung auf t0 vermindert
die Anzahl der freien Variablen in t0 , und wenn der Ausdruck ein Superkombinator ist, dann kann er abgekürzt werden. Insgesamt wird danach die Anzahl der
λ’s weniger, auch wenn man die vom Lambda-Lifting neu eingeführten mitzählt.
Das terminiert, da nach endlich vielen Anwendungen keine Abstraktionen mehr
vorhanden sind; alle sind in Abkürzungen ausgelagert.
2
Superkombinatorübersetzung KPFT → KFPTS
Ein KFPT-Ausdruck wird mittels σ nach KFPTS übersetzt, indem man
zunächst mit Lambda-Lifting daraus einen Superkombinator macht und dann
von innen her die Superkombinatorausdrücke durch neu definierte Superkombinatoren ersetzt.
D.h. man fügt eine Definition
name := E
Wenn man das noch syntaktisch etwas umschreibt in
name x1 . . . xn := E 0
wobei E ≡ λx1 . . . xn . E 0 , dann hat man KFPTS-Definitionen.
Die Stelligkeit eines Superkombinators legen wir fest als die Anzahl der im
Lambda-Präfix des Superkombinators gebundenen Variablen.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
35
Die Vergleichbarkeit der Normalordnungsreduktionen ist folgendermaßen:
Eine SK-Beta -Reduktion lässt sich ausdrücken durch mehrere BetaReduktionen in KFPT. Die Anzahl entspricht genau der Stelligkeit des zugehörigen Superkombinators.
Umgekehrt ist es genauso, entweder ist es eine Beta-Reduktion, oder man kann
mehrere Normalordnungsreduktionen jeweils zusammenfassen als eine SK-BetaReduktion, oder in KFPTS gibt es keine weitere Normalordnungsreduktion, da
zuwenig Argumente da sind, aber in KFPT kann man noch einige Reduktionen
machen, bis eine Abstraktion erreicht ist.
Ein KFPT-Superkombinatorausdruck ist problemlos nach KFPTS zu übersetzen, wobei alle Definitionen und der Ausdruck lambdafrei sind:
Es gilt:
Aussage 5.6.4 Sei t ein geschlossener KFPT-Ausdruck. Dann ist t⇓ ⇔
σ(t)⇓KF P T S
D.h Diese Übersetzung σ ist korrekt unter dem Kriterium der Erhaltung der
Terminierung.
Die Erhaltung der kontextuellen Ordnung (und damit auch der Äquivalenz) von
Ausdrücken bei der Übersetzung σ gilt ebenfalls:
Aussage 5.6.5 Seien s, t geschlossene KFPT-Ausdrücke. Dann gilt
s ≤c.KF P T t ⇔ σ(s) ≤c,KF P T S σ(t).
Beweis. Die Richtung
s ≤c.KF P T t ⇐ σ(s) ≤c,KF P T S σ(t)
gilt, da KFPT-Kontexte auch KFPTS-Kontexte sind. Für die Umkehrung
braucht man noch die Übersetzung der KFPTS-Kontexte, die rekursive Superkombinatoren enthalten.
2
Bemerkung 5.6.6 Die Übersetzung σ, obwohl sie so einfach ist, ist nicht bzgl
Reduktionsäquivalenz“korrekt: WHNFs entsprechen sich nicht bei Übersetzung.
”
Die Reduktion lässt sich übersetzen, bis auf die Reduktion von KFPT-WHNF zu
KFP-WHNF.
Beachte, dass man die Betrachtung auch für die Übersetzung von KFPT in
lambda-freies KFPTS machen kann. In lambda-freiem KFPTS bräuchte man
eine erweiterte Kontext-Definition: Um genügend viele Kontexte zu haben, muss
man erlauben, dass man vor der Kontextbildung erst noch Superkombinatoren
definieren kann.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
5.6.2
36
Übersetzung KFPTS nach KFPT
Für die Übersetzung KFPTS nach KFPT, insbesondere von rekursiven Superkombinatoren, benötigen wir etwas mehr Aufwand, da man Rekursion nicht
direkt nach KFPT übersetzen kann.
Das Stichwort heißt: Fixpunkte und Fixpunktkombinatoren.
Die Auflösung der Rekursion ist möglich mit einem Superkombinator Y , der
folgendes leistet:
Für alle Abstraktionen F gilt in KFPT: Y F ∼c F (Y F ) D.h. (Y F ) ist ein
Fixpunkt von F . Man nennt Y auch einen Fixpunktkombinator.
Solch einen Kombinator gibt es (sogar in KFP):
Y = λf.(λx.f (x x))(λx.f (x x))
Wir reduzieren (Y F ):
(Y F ) → (λx.F (x x))(λx.F (x x))
→ F (x x)[x/(λx.F (x x))]
= F ((λx.F (x x))(λx.F (x x))
Satz 5.6.7 Für alle Abstraktionen F gilt Y F ∼c F (Y F ).
Beweis. Folgt aus den Sätzen über kontextuelle Gleichheit der Reduktion in
KFPT.
2
Diese Äquivalenz kann man ausnutzen, um Rekursion aus den KFPTSDefinitionen von Superkombinatoren zu eliminieren.
Elimination der einfachen Rekursion:
Sei H x_1 \... x_n = e eine rekursive Definition in KFPTS. Wir nehmen an,
dass e nur Vorkommen von H enthält, aber keine anderen Superkombinatoren.
Dann erzeuge den Ausdruck
G = λf.λx1 . . . λxn .e[f /H]
in dem der Superkombinatorname abstrahiert ist.
Danach definieren wir F = (λx.G(x x)) (λx.G(x x)). Das entspricht F = Y G
nach einer Reduktion. Dies ist ein Ausdruck in KFPT ohne Vorkommen eines
Superkombinatornamens.
Wir zeigen, dass F zu H äquivalent ist in folgendem Sinn:
Die Terminierung ändert sich nicht, wenn man vom KFPTS-Ausdruck mit H
zu dem KFPT-Ausdruck mit F übergeht.
Definition 5.6.8 Sei τH die Übersetzung, die in jedem KFPTS-Ausdruck die
Vorkommen von H durch F ersetzt.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
37
Aussage 5.6.9 Sei in KFPTS nur eine einzige einfache rekursive Definition
(H) wie oben. Sei t ein geschlossener KFPTS-Ausdruck, und sei τH wie oben
definiert. Dann ist t⇓KF P T S ⇔ τ (t)⇓
Beweis. Wir haben schon gesehen, dass der Unterschied zwischen der KFPTNormalordnungsreduktion und der KFPTS-Normalordnungsreduktion nicht wesentlich ist. Man kann zeigen, dass sich Normalordnungsreduktion mit τH
überträgt. Der einzig interessante Fall, der einen Unterschied machen könnte,
tritt ein, wenn der Normalordnungsredex gerade H a1 . . . an ist. Dann reduziert
dieser Term auf der KFPTS-Seite zu e[a1 /x1 , . . . an /xn ]. Auf der KFPT-Seite
ergibt sich in diesem Fall:
F a1 . . . an
→
→no
∗
−→no
(λx.G (x x))(λx.G (x x)) a1 . . . an
G((λx.G (x x))(λx.G(x x))) a1 . . . an
e0 [(λx.G (x x))(λx.G (x x))/H; a1 /x1 , . . . , an /xn ]
Das ist gerade die Übersetzung τH (e[a1 /x1 , . . . an /xn ]). Da sich die WHNF entsprechen (bis auf KFPT-Reduktionen kurz vor Erreichen der WHNF), ergibt
sich mit Induktion nach der Anzahl der KFPTS-Normalordnungsreduktionen
die Behauptung.
2
Der Fall, dass die Rekursion verschränkt ist, bzw. dass die Verwendung der
Superkombinatornamen nicht eingeschränkt ist, ist einfach zu modellieren, da
es Konstruktoren gibt:
Wenn in einem KFPTS-Programm die (rekursiven) Superkombinatoren
S1 , . . . , Sn definiert sind, dann fügen wir einen n-stelligen Konstruktor c hinzu.
Die Übersetzung kann dann mit einer einzigen rekursiven Funktion F gemacht
werden, wobei F so definiert ist, dass es eine Fallunterscheidung über das erste (ganzzahlige) Argument macht, so dass (F i) gerade Si implementiert, Die
Vorkommen von Si werden in allen Definitionen durch F i ersetzt.
Die Problematik verschiedener Stelligkeit kann leicht umgangen werden, indem
man F so definiert, dass die Stelligkeit 1+ das Maximum der Stelligkeiten der
Si ist. Das Si wird jeweils durch F i d . . . d ersetzt, wobei man das Dummyargument d gerade so oft hinzufügt, dass die Stelligkeit stimmt.
Damit haben wir alle Funktionen in eine einzige rekursive Funktion kodiert.
Diese Methode ist etwas zu direkt, da sie einerseits neue Konstruktoren einführt,
und andererseits beim Hinzufügen von unabhängig definierten Funktionen die
Übersetzung ändert.
Beispiel 5.6.10 Angenommen, wir haben zwei verschränkt rekursiv definierte
Funktionen f, g.
f x = f (f x) (g x)
g x = g x (f x)
Dann definieren wir eine Funktion G:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
G
= \F
i x . case_int i
{1 ->
2 ->
(F
(F
38
1 (F 1 x) (F 2 x));
2 x (F 1 x)); ... }
Bildet man jetzt Y G, und ersetzt alle Vorkommen von f durch Y G 1 und alle
Vorkommen von g durch Y G 2, dann hat man die Rekursion eliminiert.
Denn g a wird ersetzt durch Y G 2 a. Dies wird durch Reduktion
zu
G ((\x. G(x x))((\x. G(x x)))) 2 a
und
wenn
wir
((\x. G(x x))((\x. G(x x)))) mit GG abkürzen, dann wird dies nach
Reduktion zu (GG 2 x (GG 1 x))).
Man sieht auch, dass es nicht nötig ist, die Funktionen unter einem Konstruktor
zusammenzufassen. Es genügt eine Fallunterscheidung.
Definition 5.6.11 Die Übersetzung τ : KF P T S → KF P sei so definiert, dass
Superkombinatoren, die nicht rekursiv sind, direkt nach KFPT übersetzt werden.
Rekursive Superkombinatoren werden in Gruppen zusammengefasst, so dass sich
Funktionen aus verschiedenen Gruppen nicht aufrufen. Danach kann man die
Übersetzung gruppenweise durchführen. Dies garantiert, dass man neue Definitionen zu einem Programm hinzufügen kann, ohne dass sich die Übersetzung
bereits vorhandener ändert.
Folgendes Diagramm gibt den Zusammenhang wieder zwischen Normalordnung
und Übersetzung:
Wenn t nach τ (t) übersetzt wird, und man einen Normalordnungsreduktionsschritt von t nach t0 machen kann, dann kann man τ (t) in KFP-Normalordnung
auf τ (t0 ) reduzieren, wie im Diagramm angegeben.
τt
t
no,*
no
t t'
$t'
Insgesamt haben wir damit eine Übersetzung τ von KFPTS-Superkombinatoren
nach KFPT, so dass die Terminierung und die kontextuelle Approximation erhalten bleiben, D.h. Wir können sagen, dass KFPT und KFPTS (bzgl. τ ) äquivalent sind:
Satz 5.6.12
• t⇓KF P T S ⇔ τ (t)⇓
• s ≤c,KF P T S t ⇔ τ (s) ≤c τ (t)
Begründung des Satzes. Dass die Terminierung erhalten bleibt, folgt aus den
Überlegungen zur Übertragung der Reduktion.
Die Übersetzung τ ist die Inverse zu σ: Es gilt τ (σ(t)) ∼c t.
Dass die kontextuelle Ordnung erhalten bleibt, kann man folgendermaßen sehen:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
39
1. s ≤c,KF P T S t. Sei C ein KFPT-Kontext, so dass C[τ (s)]⇓. Dieser Kontext
ist auch ein KFPTS-Kontext, so dass τ (C[s]) = C[τ (s)]. Damit gilt C[t]⇓,
und somit auch τ (C[t])⇓.
2. Sei τ (s) ≤c τ (t). Um s ≤c,KF P T S t zu zeigen, sei C ein KFPTS-Kontext,
und sei C[s]⇓. Diesen kann man mit τ nach KFPT übersetzen, und erhält
dann τ (C[t])⇓. Mit 1 erhält man dann auch t⇓.
Satz 5.6.13 KFPTS und KFPT sind äquivalent bzgl. der Übersetzungen τ und
σ.
Hier fehlt noch die eine Richtung zu σ. Das geht aber genauso wie bei τ .
Bemerkung 5.6.14 Das Hinzufügen von seq zu KFPTS ergibt die gleichen
Eigenschaften in der Übersetzungskette
Haskell → KFPTS+seq → KFPT+seq → KFP
wie bei
Haskell → KFPTS → KFPT → KFP
Wir können somit Gleichungen s ∼c t in Haskell zeigen, indem wir diese nach
KFP übersetzen und dort nachweisen. Bei Widerlegungen muss man sich vergewissern, dass die Widerlegung auch in Haskell funktioniert.
5.7
Approximationen in KFP und Induktion
Auf der Ordnung ≤c in KFP kann man aufbauen, um mittels Approximation
kontextuelle Gleichheit von interessanten Ausdrücken in KFP zu zeigen. Da
wir bereits argumentiert haben, dass sich Gleichungen von KFP nach KFPTS
übertragen lassen, werden wir die Unterschiede ignorieren.
Beispiel 5.7.1
⊥ ≤c (S bot)
Term S (S (S
bei Auswertung
Wir hatten bereits in einem Beispiel vermutet, dass die Kette
≤c (S (S bot)) ≤c . . . gegen den unendlich verschachtelten
(... ))) konvergiert. Insbesondere, dass alle Ausdrücke, die
einen solchen Term produzieren, kontextuell gleich sind.
Definition 5.7.2 Sei s1 ≤c s2 ≤c . . . eine aufsteigende Kette. Der Ausdruck s
ist eine kleinste obere Schranke (lub) dieser Kette, gdw.
∀i : si ≤c s und für alle r gilt: (∀i : si ≤c r) ⇒ s ≤c r.
Wir schreiben den lub manchmal mit einem Index, der den Lauf-Index der
aufsteigenden Kette kennzeichnen soll.
Der oben definiert Begriff des lub reicht für unsere Zwecke nicht aus. Man muss
noch etwas mehr definieren:
Definition 5.7.3 Sei s1 ≤c s2 ≤c . . . eine aufsteigende Kette. Der Ausdruck s
ist eine kontextuelle kleinste obere Schranke (club) dieser Kette, gdw.
∀C : C[s] = lubi (C[si ]). Wir notieren das als s = clubi (si ).
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
40
Die Begriffe lub und club sind nur eindeutig bis auf ∼c definiert.
Es gibt folgendes Kriterium für einen club einer aufsteigenden Kette:
Lemma 5.7.4 Sei s1 ≤c s2 ≤c . . . eine aufsteigende Kette, sei s ein Ausdruck.
Es gelte:
1. Für alle i : si ≤c s
2. Für alle Kontexte C gilt: C[s]⇓ ⇒ ∃i : C[si ]⇓
Dann gilt s = clubi (si ).
Beweis. Sei D ein Kontext. Wir wollen zeigen D[s] = lubi (D[si ]). Sei dazu C ein
weiterer Kontext und r ein Ausdruck mit ∀i : D[si ] ≤c r. Nach dem Kriterium
gilt: Wenn CD[s]⇓, dann existiert ein j mit CD[sj ]⇓, also wegen D[si ] ≤c r gilt
dann auch C[r]⇓. Es gilt also: CD[s]⇓ ⇒ C[r]⇓. Da das für alle Kontexte C gilt,
haben wir D[s] ≤c r gezeigt.
2
In Mason, Smith, Talcott gibt es ein Beispiel, das zeigt, dass nicht jeder lub auch
ein club ist. Dort wird in etwa formuliert: “Anwendung ist bzgl lub unstetig.“
Bemerkung 5.7.5 Nicht jede aufsteigende Kette hat einen club in der Menge
der Ausdrücke. Z.B. kann man für jeden unendlichen String a1 a2 a3 . . . über
{0, 1} eine aufsteigende Kette bilden: bot ≤c a1 : bot ≤c a1 : a2 : bot ≤c a1 : a2 :
a3 : bot ≤c . Man kann mit den bisher vorgestellten Mitteln nachweisen, dass
der club im Falle der Existenz eindeutig ist, gerade der “unendlichen Liste“ a1 :
a2 : . . . entspricht und für verschiedene Strings verschiedene clubs produziert.
Allerdings ist die Menge der möglichen Ausdrücke abzählbar, während die Menge
der unendlichen Strings überabzählbar ist. Damit muss es aufsteigende Ketten
geben, die keinen club besitzen.
Man kann auch konkrete aufsteigende Ketten angeben ohne club indem man
nichtberechenbare Funktionen verwendet.
Definition 5.7.6 Ein Abbildung φ, die Ausdrücke auf Ausdrücke abbildet heißt
stetig, gdw
1. s ≤c t ⇒ φ(s) ≤c φ(t)
2. Sei si aufsteigende Kette mit clubi si = s. Dann ist clubi (φ(si )) = φ(s)
Aussage 5.7.7 Jeder KFP-Ausdruck ist eine stetige Abbildung. D.h. die Abbildung φt mit:
φt s := t s
ist stetig.
Beweis. Folgt direkt aus den Definitionen, da (t .) ein Kontext ist.
2
41
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Definition 5.7.8 Zwei Ausdrücke s, t sind gleich bis auf Rechtstiefe n, wenn
jede Position, an der s, t verschiedene Markierung haben, einen Teilpfad der
Form 2
. . 2} mit m ≥ n hat. D.h. der Pfad von der Wurzel zur Position hat ein
| .{z
m
Teilstück, das mindestens n mal nach rechts geht. Diese Definition machen wir
modulo α-Gleichheit.
Beispiel 5.7.9 Die Ausdrücke f (. . . (f a) . . .) und f (. . . (f b) . . .) unterschei| {z }
| {z }
8
8
den sich an der Markierung a, b, die eine Rechtstiefe von 8 hat. Somit sind die
beiden Terme gleich bis auf Rechtstiefe 8.
Lemma 5.7.10
• Wenn zwei Ausdrücke s1 , s2 gleich sind bis auf Rechtstiefe n, und s1 →no
s01 , s2 →no s02 , dann sind s01 , s02 gleich bis auf Rechtstiefe n − 1.
• Wenn zwei Ausdrücke s1 , s2 gleich sind bis auf Rechtstiefe n, und
s1 →no,m s01 , s2 →no,m s02 , dann sind s01 , s02 gleich bis auf Rechtstiefe n−m.
• Wenn zwei Ausdrücke s1 , s2 gleich sind bis auf Rechtstiefe n ≥ 1, und s1
ist eine WHNF, dann ist auch s2 eine WHNF.
Beweis. Die jeweiligen Normalordnungsredexe von s1 , s2 sind ebenfalls gleich
bis auf Rechtstiefe n.
Eine Beta-Reduktion verändert folgendes: ((λx.t1 ) r1 ) → t1 [r1 /x]. Die Ausdrücke r1 , r2 sind gleich bis auf Rechtstiefe n − 1. Die Ausdrücke t1 [r1 /x] und
t2 [r2 /x] sind gleich bis auf Rechtstiefe n − 1; Jeder Pfad, der in Rechtstiefe
≤ n − 1; verschiedene Markierungen findet, kann nicht ganz in t1 , t2 sein, sondern geht durch eine ehemalige Position eines x, und folgt dann (einer Kopie
von) r1 bzw r2 .
Für eine Case-Reduktion gilt das analog, da die Änderungen ebenfalls Ersetzungen sind.
2
Satz 5.7.11 Sei t ein (evtl. offener) Ausdruck, und sei t0 := ⊥, t1 = (t ⊥), ti =
t(ti−1 ). Dann ist Y t ∼c clubi (ti ). Außerdem ist Y t kleinster Fixpunkt von t.
Beweis.
• ti ist eine aufsteigende Kette: Mit Induktion nach i kann man zeigen, dass
ti aufsteigend ist: ⊥ ≤c t1 . Dann mit Induktion: aus ti−1 ≤c ti folgt mit
der Monotonie: t ti−1 ≤c t ti , d.h. ti ≤c ti+1 .
• ∀i : ti ≤c Y t. Es gilt ⊥ = t0 ≤c Y t.
Induktionsschritt: Sei tn ≤c Y t. Dann gilt auch t tn ≤c t(Y t), da ≤c eine
Präkongruenz ist. Das bedeutet aber gerade tn+1 ≤c Y t.
42
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
• Y t ≤c clubi (ti ). Wir weisen das Kriterium in Lemma 5.7.4 nach.
Sei C ein Kontext, so dass C[Y t]⇓ und sei i die Anzahl der notwendigen
Normalordnungsreduktionen. Dann gilt C[ti ]⇓. Dazu ersetzen wir C[Y t]
durch C[ti+1 (Y t)]. Dieser Term hat nach Aussage 5.3.5 nicht mehr als i
Normalordnungsreduktionen zur WHNF. Er ist gleich zu C[ti+1 ] bis auf
Rechtstiefe i + 1. Nach Lemma 5.7.10 ist die WHNF von C[ti+1 (Y t)]
gleich bis auf Rechtstiefe 1 zu dem Ausdruck, der nach i Normalordnungsreduktionen aus C[ti+1 ] entsteht. Das bedeutet, C[ti+1 ] ist ebenfalls eine
WHNF.
Jetzt können wir schließen, dass Y t ∼c clubi (ti ).
• Y t ist kleinster Fixpunkt von t: Sei f ein Fixpunkt von t. Dann gilt
t f ∼c f . Mit Induktion kann man leicht zeigen, dass ti ≤c f :
Es ist bot ≤c f ; und wenn ti ≤c f , dann auch t ti ≤c t f , d.h. ti+1 ≤c f .
Aus der Eigenschaft der lubs folgt jetzt Y t ≤c f . D.h. Y t ist der kleinste
Fixpunkt von t (bzgl. ≤c ).
2
Bemerkung Die Ausdrücke ti entsprechen der rekursiv definierten Funktion t,
wobei das erste Argument der Name der Funktion ist, und man die Rekursion
nach i Schritten abbricht, und dann bot evaluiert. D.h. eine rekursive Funktion
wird approximiert durch Funktionen, die jeweils immer mehr rekursive Aufrufe
machen dürfen.
Beispiel 5.7.12 Sei S ein einstelliger Konstruktor. Jetzt können wir zeigen,
dass alle Ausdrücke t, die einen unendlichen Ausdruck (S (S (S ...))), mittels Reduktion erzeugen, kontextuell äquivalent sind. Genauer: Ausdrücke t mit
der Eigenschaft: ∀n.∃t0 : S(. . . (S t0 ) . . .).
| {z }
n
Der Satz zeigt, dass (Y S) ∼c club(si ), wobei si = (S(. . . (S ⊥))). Damit erhält
| {z }
i
man auch, dass (Y S) ≤c t, da si ≤c t für jede Approximation si gilt. Es fehlt
noch der Nachweis t ≤c (Y S). Den kann man analog zum Beweis von Satz
5.7.11 ausführen, indem man mit der Rechtstiefe argumentiert.
5.7.1
Nachweis von Gleichungen: Induktion
Definition 5.7.13 Ein (totales oder partielles) Prädikat P (·) ist eine Funktion
von Ausdrücken in die Menge {True, False}. Das Prädikat P (·) ist zulässig,
wenn für jede ≤c -aufsteigende Folge von Ausdrücken si gilt: Wenn ab einem
Index i0 gilt, dass für alle i ≥ i0 : P (si ) gilt, dann gilt auch P (clubi (si )).
Bei mehreren Argumenten ist P (x1 , . . . , xn ) zulässig, wenn für alle ≤c aufsteigenden Folgen si,j , j = 1, . . . , n folgendes gilt: Wenn es einen Index i0 gibt, so dass für alle i ≥ i0 P (si,1 , . . . , si,n ) gilt, dann auch
P (clubi (si,1 ), . . . , clubi (si,n )).
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
43
Will man eine Eigenschaft einer Definition oder eines Ausdrucks nachweisen, so
kann man dies mit einer speziellen Form der Induktion machen:
Fixpunktinduktion.
Sei P ein zulässiges Prädikat. Will man P (s) nachweisen, bzw.
∀x1 , . . . , xn .P (s), so kann man in vielen interessanten Fällen diesen
Nachweis führen, indem man eine Folge si angibt mit clubi si = s,
und P (si ) für alle i nachweist; d.h. P (si ) = True.
Da P zulässig ist, kann man daraus schließen, dass P (s) gilt.
Meist sind diese Prädikate Gleichungen zwischen Ausdrücken, wobei die Semantik der Gleichungen gerade ∼c ist.
Aussage 5.7.14 Folgendes sind zulässiges Prädikate.
• P (x) ≡ x ∼c t für festes t
• P (x, y) ≡ x ∼c y
• P (x) ≡ x ≤c t für festes t
• P (x) ≡ t ≤c x für festes t
• P (x, y) ≡ x ≤c y
• P (x, y) ≡ x ≤c y
• P (x) ≡ t <c x für festes t
Beweis. Wenn ab einem i0 für alle i ≥ i0 : si ∼c t gilt, dann offenbar auch
club(si ) = t.
Zum Nachweis der Behauptung x ≤c y ein zulässiges Pr7ädikat ist, seien si , ti
jeweils ≤c -aufsteigende Ketten mit club. Sei i0 ein Index, so dass für alle i ≥
i0 : si ≤c ti gilt. Dann gilt, dass si ≤c clubi (ti ). Aus der lub-Eigenschaft folgt
dann clubi (si ) ≤c clubi (ti ).
Ähnlich kann man die anderen Behauptungen beweisen
2 Beachte, dass
P (x) ≡ x <c x für festes t kein zulässiges Prädikat ist.
Man kann weitere zulässige Prädikate bilden durch Zusammensetzen von anderen zulässigen, indem man die logischen Operatoren ∧, ∨, verwendet. Allerdings
ist not nicht erlaubt.
Bemerkung 5.7.15 Das Prädikat “terminiert“ kann man in stetigen zusammengesetzten Prädikaten verwenden: z.B. das Prädikat
P (x) ⇔ (f x) terminiert
kann man in modifizierter Form durchaus verwenden, wenn man es als
(f x) >c bot darstellt. Denn wenn für alle i gilt: si >c bot, dann gilt auch
clubi (si ) >c bot. Damit das brauchbar ist, muss man evtl. den Anfang der Kette
weglassen.
Man kann auch das Prädikat “nichtterminiert verwenden, denn es lässt sich
”
durch P (x) := x ∼c ⊥ simulieren.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
44
Zusammenfassend darf man (mindestens) zum Zusammensetzen von stetigen
Prädikaten verwenden:
• All-Quantor; und, oder.
• ∼c , ≤c . x terminiert“.
”
• KFP-Ausdrücke
Der Nachweis, dass P (si ) für alle i gilt, wird oft in Form einer Induktion durchgeführt. Oft kann man die Form der möglichen Argumente rekursiv beschreiben,
wobei man alle WHNFs aufzählt.
Eine Standardform der Induktion ist der Nachweis von Eigenschaften für
Funktionen, die Listenargumente haben.
Induktionsschema zum Nachweis von P (.) für Listen:
• Zeige P (⊥).
• Zeige P (N il).
• Zeige: P (xs) ⇒ P (s : xs).
Dann kann man schließen: P (s) gilt für alle Listen s.
Definition 5.7.16 Sei eine Funktion zur Berechnung der “Partial-Liste“ einer
Liste definiert als
partlist 0 xs = bot
partlist (n+1) [] = []
partlist (n+1) (x:xs) = x: partlist n xs
Beispiel 5.7.17 Die Liste l = [1..] hat als Partiallisten bot, 1:bot, 1:2:bot.
Lemma 5.7.18 Sei eine (unendliche) Liste l 6= ⊥ gegeben und sei li :=
partlist i l. Dann ist li aufsteigend bzgl. ≤c und clubi (li ) = l.
Beweis. Wir nehmen an, dass sich die Liste l beliebig tief in Richtung “Tail“
auswerten lässt. Anderenfalls wäre der n-te Tail der Liste = bot, und die Behauptung leicht zu zeigen.
Es ist klar, dass li ≤c l. Um l = clubi li zu zeigen, müssen wir zeigen, dass für
jeden Kontext C[]: C[l]⇓ ⇒ ∃i : C[li ]⇓.
Wir verwenden dazu die Rechtstiefe. Sei C ein Kontext, so dass C[l]⇓. Sei i die
Anzahl der Normalordnungsschritte, um C[l] zu einer WHNF zu reduzieren. Wir
können annehmen, dass l so ausgewertet ist, dass l die Form a1 : a2 : . . . ai+1 :
ai+2 hat, wobei ai+2 auch unausgewertet sein kann. Sei li , definiert wie oben,
bereits so weit ausgewertet, dass li = a1 : a2 : . . . ai+1 : ⊥. Dann sind li und l
gleich bis auf Rechtstiefe i + 1. Lemma 5.7.10 zeigt dann, dass C[li ] ebenfalls
nach i Normalordnungsschritten zu einer WHNF wird. D.h. C[li ]⇓.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
45
Damit ist das Kriterium von Lemma 5.7.4 erfüllt und wir können schließen, dass
l = clubi (li ) ist.
2
Beispiel 5.7.19 Eine Ungleichung ist i.a. nicht zulässig:
Die Ungleichheit 6= ist nicht stetig: Sei l = repeat 1 und li = partlist i l.
Dann gilt li 6∼c l für alle i, aber clubi (li ) = l. Wäre 6= stetig, dann ergäbe sich
l 6∼c l, was Unsinn ist.
Das Induktionsschema für unendliche Listen funktioniert für das Prädikat
P (x) ≡ x 6= l. Offenbar gilt bot 6∼c l und [] 6∼c l. Nimmt man xs 6∼c l als
Induktionshypothese und will man x : xs 6∼c l zeigen, so gibt es zwei Fälle.
Wenn x 6∼c 1, dann ist das offenbar richtig. Wenn x ∼c 1, dann erfordert
1 : xs 6∼c l wegen l = 1 : l gerade xs 6∼c l. Das war die Induktionsbehauptung.
Begründung des Induktionsschemas Stetigkeit und club werden benutzt.
Da alle Listen sich als club von partiellen Listen darstellen lassen, kann man
für jede Liste l eine aufsteigende Folge li mittels partlist definieren, die diese
Liste als club hat.
• Ist die Liste endlich, dann zeigt das Induktionsschema die Aussage direkt
mit normaler Induktion
• Ist die Liste unendlich, dann zeigt das Induktionsschema die Aussage P (l)
für alle partiellen Listen li = partlist l mit normaler Induktion. Da das
Prädikat zulässig ist, darf man wegen l = clubi (li ) schließen, dass P (l)
gilt.
Bemerkung 5.7.20 Wir wiederholen nochmal die Definitionen einiger Funktionen für das folgende:
append xs ys = case xs of {[] -> ys; u:us -> u:(append us ys)
length xs
= case xs of {[] -> 0 ; y:ys -> 1 + (length ys)}
concat xs
= case xs of {[] -> []; u:us -> append u (concat us)}
reverse xs
= case xs of {[] -> []; u:us -> append (reverse us) (u:[])}
foldl f e xs = case xs of {[] -> e ; u:us -> foldl f (f e u) us}
foldr f e xs = case xs of {[] -> e ; u:us -> f u (foldr f e us)}
Beispiel 5.7.21 Zeige append [] xs ∼c xs.
Da append mit Fallunterscheidung über das erste Argument definiert ist, kann
man das sofort zu xs reduzieren.
Beispiel 5.7.22 Zeige append xs [] ∼c xs. Da append mit Fallunterscheidung über das erste Argument definiert ist, ist das nicht direkt mit Reduktion
zu zeigen. Wir benutzen das Induktionsschema für das erste Argument.
• append bot [] ∼c bot, da die Auswertung von bot durch die caseReduktion nicht terminiert.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
46
• append [] [] ∼c [] mittels Reduktion
• append (x:xs) [] reduziert zu x: (append xs []). Nach Induktionshypothese ist (append xs []) ∼c xs. Da ∼c sich über Konstruktoren ziehen
lässt, erhalten wir append (x:xs) [] ∼c x:xs.
Insgesamt erhalten wir jetzt mit dem Induktionsschema die Behauptung.
Hiermit können wir jetzt weitere Gleichheiten zeigen:
Beispiel 5.7.23 append
ist
assoziativ.
Es
gilt:
(append (append bot xs) ys)= bot = (append bot (append xs ys))
Es
gilt:
(append (append [] xs) ys)=
(append xs ys)
=
(append [] append(xs ys))
Es gilt: (append (append (z:zs) xs) ys)= (append (z : append zs xs) ys)
= z : (append (append zs xs) ys)
= z : (append zs (append xs ys)) (mit Induktionshypothese)
= (append (z:zs) (append xs ys)).
Danach können wir das Induktionsschema verwenden.
Definition 5.7.24 Ein Induktionsschema zum Nachweis von Eigenschaften
P endlicher Listen, wobei P nicht stetig zu sein braucht.
• Zeige P ([])
• Zeige P (xs) ⇒ P (x : xs).
Dann kann man P (xs) für alle endlichen Listen xs schließen.
Definition 5.7.25 Ein weiteres Induktionsschema zum Nachweis von (beliebigen) Eigenschaften P endlicher Listen kann man auf der Länge der Listen
aufbauen.
• Zeige: P ([])
• Zeige:
P (xs) für alle Listen der Länge n
⇒ P (ys) für alle Listen der Länge n + 1
Dann kann man P (xs) für alle endlichen Listen schließen.
Beispiel 5.7.26 Wir vergleichen drei Längenfunktionen:
length1 xs = case xs of [] -> 0; y:ys -> 1+(length ys)
length2 xs = foldr (\x _ -> 1+x) 0 xs
length3 xs = foldl’ (\x _ -> 1+x) 0 xs
47
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Behauptung: die ersten beiden Definition sind gleich:
length1 bot
= bot
length2 bot
= foldr (\x _ -> 1+x) 0 bot = bot
length1 []
= 0
length2 []
= foldr (\x _ -> 1+x) 0 [] = 0
length1 (u:us) = 1+(length us)
length2 (u:us) = foldr (\x _ -> 1+x) 0 (u:us)
= 1 + (foldr (\x _ -> 1+x) 0 us
Behauptung: die dritte ist ebenfalls gleich der ersten:
length3 bot
= bot
length3 []
= 0
length3 (u:us) = foldl’ (\x _ -> 1+x) 0 (u:us)
= strict (foldl (\x _ -> 1+x)) (1 + 0) us
= strict (foldl (\x _ -> 1+x)) 1 us
Hier braucht man einen Gleichheitsbeweis mit Induktion nach der Länge der
Listen für endliche Listen und eine extra Argumentation, dass sich bot für unendliche Listen ergibt.
Beispiel 5.7.27 Für alle endlichen Listen gilt:
• length(xs ++ ys) = (length xs) + (length ys)
Das zeigt man mit Induktion nach der Länge der Listen:
Wenn xs = [], dann gilt die Behauptung.
length((x:xs) ++ ys) reduziert zu length(x:(xs ++ ys); dies reduziert zu 1+ length(xs ++ ys). Mit Induktion ist das gleich
1+ length(xs) + length ys). Da length((x:xs) zu 1 + length xs
reduziert und + assoziativ ist, erhält man die Behauptung.
• length (reverse xs)= length xs
Für Listen der Länge 0 gilt die Behauptung.
Für Listen der Länge > 0 erhält man:
length (reverse (x:xs))→ length (reverse xs) ++ [x])
Mit obiger Gleichung erhält man: = length (reverse xs) + 1)
Induktion nach der Länge ergibt: = (length xs) + 1) = (length x:xs)
• reverse (reverse xs) = xs.
Das ist komplizierter: Man benötigt als Zwischenbehauptungen: ++ ist assoziativ und reverse(xs ++ ys) = (reverse ys) ++ (reverse xs). Die
sind jeweils mit Induktion zu zeigen.
Danach kann man für nichtleere Listen schließen:
reverse (reverse (x:xs)
= reverse ((reverse xs) ++ [x])
= reverse [x] ++ (reverse(reverse xs))
= [x] ++ xs = x:xs.
Aber für unendliche Listen ist die letzte Aussage
reverse (repeat 1) = bot und reverse bot = bot.
falsch,
denn
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
48
Bemerkung 5.7.28 P (x) ≡ reverse (reverse xs) = xs ist ein Beispiel für
ein stetiges Prädikat, das sowohl für alle endlichen Listen als auch für xs = bot
gilt, aber nicht für alle Listen. Der Beweis, dass reverse (reverse xs) = xs
für alle endlichen Listen gilt wurde nicht nach dem Schema für alle Listen,
sondern nach der Länge der Listen geführt.
Induktionsschema für alle Ausdrücke: Eine Erweiterung der Induktion
kann zum Nachweis von stetigen Eigenschaften P (·) benutzt werden, wobei alle
Ausdrücke als Argumente zulässig sind. Insbesondere kann dieses Schema zum
Nachweis von Gleichungen benutzt werden.
• Zeige P (⊥).
• Zeige P (λx.r).
• Zeige für alle Konstruktoren c: P (x1 )∧. . .∧P (xn ) ⇒ P (c x1 . . . xn )) wobei
n = ar(c).
Dann kann man schließen: P (s) gilt für alle Ausdrücke s.
Bemerkung 5.7.29 Das Schema scheint nicht die Verallgemeinerung des Induktionsschemas für alle Listen zu sein. Aber es ist doch konsistent dazu, denn
der Induktionsschritt P (x) ∧ P (xs) ⇒ P (x : xs) passt trotzdem, denn im Falle der Listen zeigt man den Schritt für alle x. Im Falle der Betrachtung aller
Ausdrücke zeigt man ja gerade P (x) für alle Ausdrücke x.
Beispiel 5.7.30 Am Beispiel der Assoziativität von append kann man zeigen,
dass die Assoziativitätsgleichung
append(append x y) z = append(x (append y z))
für alle Ausdrücke x, y, z gilt.
• Für x = (λx.r) erhält man auf beiden Seiten ∼c bot, da ein Typfehler
auftritt.
• Für x = c x1 . . . xn erhält man ebenfalls auf beiden Seiten ∼c bot, da ein
Typfehler auftritt.
• die anderen Fälle wurden bereits gezeigt.
Bemerkung 5.7.31 Diese Gleichheiten kann man verwenden, um Programme
in korrekter Weise zu verändern. Z.B. darf man die Assoziativität von append
verwenden, um beliebige append-Ausdrücke umzuklammern. Dies wäre falsch,
wenn die Assoziativität nur für endliche Listen gelten würde.
Wenn klar ist, dass an einer Stelle im Programm nur endliche Listen verwendet
werden können, darf man dort auch die Gleichheiten verwenden, die für endliche
Listen gelten. Z.B. wenn nach der Abfrage (liste-endlich l) die Variable l
verwendet wird, dann kann man das wie eine endliche Liste behandeln, denn im
Falle einer unendlichen Liste ergibt sich bot bereits bei der Abfrage.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
49
Übungsaufgabe 5.7.32 Welche der folgenden Gleichungen gelten für endliche, welche für unendliche Listen?
• xs ++ [] = xs?
• concat (xs ++ ys) = (concat xs) ++ (concat ys)?
• (take n xs) ++ (drop n xs) = xs?
• Für Listen xs, ys gleicher Länge: unzip (zip xs ys) = (xs,ys)? mit
den zwei Definitionsvarianten
unzip_1
unzip_2
= foldr (\(a,b) paar ->
(a:(fst paar), b:(snd paar))) ([], [])
= foldr (\(a,b) (as,bs) -> (a:as, b:bs)) ([], [])
• map f (map g xs) = map (f.g) xs (Verschmelzungsgesetz für map)
5.7.2
Gleichungen für (un-)endliche Listen: map, concat,
foldr, foldl
Das Buch von Richard Bird, 98 enthält einige Gleichungen und Gesetze zum
Zusammenhang zwischen foldr, foldl, map, concat und weiteren Funktionen. Diese Gleichungen können für verschiedene Nachweise der Korrektheit und
für Optimierungen verwendet werden. Im folgenden einige dieser Gleichungen:
Diese praktische Verwendung dieser Gleichungen liegt in der Optimierung zur
Compilezeit, wobei man diese Gleichungen als Programmtransformationen verwendet. Allerdings gibt es viele Gleichungen, die man alle einzeln von Hand
verifizieren müsste, bevor man sie in einen Compiler einbaut. Die automatische
Generierung ist zT möglich, aber auch sehr aufwändig. Als weitere Information
über die Verwendbarkeit der Gleichungen braucht man noch Umgebungsinformation: Z.B.: ist die lokale Funktion f strikt? Ist ein Ausdruck t immer eine
endliche Liste? Was man natürlich auch noch wissen muss, ist ob eine Regel gut
ist, d.h. wirklich etwas verbessert.
Global muss man wissen, ob die Anwendung der Transformationen im Compiler terminiert, und ob die Regeln die Tendenz haben, das Programm (evtl.
überproportional) zu vergrößern.
Sei f die binäre Verknüpfung eines Monoids mit dem Einselement e. D.h. als
Funktion sei f eine zweistellige Funktion, e ein Ausdruck, so dass modulo ∼c
die Monoidgleichungen gelten:
f x (f y z) = f (f x y) z Assoziativität
f xe
= x
Einselement
f ex
= x
Einselement
f bot x
= bot
Nullelement
f x bot
= bot
Nullelement
Oft ist bot ein Nullelement des Monoids.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
50
Erstes Dualitätsgesetz für FOLD
Für alle endliche Listen xs mit Elementen aus dem Monoid (ohne Nullelement)
gilt:
foldr f e xs = foldl f e xs
Begründung Die Definitionen sind:
foldr
:: (a -> b -> b) -> b -> [a] -> b
foldr f z []
= z
foldr f z (x:xs) = f x (foldr f z xs)
foldl
:: (a -> b -> a) -> a -> [b] -> a
foldl f z []
= z
foldl f z (x:xs) = foldl f (f z x) xs
Wir verwenden die Übersetzung, die zuerst ein case auf die Liste macht (s.o.)
Mit Induktionsschema Nil; x:xs
Für die leere Liste gilt: foldr f e [] = e = foldl f e []
Für eine Liste [x] gilt: foldr f e [x] = f x e = x = f e x = foldl f e [x]
Für eine Liste x1:x2:xs gilt: foldr f e (x1:x2:xs) = f x1 (f x2 (foldr f e xs)).
Assoziativität
des
Operators
f
ergibt:
das
ist
gleich
f (f x1 x2) (foldr f e xs)) = foldr f e (f x1 x2) : xs))
Dieselbe Rechnung für foldl ergibt:
foldl f e (x1:x2:xs) = foldl f (f (f e x1) x2) xs. Assoziativität
ergibt foldl f (f e (f x1 x2)) xs = foldl f e (f x1 x2):xs. Jetzt
kann man Induktion über die Länge machen.
Bemerkung 5.7.33 Offenbar gilt, dass die Gleichung für unendliche Listen
falsch sein kann, denn foldl ergibt immer bot für unendliche Listen, während
foldr auch Ergebnisse liefern kann. Der bot-Fall gilt, aber die versteckte Annahme in der Induktion ist der Fall [x], der gilt, aber man muss noch den Fall
x:bot untersuchen. In diesem Falle erhält man: einerseits f x bot, andererseits bot.
D.h. das erste Dualitätsgesetz für fold gilt für alle Listen, wenn der Operator f
strikt im zweiten Argument ist: wenn f x bot = bot für alle x:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
51
Zweites Dualitätsgesetz für FOLD
Seien f,g Operatoren, e eine Einheit, so dass
• x 0 f0 (y 0 g0 z) = (x 0 f0 y) 0 g0 z
• x0 f0 e = e0 g0 x
Dann gilt für alle endlichen Listen xs:
foldr f e xs = foldl g e xs
Zunächst muss man folgende Zwischenbehauptung für alle endlichen Listen xs
zeigen:
x ’f’ (foldl g y xs) = foldl g (x ’f’ y) xs
Beweis:
Fall []: Beide Seiten reduzieren auf x ’f’ y
Fall (z:zs): x ’f’ (foldl g y (z:zs)) = x ’f’ (foldl g (g y z) zs)
= (Induktion) foldl g (x ’f’ (y ’g’ z)) zs)
= (Voraussetzung) foldl g ((x ’f’ y) ’g’ z) zs)
Für die andere Seite erhält man:
foldl g (x ’f’ y) (z:zs) = foldl g ((x ’f’ y) ’g’ z) zs
Die Zwischenbehauptung ist somit gezeigt.
Nachweis der eigentlichen Behauptung mit Induktion:
• Fall []: foldr f e [] = e = foldl g e []
• Fall x:xs: foldr f e (x:xs) = f x (foldr f e xs)
= (Induktion) f x (foldl g e xs)
Die
Zwischenbehauptung
zeigt
jetzt
mit
y
=
e
die
Gleichung
f x (foldl g e xs)
=
foldl g (f x e) xs) = foldl g (g e x) xs).
Andererseits:
foldl g e (x:xs) = foldl g (g e x) xs und damit gilt die Behauptung des Dualitätssatzes.
Beispiel 5.7.34
Mit dem zweiten Dualitätssatz für fold kann man die Gleichheit der beiden reverse-Definition zeigen.
• reverse1 = foldr snoc [] where snoc x xs = xs ++ [x]
Das entspricht der Definition
reverse1 [] = []
reverse1 (x:xs) = (reverse1 xs) ++ [x]
was man leicht durch Induktion nachweisen kann (für alle Listen)
• reverse2 = foldl cons [] where cons xs x = x:xs
Das entspricht der Definition
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
52
reverse2 xs
= reverse2r xs []
reverse2r []
ys = ys
reverse2r (x:xs) ys = reverse2r xs (x:ys)
Um zu zeigen, dass die Ergebnisse gleich sind für endliche Listen, muss man nur
nachweisen, dass x ’snoc’ (y ’cons’ z) = (x ’snoc’ y) ’cons’ z D.h.
dass (z:y) ++ [x] = z:(y ++ [x])]. Das kann man mit einem Reduktionsschritt von append zeigen.
Man hat noch nachzuweisen, dass auch x ’snoc’ [] = [] ’cons’ x. Beides
reduziert zu [x].
Drittes Dualitätsgesetz für FOLD
Es gilt für alle endlichen Listen xs:
foldr f e xs = foldl (flip f) e (reverse xs)
Es gilt für alle xs (insbesondere alle Listen):
foldl f e xs = foldr (flip f) e (reverse xs)
Die Gleichungen sind konsistent mit der Intuition:
foldr + 0 [1,2,3,4]
ergibt
1 + (2 + (3 + (4 + 0))),
während
foldl (flip +) 0 [4,3,2,1] ergibt (((0 +’ 4) +’ 3) +’ 2) +’ 1 mit
flip + = +’.
Nachweis
Zunächst eine andere Gleichung, die für alle Listen gilt, den Beweis kann man
als Übungsaufgabe durchführen.
foldr g e (xs++ys) = foldr g (foldr g e ys) xs
Jetzt der Nachweis der zweiten Gleichung:
• Fall ungetypt. D.h. xs ist Abstraktion oder hat falschen Topkonstruktor:
Beide Seiten sind äquivalent zu bot.
• Fall bot: Beide Seiten reduzieren zu bot.
• Fall []: Beide Seiten reduzieren zu e.
• Fall (x:xs).
foldl f e (x:xs) → foldl f (f e x) xs
foldr (flip f) e (reverse (x:xs))
→ foldr (flip f) e ((reverse xs) ++ [x]))
= (ZWbeh) foldr (flip f) (foldr (flip f) e [x]) (reverse xs)
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
53
= foldr (flip f) (f e x) (reverse xs)
= (Induktion) foldl f (f e x) xs
Damit sind die beiden Seiten gleich, und die Behauptung gilt für alle
Listen.
Übungsaufgabe 5.7.35 Ist foldr (:) [] = Id ?
Beispiel 5.7.36 Den ersten Teil des Dualitätssatzes kann man verwenden, um
reverse (reverse xs) = xs für endliche Listen zu zeigen:
foldr (:) [] xs = foldl (flip (:)) [] (reverse xs)
Die linke Seite wird zu xs, da xs endliche Liste. Auf der rechten Seite ist foldl (flip (:)) [] = reverse, deshalb ist die rechte Seite gerade
reverse (reverse xs).
Übungsaufgabe 5.7.37 Warum kann man mit der zweiten Aussage des Dualitätssatzes nicht reverse (reverse xs) = xs für alle (auch unendliche Listen) zeigen?
Verschmelzungs-Gesetze für Fold
Der Sinn und Zweck dieser Gesetze ist die Optimierung der Listenverarbeitung,
die Zwischenlisten erzeugt und gleich wieder verwirft. Diese Zwischenlisten
sollen vermieden werden durch automatische Umstellung der Verarbeitung.
FOLD-MAP Verschmelzung
foldr f a . map g = foldr (f.g) a
Das ist einfach nachzuweisen:
• Fall bot: ergibt bot.
• Fall []: ergibt a.
• Fall x:xs:
Linke Seite: foldr f a . map g (x:xs)
= foldr f a ((g x) : map g xs) = f (g x) (foldr f a (map g xs))
Die rechte Seite ergibt:
foldr (f.g) a (x:xs) = f (g x) (foldr (f.g) a xs).
Anwenden der Induktionshypothese zeigt die Behauptung für den Fall
x:xs.
• Danach kann man das Induktionsschema für Listen anwenden.
Die Optimierung besteht darin, die Verwendung von map zu eliminieren, denn
die Ergebnisliste wird von map aufgebaut und von foldr gleich wieder abgebaut.
Das Verschmelzungsgesetz für foldl und map ist:
54
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
foldl f a . map g
=
foldl (\x,y -> f x (g y))
a
Beispiel 5.7.38 Zu foldl-map-Verschmelzung. Das Ergebnis bei Anwendung
auf eine Liste [x1,x2,x3] ist gerade ((a + (g x1)) + (g x2)) + (g x3)
Nachweis
• Fall bot: Das Ergebnis ist jeweils bot.
• Fall []: Das Ergebnis ist jeweils [].
• Fall x:xs
foldl f a . map g (x:xs)
= foldl f a ((g x): map g (x:xs))
= foldl f (f a (g x)) (map g xs)
Auf der rechten Seite ergibt sich: (foldl (\x,y -> f x (g y)) a (x:xs)
= foldl (f.g) (f a (g x)) xs.
Anwenden der Induktionshypothese liefert die Behauptung in diesem
Fall.
• Danach kann man das Induktionsschema für Listen anwenden.
FOLD-CONCAT Verschmelzung
foldr f a . concat = foldr (flip (foldr f)) a
Zum Nachweis dieser Gleichung benutzt man das Induktionsschema für Listen:
Die Fälle bot, [] sind einfach. Der Nachweis für (xs:xss) hat drei Unterfälle
Fall []:xss:
foldr f a (concat ([]:xss)) = foldr f a (concat (xss))
foldr (flip (foldr f)) a ([]:xss))
= ((flip (foldr f)) [] (foldr (flip (foldr f)) a (xss)))
= foldr f (foldr (flip (foldr f)) a xss) []
= foldr (flip (foldr f)) a xss
Fall bot:xss:
(foldr f a . concat) (bot:xss)
= foldr f a (concat (bot:xss))
= foldr f a (foldr (++) [] (bot:xss))
= foldr f a (bot ++ foldr (++) [] (xss))
= foldr f a bot
= bot
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
55
foldr (flip (foldr f)) a (bot:xs)
= (flip (foldr f)) bot (foldr (flip (foldr f)) a xs)
= foldr f (foldr (flip (foldr f)) a xs) bot
= bot
Fall (x:xs):xss:
foldr f a (concat (x:xs):xss) = foldr f a (x: concat (xs:xss))
= f x (foldr f a (concat (xs:xss)))
foldr (flip (foldr f)) a ((x:xs):xss)
= (flip (foldr f)) (x:xs) (foldr (flip (foldr f)) a xss)
= foldr f (foldr (flip (foldr f)) a xss) (x:xs)
Dazu zunächst noch die folgende Rechnung:
= f x (foldr f (foldr (flip (foldr f)) a xss) xs)
foldr (flip (foldr f)) a (xs:xss)
= (flip (foldr f)) xs (foldr (flip (foldr f)) a xss)
= (foldr f (foldr (flip (foldr f)) a xss) xs)
Insgesamt hat man das Gesetz nachgewiesen.
Übungsaufgabe 5.7.39 Wie sieht das Gesetz für foldl aus?
foldl f a . concat = ???
FOLD-FILTER Verschmelzung
foldr f a (filter p xs) = foldr (\x y -> if (p x) then f x y else y) a xs
foldl f a (filter p xs) = foldl (\x y -> if (p y) then f x y else x) a xs
Den Nachweis kann man analog wie für die anderen Regeln führen.
Fold auf Bäumen: Verschmelzungsgesetze
Die Faltung auf binären Bäumen kann man wie folgt definieren:
data Binbaum a = Blatt
a | Knoten (Binbaum a) (Binbaum a)
-- foldr Entsprechung
kann terminieren f"ur unendliche B"aume
foldrbt :: (a -> b -> b) -> b -> Binbaum a -> b
foldrbt op a (Blatt x) = op x a
foldrbt op a (Knoten x y) = (foldrbt op (foldrbt op a y) x)
-- foldl Entsprechung
terminiert nie f"ur unendliche B"aume
foldlbt :: (a -> b -> a) -> a -> Binbaum b -> a
foldlbt op a (Blatt x) = op a x
foldlbt op a (Knoten x y) = (foldlbt op (foldlbt op a x) y)
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
56
-- entspricht foldl mit Platzoptimierung:
foldlbtstr op a (Blatt x) = op a x
foldlbtstr op a (Knoten x y) = (strict (foldlbtstr op) (foldlbtstr op a x) y)
-- entspricht map:
mapbt f (Blatt x) = Blatt (f x)
mapbt f (Knoten bl br) =
Knoten (mapbt f bl)
(mapbt f br)
-- reverse-Entsprechung
reversebt (Blatt x) = (Blatt x)
reversebt (Knoten x y) = Knoten (reversebt y) (reversebt x)
Ein Verschmelzungsgesetz für foldrbt ist die Vermeidung des Baumaufbaus
nach dem map über den Baum:
foldrbt op a . mapbt f
= foldrbt (\x y -> (op (f x) y)) a
Der Nachweis des Gesetzes für alle binären Bäume, wobei man ein auf binäre
Bäume angepasstes Induktionsschema verwenden muss.
• Für bot ist das Ergebnis jeweils bot.
• Für Blatt x ist das Ergebnis jeweils op (f x) a.
• Für Knoten x y ergibt sich links: foldrbt op a (mapbt f (Knoten x y))
→ foldrbt op a (Knoten (mapbt f x) (map f y))
foldrbt op (foldrbt op a (mapbt f y)) (mapbt f x)
Es ergibt sich rechts:
foldrbt (\x y -> (op (f x) y)) a (Knoten x y)
→ foldrbt (\x y -> (op (f x) y)) (foldrbt (\x y -> (op (f x) y)) a y)
Jetzt kann man die Induktionshypothese anwenden.
Das Verschmelzungsgesetz für foldlbt ist:
foldlbt op a . mapbt f
= foldlbt (\x y -> (op x (f y))) a
Bei Kombinationen zwischen Listen und Bäumen kann man ebenfalls solche
Verschmelzungen durchführen.
randbt = foldbt (:) []
Eine Verschmelzungsregel ist:
map f . randbt = foldrbt (\x y -> (f x):y) []
Bemerkung 5.7.40 Die Anzahl der Reduktionen der “optimierten ist in Hugs
”
immer um einige Prozent gr/ößer, so dass eine Verbesserung nicht beobachtbar
ist. Eine echte Optimierung ist immer der übergang zu foldlbtstr, falls das
möglich ist und korrekt ist (siehe unten).
x
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
57
Bäume mit beliebig vielen Töchterknoten
Im folgenden der Quellcode mit Testfunktionen:
data Gbaum a = Gblatt
a | Gknoten [Gbaum a]
-- foldr Entsprechung
kann terminieren f"ur unendliche B"aume
foldrgt :: (a -> b -> b) -> b -> Gbaum a -> b
foldrgt op a (Gblatt x) = op x a
foldrgt op a (Gknoten []) = a
foldrgt op a (Gknoten (x:xs)) = (foldrgt op (foldrgt op a (Gknoten xs)) x)
testrgt = foldrgt (:) [] (Gknoten [Gknoten [Gblatt 1, Gblatt 2], Gblatt 3])
testrgt2 = foldrgt (:) [] (Gknoten (map Gblatt [1..]))
-- foldl Entsprechung
kann terminieren f"ur unendliche B"aume
foldlgt :: (a -> b -> a) -> a -> Gbaum b -> a
foldlgt op a (Gblatt x) = op a x
foldlgt op a (Gknoten []) = a
foldlgt op a (Gknoten (x:xs)) = (foldlgt op (foldlgt op a x) (Gknoten xs))
testlgt = foldlgt (flip (:)) [] (Gknoten [Gknoten [Gblatt 1, Gblatt 2], Gblatt 3])
testlgt2 = foldlgt (flip (:)) [] (Gknoten (map Gblatt [1..]))
-- testlgt2 = bot
-- entspricht foldl mit Platzoptimierung:
foldlgtstr op a (Gblatt x) = op a x
foldlgtstr op a (Gknoten []) = a
foldlgtstr op a (Gknoten (x:xs)) =
(strict (foldlgtstr op) (foldlgtstr op a x) (Gknoten xs))
testlgtstr = foldlgtstr (flip (:)) []
(Gknoten [Gknoten [Gblatt 1, Gblatt 2], Gblatt 3])
testlgtstr2 = foldlgtstr (flip (:)) []
(Gknoten (map Gblatt (take 10000 [1..])))
mapgt f (Gblatt a) = Gblatt (f a)
mapgt f (Gknoten (xs)) = Gknoten (map (mapgt f) xs)
testmapgt = mapgt quadrat (Gknoten [Gknoten [Gblatt 1, Gblatt 2], Gblatt 3])
-- Reverse
reversegt (Gblatt x) = (Gblatt x)
reversegt (Gknoten b) = Gknoten (foldl (\x y -> (reversegt y) : x) [] b)
Die Verallgemeinerung des fold-map Verschmelzungsgesetzes ist:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
foldrgt op a . mapgt f
58
= foldrgt (\x y -> (op (f x) y)) a
Dies ergibt für Gblatt a gerade Gblatt (f a).
Für (Gknoten []): Beide Seiten ergeben a.
Für (Gknoten (x:xs)):
foldrgt op a (mapgt f (Gknoten (x:xs)))
→
foldrgt op a (Gknoten (map (mapgt f) (x:xs)))
→
foldrgt op a (Gknoten ((mapgt f x) : map (mapgt f) xs))
→
(foldrgt op (foldrgt op a (Gknoten (map (mapgt f) xs))) (mapgt f x)).
Die rechte Seite ergibt: foldrgt (\x y -> (op (f x) y)) a (Gknoten (x:xs))
→
foldrgt (\x y -> (op (f x) y)) a (Gknoten (x:xs))
→
foldrgt (\x y -> (op (f x) y))
(foldrgt (\x y -> (op (f x) y)) a (Gknoten xs))
Jetzt kann man ein für Gbaum angepasstes, verallgemeinertes Induktionsschema
verwenden.
5.7.3
Nachweis von Aussagen in typisierten funktionalen
Programmiersprachen
In diesem Paragraphen nehmen wir an, dass alle Ausdrücke und Unterausdrücke
polymorph getypt sind. Die Grundausdrücke vom Grundtyp2 α bezeichnen wir
mit Exp(α). Wir nehmen an, dass die Typisierung die folgenden Eigenschaften
erfüllt:
• Wenn t ein geschlossener Ausdruck ist, α ein Grundtyp, t :: α, und t → t0 ,
dann auch t0 :: α.
• Wenn t :: τ = (c α1 . . . αn ), wobei c ein Typkonstruktor ist, aber nicht
gleich →, dann gilt einer der Fälle:
– t hat keine WHNF, oder
– t hat eine WHNF der Form ci t1 . . . tm , und ci ist ein Datenkonstruktor zum Typ c, und der Typ von tj ist eindeutig bestimmbar. D.h. es
gibt eine Funktion comptyp(τ, ci , j), die einen Grundtyp liefert, und
es gilt tj :: comptyp(τ, ci , j).
• Wenn t geschlossen ist, und t :: α1 → α2 , dann hat t entweder keine
WHNF oder eine FWHNF.
Jetzt können wir das Induktionsschema angeben:
Induktionsschema für stetige Prädikate unter Typisierung:
• Zeige P (⊥).
2 ein
Grundtyp ist ein Typ ohne Variablen
x
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
59
• Wenn α = τ1 → τ2 ein Funktionstyp, dann zeige P (λx : τ1 .r :: τ2 ).
• Wenn α = (c τ1 . . . τn ): Zeige für alle Konstruktoren cj , die zum Typ c
gehören:
P (x1 :: comptyp(α, cj , 1)) ∧ . . . ∧ P (x1 :: comptyp(α, cj , ar(cj ))
⇒ P (cj x1 . . . xar(cj ) ))
Dann kann man schließen: P (s) gilt für alle Typen α und alle Ausdrücke s : α.
Die Begründung für dieses Induktionsschema ist analog wie für unendliche Listen bzw. für alle Ausdrücke: Für Ausdrücke, die aus endlich vielen Konstruktoren, bot und FWHNFs aufgebaut sind, kann man die Eigenschaft dann mit
Induktion begründen. Für die Ausdrücke, die man nur “unendlich“ darstellen
kann mittels Konstruktoren, bot und FWHNFs, verwendet man Stetigkeit. Die
Aussage für alle Ausdrücke erhält man über die clubs von Partialausdrücken
analog zu Partiallisten. Der entsprechende Beweis ist analog zu Lemma 5.7.18.
5.8
Änderung
strikt
der
Auswertungsreihenfolge:
Die gezielte Änderung der Auswertungsreihenfolge ist eine Optimierung, die von
allen Kompilern für nicht-strikte FPS verwendet wird. Auch hier ist wesentlich,
dass die Semantik, sprich die kontextuelle Äquivalenz, erhalten bleibt.
Beispiel 5.8.1 Betrachte nochmal die Funktionen
NT x
K x y
=
=
NT x
x
Ändert man die Auswertungsreihenfolge so ab, dass die Funktion K zunächst das
erste, dann das zweite Argument auswertet und dann das Resultat berechnet,
dann terminiert die Auswertung von K 1 bot nicht mehr, obwohl (K 1 bot)⇓.
D.h. offenbar ist die Semantik verändert.
Ändert man die Auswertungsreihenfolge so ab, dass der Superkombinator K bei
jeder Auswertung zunächst das erste Argument auswertet und dann das Resultat
berechnet, dann ist der Fehler behoben. Eine erste Vermutung ist, dass diese
Änderung richtig ist, aber der Nachweis fehlt.
Im folgenden werden wir den Nachweis erbringen, dass diese Veränderung die
Semantik erhält.
Zuerst brauchen wir noch eine Betrachtung zur Normalordnungsreduktion.
Lemma 5.8.2 Wenn ein geschlossener Ausdruck t eine Normalordnungsredukno,∗
tion t −→ R[s1 ] hat, wobei R ein Reduktionskontext ist, dann wird die Normalordnungsreduktion den Unterausdruck s1 zunächst solange reduzieren, bis er
selbst in WHNF ist. Es kann folgendes passieren:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
no,∗
no
no
no,∗
no
no
60
no
• t −→ R[s1 ] −→ R[s2 ] −→ . . . −→ R[sn ], wobei sn in WHNF ist, und danach
wird evtl. auch R durch die Normalordnungsreduktion verändert
• t −→ R[s1 ] −→ R[s2 ] −→ . . . und der Ausdruck s1 wird niemals zur WHNF
reduziert.
Zunächst definieren wir Striktheit.
Definition 5.8.3 Ein KFP-Ausdruck f ist strikt, gdw. (f bot)⇑. Hierbei ist bot
ein nichtterminierender Ausdruck.
Eine Erweiterung ist die Definition der Striktheit in einem Argument:
Definition 5.8.4 Die (geschlossene) n-stellige Funktion f ist strikt im i-ten
Argument, gdw. für alle geschlossenen Ausdrücke tj : (f t1 . . . ti−1 bot ti . . . tn ).
Man könnte auch eine (i, n)-Striktheit definieren, wenn man die Sprache mit Superkombinatoren betrachtet: f ist (i, n)-strikt gdw. für alle geschlossenen Ausdrücke tj : (f t1 . . . ti−1 bot ti+1 . . . tn )⇑.
Definition 5.8.5 Sei f strikt. Dann sei die f -strikte Auswertung diejenige, die
bei Auswertung von t, falls ein Unterterms f s auszuwerten ist, zuerst s mit fstrikter Auswertung in WHNF überführt, und ansonsten wie die Normalordnung
reduziert.
Lemma 5.8.6 Wenn f strikt ist, und (f s)⇓, dann gilt s⇓, und die Anzahl der
Normalordnungsreduktionen von s ist echt kleiner als die Anzahl der Normalordnungsreduktionen von f s
Beweis. Es ist klar, dass s⇓. Was noch fehlt, ist die Aussage zur Anzahl. Markiert
man den Term s und alle während der Reduktion entstehenden Kopien, dann
gibt es zwei Fälle:
1. Die Normalordnungs-Reduktion von f s reduziert einen der markierten Unterterme s während der Reduktion ebenfalls als Subprozedur zur
WHNF. Dann gilt die Behauptung.
2. Die Normalordnungs-Reduktion reduziert niemals die markierten Kopien
von s. Dann kann man aber s durch bot ersetzen und die NormalordnungsReduktion terminiert immer noch. Das ist ein Widerspruch zur Annahme
der Striktheit von f .
2
Beispiel 5.8.7 Beachte: es gilt nicht dass aus s⇓ , f strikt auch folgt, dass
(f s)⇓: Sei f x = bot Dann ist f strikt, und sei s = True. Dies ist eine
WHNF. Damit terminiert (f s) nicht, obwohl s terminiert.
61
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Definition 5.8.8 Eine Auswertungsstrategie S ist eine Relation t →S t0 auf
Termen. Diese Relation muss effektiv sein, d.h es muss einen Algorithmus geben,
der bei Eingabe des Terms t das Redukt t0 ausgibt, oder sagt: keine Reduktion
möglich, oder: ist eine WHNF.
∗
Eine Strategie S ist korrekt, gdw. für alle Terme t: t⇓ ⇔ t −→S t0 , wobei t0 eine
WHNF ist.
Satz 5.8.9 Die f-strikte Auswertung ist eine korrekt Strategie
Beweis. Die eine Richtung ist klar, denn die f-strikte Auswertung ist mit Reduktionen simulierbar. Danach kann man den Satz 5.3.11 anwenden.
Jetzt zeigen wir: Wenn t⇓, dann terminiert auch die f-strikte Auswertung von t
mit einer WHNF. Angenommen, das ist falsch. Dann können wir einen Ausdruck
t finden, so dass die Normalordnung terminiert, aber nicht die f-strikte Auswertung. Wir wählen die Anzahl der Normalordnungsreduktionen von t minimal
und als zweiten Parameter die Größe von t minimal.
Falls der Normalordnungsredex in t nicht von der Form f s ist, stimmen beide
Reduktionen überein. Damit ist das Beispiel nicht minimal.
Auch wenn der Term bereits in WHNF ist, terminieren beide Reduktionen.
Also brauchen wir nur noch den Fall zu betrachten, dass ist der Normalordnungsredex von der Form f s ist und t = R[(f s)], wobei R ein Reduktionskontext ist.
Da f strikt ist, und (f s)⇓ gelten muss, gilt auch s⇓. Nach Lemma 5.8.6 ist die
Anzahl der Normalordnungsreduktionen von s kleiner als die von f s. Wegen
der Minimalität des Gegenbeispiels terminiert die f -strikte Reduktion von s.
Jetzt betrachten wir die f -strikte Reduktionsfolge R[f s] →S R[f s1 ] →S . . . →S
no
no
R[f sn ] −→ R[r[sn /x]] wobei f = λx . r. Es gilt wegen R[f s] −→ R[r[s/x]].
no
∗
Aus Aussage 5.3.5 und der Tatsache R[f s] −→ R[r[s/x]] −→ R[r[sn /x]
können wir jetzt schließen, dass die Anzahl der Normalordnungsreduktionen
von R[r[sn /x]] echt kleiner als die von R[f s] ist, also gilt für diesen Ausdruck
die Behauptung. Damit gilt die Behauptung auch für den Ausdruck R[f s], da
wir eine terminierende f-strikte Reduktion durch Zusammensetzen konstruiert
haben.
2
Die Behauptung gilt auch, wenn wir statt einer einzigen Funktion f eine Menge
von Funktionen nehmen.
Beispiel 5.8.10 Ein Anwendungsbeispiel, das wir in zwei Varianten schon gesehen haben, ist der Übergang von foldl zu foldl’. Das ist z.B. möglich bei
fold (+) 0. D. Damit man das durch strict (fold (+)) 0 ersetzen kann,
muss man dazu zeigen, dass foldl strikt im zweiten Argument ist, wenn der
Operator strikt im zweiten Argumenten ist:
foldl (+) bot []
=
bot
gilt
und
foldl (+) bot (x:xs)
=
foldl (+) (0 + bot) xs = foldl (+) bot xs. Daraus kann man mit
Induktion auf Striktheit schließen.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
62
Die gleiche Argumentation kann man für das fold über binären Bäumen
durchführen.
5.8.1
Andere Auswertungen: hyperstrikt
Die Kernsprache KFP kann die Auswertung sowohl für strikte als auch nichtstrikte FPS mit case und Konstruktoren modellieren.
Aber auch hier ist wieder eine Warnung angebracht: In einer strikten FPS sind
alle Abstraktionen und alle Konstruktoren strikt, (das case-Konstrukt nicht),
so dass man als Bild aller Ausdrücke nur eine Teilmenge von KFPTS hat, was
wieder eine veränderte kontextuelle Ordnung bewirkt:
Z.B. ist λf . bot =c λf . f bot in einer strikten FPS, da man nur strikte Objekte
für f einsetzen kann. Diese Funktionen sind in KFP aber unterscheidbar, indem
man sie auf λx . True anwendet.
Bemerkung 5.8.11 Den Operator hypereval, der Ausdrücke hyperstrikt evaluiert, kann man in KFP definieren: Die Wirkungsweise soll folgendermaßen
sein:
• hypereval(λx.r) → (λx.r) (Auswertung beendet)
no
• hypereval(t) → hypereval(t0 ), falls t −→ t0 (Auswertung)
• hypereval(c t1 . . . tar(c) ) → c (hypereval t1 ) . . . (hypereval tar(c) )
Die Definition dieses Operators kann man als Übungsaufgabe machen, man
braucht dazu eigentlich nur ein rekursives case.
Als Fazit können wir festhalten, dass
• Sowohl der Operator strict als auch hyperstrict in KFP definierbar
sind, und somit kein Problem darstellen.
• Die Gleichheiten, die in KFP für korrekt übersetzte Funktionen einer strikten FP gelten, gelten auch bzgl der kontextuellen Gleichheit der strikten
funktionalen Programmiersprache.
Aber: in strikten funktionalen Programmiersprachen wird teilweise argumentiert, dass man nicht-strikte Funktionen definieren kann, indem man
Auswertung verzögert durch abstrahieren mit lambda und Auswertung
erzwingt durch Anwendung, erhält man offenbar doch nicht die richtige
Gleichheit, wie man sieht. Diese Simulation ist nicht korrekt.
5.8.2
Strikte Superkombinatoren
Definition 5.8.12 Eine Funktion (Superkombinator) f mit Definition
f x1 . . . xn = t heißt strikt in k-ten Argument, wenn folgendes gilt:
wenn ak , 1 ≤ k ≤ n keine WHNF hat, dann gilt für alle Ausdrücke
a1 , . . . , an : (f a1 . . . an ) hat keine WHNF.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
63
Da man als semantischen Wert alle Ausdrücke ohne WHNF mit ⊥ zusammenfasst, schreibt man dies auch oft kürzer als
(f a1 . . . ak−1 ⊥ ak+1 . . . an ) = ⊥
Eine Funktion f mit Definition f x1 . . . xn = t heißt hyper-strikt in k-ten
Argument, wenn für alle Ausdrücke a1 , . . . , an : wenn ak , 1 ≤ k ≤ n keine Normalform hat, dann hat (f a1 . . . an ) keine NF.
Die Striktheit von f kann zwei verschiedene Ursachen haben:
• Alle Ausdrücke der Form (f a1 . . . an ) haben keine WHNF (f terminiert
nicht), oder
• Jede Auswertung von (f a1 . . . an ) zur WHNF muss irgendwann auch ak
zur WHNF auswerten.
Es gilt: Striktheit ist eine unentscheidbare Eigenschaft von Funktionen, da diese
das Halteproblem für Turingmaschinen umfasst.
Das Wissen über Striktheit von Funktionen kann in der Auswertungsstrategie
ausgenutzt werden.
Hat man mehrere strikte Argumentpositionen von Funktionen, so legt man im
allgemeinen noch eine Reihenfolge von links nach rechts fest, aber auch eine
parallele Auswertung zur WHNF könnte angestoßen werden.
Lemma 5.8.13 Der n-stellige Superkombinator f sei strikt im k-ten Argument.
Wenn (f a1 . . . an )⇓, dann gilt auch ak ⇓.
Beweis. Gibt es a1 , . . . , ak−1 , ak+1 , . . . , an , so dass ak nicht zu WHNF reduziert
wird, dann kann man statt ak auch bot einsetzen. Die Reduktion ändert sich
dadurch nicht. Dies ist aber ein Widerspruch zur Striktheit von f im k-ten
Argument.
2
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
5.9
64
Verzögerte (Lazy) Auswertung
Die Auswertungsidee bei nicht-strikten funktionalen Programmiersprachen kann
man folgendermaßen beschreiben: Es gibt einen Auftrag: “werte aus (zu WHNF,
oder zu Normalform)“. Dieser Auftrag kann Unteraufträge anstoßen. Der oberste Auftrag wird auch dann erfüllt, wenn immer nur die unbedingt erforderlichen
Unteraufträge gestartet werden, d.h wenn nur das allernotwendigste ausgewertet wird. Die Normalordnungsreduktion entspricht dieser Auswertungsstrategie,
wenn zu WHNF ausgewertet werden soll.
Der Auftrag “werte aus Normalform aus“ kann in ähnlicher Weise erfolgen.
Allerdings gibt es verschiedene Möglichkeiten:
1. Die Strategie, den Auftrag “werte zu Normalform aus“, ungeprüft nach
unten weiterzugeben hat manchmal den Nachteil, dass zu viel ausgewertet
wird.
2. Die strikte Auswertung liefert zwar immer eine Normalform, wenn sie terminiert, aber sie kann den Auftrag nicht immer erfüllen, dann manchmal
terminiert diese nicht, obwohl eine Normalform existiert.
3. Ein Strategie, die die Normalform, falls sie existiert, immer findet ist:
(a) Werte zu WHNF aus.
(b) Werte die Komponenten der WHNF ebenfalls zu WHNF aus, usw
rekursiv.
Erinnerung: Ein Ausdruck ist in WHNF, wenn er von folgender Form ist:
1. (c t1 . . . tn ) und c ist ein Konstruktor
2. (f t1 . . . tn ) und f ist ein Superkombinator.
3. λx.s
Wir vergleichen den Ressourcenbedarf von Implementierungen der verschiedenen Reihenfolgen der Auswertung:
Vergleicht man die Reduktion in Anwendungsordnung und Normalordnung auf
Ausdrücken in Baumform, dann stellt man fest: Beim Reduzieren der Ausdrücke
(wie bisher) kann die Reduktion in Normalordnung exponentiell mehr Platz und
Zeit benötigen als die Reduktion in Anwendungsordnung:
Beispiel 5.9.1 Wir berechnen 2n auf eine etwas merkwürdige Weise:
dd n x = (if n == 0 then x else (dd (n - 1) (x + x)))
(dd n 1) ergibt dann 2n .
65
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Die
Auswertung
in
Anwendungsordnung
sieht
so
aus:
(dd 10 1) →
(if 10 == 0 then 1 else (dd (10 - 1) (1 + 1)))
→
(dd (10 - 1) (1 + 1))
→
(dd 9 2)
→
(if 9 == 0 then 2 else (dd (9 - 1) (2 + 2)))
→
(dd 8 4)
→
(if 8 == 0 then 4 else (dd (8 - 1) (4 + 4)))
...
...
→
1024
Die
Auswertung
in
Normalordnung
sieht
so
aus:
(dd 10 1) → (if 10 == 0 then 1 else (dd (10 - 1) (1 + 1)))
→
(dd (10 - 1) (1 + 1))
→
(if 10 - 1 == 0 then (1 + 1)
else (dd ((10 - 1) - 1) ((1 + 1) + (1 + 1))))
..........................................................................
Man sieht, dass zunächst ein +-Ausdruck mit 2n 1’en aufgebaut wird, der dann
erst addiert wird.
!!! Dies ist aber ein selbst geschaffenes Problem, denn die Exponentialität kam
durch das explizite Kopieren z.B. des Ausdrucks (1 + 1).
Lösung: Man ersetzt den Baum durch einen gerichteten Graphen. D.h. man
verwendet und verwaltet gemeinsame Knoten (node-sharing) bei der Reduktion.
Wir werden sehen, dass danach die Anzahl der Reduktionsschritte in dieser
Auswertung bei Anwendungsordnung und Normalordnung wieder gleich ist .
+
+
+
+
+
1
1
+
+
+
1
+
1
1
1
1
+
1
1
5.9.1
1
Anzahl der Reduktionen in DAG-Darstellung
Wir simulieren die gemeinsamen Knoten durch einen Baum mit Gleichheitsmarkierungen (z.B. eine Zahl). Bei der Beta-Reduktion werden Argumente in der
66
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Simulation kopiert, aber mit gleicher Markierung versehen. Die Beta-Reduktion
wird jeweils analog auf allen gleich markierten Termen durchgeführt (und zählt
dann nur als ein Schritt). Ein Term der unmittelbar reduziert wird, verliert seine
Markierung auf der obersten Ebene.
Definition 5.9.2 Die Reduktionsstrategie, die Normalordnung verwendet und
gemeinsame Knoten verwendet (shared nodes), nennt man verzögerte Reduktion (lazy reduction). Wir bezeichnen diese Reduktion mit →lz .
Eine Reduktion, die entweder eine Beta-Reduktions oder aus eine CaseReduktions durchführt und sharing beachtet, sei als →s bezeichnet.
Die Anwendungsordnung mit sharing bezeichnen wir als →aos .
In der Implementierung werden dann als gleich markierte Terme auch gemeinsam benutzt, d.h. nur einmal gespeichert.
Lemma 5.9.3 Gleich markierte Positionen liegen stets nebeneinander, d.h. an
unabhängigen Positionen. Sie sind gleich, auch wenn man alle Markierungen
mit beachtet.
Beweis. Das folgt allein schon daraus, dass die als gleich markierten Positionen
identische Ausdrücke sind, die nicht ineinander liegen können. Dass die Terme
gleich sind, folgt daraus, dass sie stets gleich behandelt werden.
2
In den folgenden Lemmas und Aussagen verwenden wir Terme mit sharing.
Lemma 5.9.4 Wenn t →lz t0 und t →s t00 , dann gilt entweder t0 = t00 , oder
t00 →lz t0 , oder es gibt ein t000 , so dass t00 →lz t000 und t0 →s t000 .
lz
t
t'
lz
lz
t''
t'
t
lz
t''
∃ t''': t'''
Beweis.
Wir argumentieren in diesem Beweis mit gerichteten Graphen, simuliert mit
Markierungen in Bäumen. Allerdings erfordert ein genauer Beweis eine besser handhabbare Formalisierung. Die hier gegebenen Begründungen sind eher
eine Skizze. Zudem ist diese Methode falsch bei Erweiterung auf nichtdeterministische Kalküle.
i) Sei t ≡ R[f t1 . . . tn ] geschlossen und f t1 . . . tn der Normalordnungs-Redex.
Dann hat (f t1 . . . tn ) keine freien Variablen. Sei f definiert als f x1 . . . xn =
r. Dann wird f t1 . . . tn zu r[t1 /x1 . . . tn /xn ] reduziert. Um alle Reduktionen anzudeuten, schreiben wir: t ≡ C[f t1 . . . tn , . . . , f t1 . . . tn ]. Die lazy
no
Reduktion reduziert alle gleichzeitig. D.h. C[f t1 . . . tn , . . . , f t1 . . . tn ] −→
C[r[t1 /x1 . . . tn /xn ], . . . , r[t1 /x1 . . . tn /xn ]].
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
67
Wenn t →s t00 eine Reduktion ist, die nur von f t1 . . . tn unabhängige Redexe
reduziert, dann sind die Reduktionen vertauschbar.
Wenn die shared-Reduktion auch in einigen ti gemacht wird, dann gilt: ti → t0i ,
und t000 = C 0 [r[t01 /x1 . . . . . . t0n /xn ], wobei alle Reduktionen “parallel“ in den als
gleich markierten Termen gemacht werden, möglicherweise auch in C. Abhängig
davon, ob xi in r vorkommt oder nicht, gilt einer der beiden im Lemma genannten Fälle.
Wenn die shared-Reduktion oberhalb einer Variante des Ausdrucks f t1 . . . tn
gemacht wird, dann enthält t00 evtl. mehr Kopien dieses Terms, aber der Normalordnungsredex hat sich nicht geändert. Das Diagramm lässt sich schließen
wie in den anderen Fällen.
ii) Wenn der NO-Redex ein case-Ausdruck ist, dann ist die Begründung analog.
2
Mit #LR(t) bezeichnen wir die Anzahl der Lazy-Reduktions-schritte des Terms
t bis zum Erreichen einer WHNF. Mit #AO(t) die Anzahl der Reduktionsschritte in Anwendungsordnung (mit sharing). Wir schreiben #AO(t) = ∞, wenn die
Anwendungsordnung nicht terminiert.
Lemma 5.9.5 Sei t ein Ausdruck. Wenn t →lz t0 und #AO(t) < ∞, dann ist
#AO(t0 ) < #AO(t).
Beweis. Induktion nach #AO(t).W enn#AO(t) = 1, dann gilt sowohl t →lz t0
als auch t →aos t0 und damit ist t0 in WHNF.
Wenn #AO(t) = n > 1, dann betrachte t →aos t00 . Nach Lemma 5.9.4 gibt es
drei Fälle:
Wenn t00 = t0 , dann können wir Induktion verwenden.
Wenn t00 →lz t0 , dann können wir Induktion verwenden und erhalten: #AO(t0 ) <
#AO(t00 ) < #AO(t).
Wenn t00 →lz t000 und t0 →aos t000 , dann gilt mit Induktion #AO(t000 ) <
#AO(t00 ), also auch #AO(t0 ) = #AO(t000 ) + 1 < #AO(t00 ) + 1 = #AO(t).
lz
t
t'
aos
aos
lz
t''
t'''
2
Aussage 5.9.6 #LR(t) ≤ #AO(t).
Beweis. Wir können annehmen, dass #AO(t) < ∞. Mit Induktion nach der
Anzahl der Schritte in AO zur WHNF. Wenn #AO(t) = 0, dann ist t in WHNF,
also ist auch #LR(t) = 0. Sei #AO(t) = n > 0. Dann betrachte t →aos t00 . Sei
t →lz t0 . Es gilt #AO(t00 ) = n − 1.
68
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
Wenn t0 = t00 , dann können wir Induktion verwenden.
Wenn t00 →lz t0 , dann können wir ebenfalls Induktion verwenden und erhalten
#LR(t00 ) ≤ n − 1, also #LR(t0 ) ≤ n − 2, somit #LR(t) ≤ n − 1 < #AO(t) = n.
Wenn t00 →lz t000 und t0 →aos t000 , dann gilt wegen Lemma 5.9.5, dass #AO(t000 ) <
#AO(t00 ) = n − 1, somit #AO(t0 ) ≤ n − 1, also mit Induktion #LR(t0 ) ≤ n − 1.
Da t →no t0 , erhalten wir #LR(t) ≤ n.
2
Theoretisch gilt: lazy evaluation (verzögerte Reduktion) ist noch nicht optimal in
der Anzahl der Reduktionen. Der Grund liegt in der Ausführung der Ersetzung:
Der Rumpf eines Superkombinators wird jedesmal kopiert, aber darin könnten
sich auswertbare Ausdrücke befinden, die erst nach dem Kopieren des Rumpfs
ausgewertet werden. Analog werden beim Erzeugen von partiellen Anwendungen
teilweise Reduktionen doppelt ausgeführt, obwohl dies nicht notwendig ist.
Es gibt Untersuchungen zu optimalen Reduktionsstrategien im Lambda-Kalkül.
Diese Strategien können nur teilweise in die Implementierung von funktionalen
Programmiersprachen Eingang finden.
Beispiel 5.9.7 polynom x y = (x * x) + (y * y)
Werte folgenden Ausdruck aus: (\p . (p 2) + (p 3))
(polynom 1)
Man sieht, dass (1 ∗ 1) zweimal ausgewertet wird, obwohl dies eigentlich unnötig
ist.
Die Standardkompilierung achtet nicht auf diese Optimierungsmöglichkeit. Den
Optimierungsschritt, der zum Ziel hat, möglichst viele der reduzierbaren Ausdrücke bereits zur Kompilierzeit auszuwerten, nennt man “partielle Auswertung“. Wie wir gesehen haben, ist die partielle Auswertung korrekt im Sinne
der kontextuellen Äquivalenz.
Es gibt eine Optimierung, die auf ein besseres Kopieren des Funktionsrumpfes
achtet und Ineffizienzen wie oben vermeidet (“fully lazy“).
Beim Kompilieren hat die Anwendungsordnung den Vorteil, dass man zur Kompilierzeit für jede Funktion genau die Reihenfolge der Auswertungen vorhersagen
kann und dass man keinen Test benötigt, ob Ausdrücke ausgewertet sind.
5.9.2
Sequentialität, Parallelität
Betrachtet man die Funktion ||, das logische oder, dann beobachtet man, dass
True || ⊥ zu True auswertet, während ⊥ || True nicht terminiert, obwohl man
eigentlich sagen könnte, dass eigentlich nur True als Wert zurückkommen kann.
Man kann auf die Idee kommen, dies zu verhindern, und zu versuchen, die scheinbar falsche Implementierung von || zu verbessern. Allerdings werden wir zeigen,
dass man keine Funktion definieren kann, die dieses Verhalten zeigt. Wenn man
dieses Verhalten erzwingen will, dann muss man den ganzen Auswertungsprozess selbst in KFP (Haskell) implementieren und dann den linken und rechten
Term parallel auswerten (d.h. irgendwie abwechselnd).
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
69
Aussage 5.9.8 Man kann in KFP (Haskell, Gofer, Lambda-Kalkül,. . . ) keine
arithmetische Funktion ∗ definieren, die
0∗⊥=⊥∗0=0
liefert, aber ansonsten das Produkt der Argumente als Resultat liefert.
Beweis. Es genügt dies für einen Datentyp mit Konstanten 0, 1 zu zeigen und
eine Multiplikationstabelle 0 ∗ 1 = 1 ∗ 0 = 0 ∗ 0 = 0, 1 ∗ 1 = 1. Zusätzlich soll
0 ∗ ⊥ = ⊥ ∗ 0 = 0 aber auch 1 ∗ ⊥ = ⊥ ∗ 1 = ⊥ gelten.
Angenommen, man hat ein f x y in KFP definiert, das dieses leistet. Dann
betrachte die Auswertung (f t1 t2 ).
Die Normalordnung findet eine WHNF von (f t1 t2 ), falls eine existiert. Eine
Eigenschaft der Normalordnung ist: wenn ein Unterausdruck reduziert wird,
dann wird dieser mindestens solange reduziert, bis er selbst in WHNF ist. D.h.,
wenn man f t1 t2 reduziert, dann wird irgendwann zum ersten mal entweder
t1 oder t2 reduziert. Diese Entscheidung hängt aber nicht von der speziellen
Form von t1 oder t2 ab. ObdA sei t1 das erste Argument, das in Normalordnung
reduziert wird. Dann ist f ⊥ t2 = ⊥ , unabhängig vom Inhalt von t2 , denn wenn
es einen anderen Wert hätte, würde die Normalordnung die Kopfnormalform
nicht finden. Damit kann es eine solche Funktion nicht geben.
2
Aussage 5.9.9 Für jeden KFPTS-Superkombinator f gilt:
Entweder hat für jede Anzahl n und alle Argumente ai der Term f a1 . . . an
eine Abstraktion als WHNF (d.h. ist ∼c Y K), oder es gibt eine Anzahl n von
Argumenten, so dass eine der folgenden Aussagen gilt:
• ∀a1 , . . . an : (f a1 . . . an )⇑.
• Es gibt ein Konstruktor c, so dass ∀a1 , . . . an : f a1 . . . an hat eine WHNF
mit c als Top-Konstruktor.
• f ist strikt in mindestens einem Argument.
Beweis. Wir nehmen an, dass f 6∼c Y K. Die Reduktion von f x1 . . . xn kann
folgendes ergeben:
• sie kann nicht terminieren oder einen Typfehler liefern. Dann gilt 5.9.9
• oder es ergibt sich ein Term der Form (c t_1 ... t_m); dann gilt Fall
5.9.9.
• Sie kann einen Term der Form λy.t ergeben. Dann kann man noch ein
Argument hinzufügen. Das Hinzufügen terminiert, denn f ∼c Y K war
ausgeschlossen.
• Sie kann einen Term R[xi ] ergeben, wobei R ein Reduktionskontext ist.
Das ist Fall 5.9.9.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
70
2
Beachte, dass der Fall f ∼c Y K bei polymorpher Typisierung nicht auftritt,
da der Typ unendlich groß sein müsste. Die Anzahl n lässt sich bei polymorph
getypten Superkombinatoren am Typ ablesen.
5.10
Church-Zahlen: KFP ist nicht minimal
Das Konstrukt case und Konstruktoren sind nicht unbedingt notwendig, wobei hier gemeint ist, dass man das Verhalten von case und Konstruktoren in
KFP ohne Konstruktoren implementieren kann. Die kontextuelle Äquivalenz
geht dabei möglicherweise verloren.
Als erstes Beispiel können wir Funktionen definieren, die sich wie die Booleschen Funktionen verhalten: Danach die Zahlen, wie A. Church sie definiert hat
Church-Zahlen).
1. man definiert Boolesche Funktionen:
T RU E x y
F ALSE x y
IF x y z
AN D x y
OR x y
N OT x
=
=
=
=
=
=
x
y
(x y z)
x (y T RU E F ALSE) F ALSE
x T RU E y
x F ALSE T RU E
2. Paare und auch Tupel:
P AIR x y z
F ST x
SN D x
= zxy
= x T RU E
= x F ALSE
3. Zahlen (nach A. Church.) Die Darstellung der Zahlen ist: zahl s z =
s (s . . . (s z) . . .)
ZERO x y
SU CC n s z
P LU S m n
T IM ES m n
EXP T b e
P REDR p
P RED n
M IN U S m n
ZERO? n
EQU AL? m n
=
=
=
=
=
=
=
=
=
=
y
s (n s z)
m SU CC n
m (P LU S n) ZERO
e (T IM ES b)(SU CC ZERO)
P AIR (SU CC (F ST p))(F ST p)
SN D (n P REDR (P AIR ZERO ZERO))
n P RED m
n (T RU E F ALSE) T RU E
(AN D (ZERO? (M IN U S m n))
(ZERO? (M IN U S n m)))
71
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
(P AIR x y) soll die Paarbildung nachbilden. Beachte, dass ZERO und FALSE
diesselbe Funktion sind.
Übungsaufgabe 5.10.1 Berechne mit obigen Definitionen 1 = 1 und 1 = 2:
Man kann nachprüfen, dass folgendes gilt:
• P RED (SU CC n) = n, wenn n von der Form (SU CC k ZERO) ist.
• P RED ZERO = ZERO, d.h. P RED berechnet für positive Zahlen n die
nächstkleinere Zahl und für die N ull ist das Resultat jeweils N ull.
Als Beispiel berechnen wir: (P RED ZERO) und kürzen ab: T RU E =
T, F ALSE = F, ZERO = Z
(P RED Z) →
→
→
→
=
SN D (Z P REDR (P AIR Z Z))
(Z P REDR (P AIR Z Z)) F
(P AIR Z Z) F
F Z Z
Z
ZERO? ZERO
→ ZERO (T F ) T
→ T
ZERO? (SU CC ZERO) → SU CC Z (T F ) T
→ T F (Z (T F ) T )
→ F
Will man negative Zahlen codieren, so kann man dies durch Zahlen mit Vorzeichen in der folgenden Form durchführen: +n als (P AIR T RU E n) −n als
(P AIR F ALSE n)
Übungsaufgabe 5.10.2 Definition der Rechenoperationen auf diesen ganzen
Zahlen.
Bemerkung 5.10.3 Was bleibt (nicht) erhalten bei dieser Übersetzung?
Ungetypte Ausdrücke werden teilweise in terminierende übersetzt, da das case nicht exakt übersetzbar ist. Die kontextuelle Ordnung bleibt ebenfalls nicht
erhalten, da Y K nach der Übersetzung zum größten Element wird.
Was erhalten bleibt, ist die Eigenschaft:
no,∗
no,∗
Wenn t −→ t0 wobei t0 in WHNF, dann auch τ (t) −→ τ (t0 ), wobei τ (t0 ) in
WHNF. D.h. nur erfolgreiche Berechnungen werden korrekt übersetzt.
D.h. diese Übersetzung ist keine volle Übersetzung der gesamten Sprache
KFPTS, sondern eine, die Berechnungen simulieren kann.
Betrachtet man nur die Übersetzung der getypten Ausdrücke, so könnte sich die
Situation etwas verbessern: unklar.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
5.10.1
72
SKI-Kombinatoren
Es gibt eine Übersetzung des Lambda-Kalküls in eine einfache Superkombinatorsprache, bei der ebenfalls die Äquivalenz erhalten bleibt: die sogenannte
SKI-Übersetzung. Die Sprache KFP (KFPTS) muss man erst vorbehandeln,
d.h. in den Lambdakalkül übersetzen, um danach diese Übersetzung verwenden
zu können.
Dazu benötigen wird drei Kombinatoren S, K, und I:
Sxyz
K xy
I x
= (x z) (x z)
= x
= x
Diese Definitionen können offenbar in KFPTS, also auch in KFP durchgeführt
werden. Die Reduktion ist wie in KFPTS. Die Übersetzung von reinen LambdaTermen benötigt die jeweils abstrahierte Variable und eine erweiterte ZwischenDarstellung von Ausdrücken.
[x]E bedeutet: die Variable x soll aus E abstrahiert werden und hat Wirkung
analog zu (λx.E). Dies kann auch verschachtelt sein.
λ-Ausdrücke werden so übersetzt:
λx.E → ([x] E)
Danach wirken die folgenden drei Übersetzungsregeln:
1) [x] (a b) → S ([x] a) ([x] b)
2) [x] x
→ I
3) [x] y
→ K y (y Variable oder Konstante, die von x
verschieden ist. Die Konstanten hier nur:S, K, I)
Beispiel 5.10.4 Wir übersetzen
T RU E x y = λx.(λy.x)) in SKI:
[x] ([y] x) → [x] (K x) → S ([x] K)([x] x) → S (K K) I
Die Reduktion von (T RU E a) mittels SKI ergibt:
S (K K) I a → (K K a) (I a) → K a
Man kann jetzt zeigen, dass die Wirkung des Originalausdrucks und des übersetzten Ausdrucks identisch ist.
Die β-Reduktion wird durch die SKI-Kombinatoren implementiert:
(λx.E) t → E[x/t] . Die übersetzte Version leistet das gleiche: ([x]E) t0 hat drei
Möglichkeiten:
1. E ist Applikation. Dann wurde dies mittels S übersetzt:
([x] (a b)) t0 → S ([x] a)([x] b) t0 → ([x] a) t0 )(([x] b) t0 )
2. E ist die Variable x:
([x] x) t0 → I t0 → t0
3. E ist Konstante oder eine Variable 6= x ([x] y) t0 → K y t0 → y
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 11. Juli 2003
73
Man kann zeigen, dass α-äquivalente (bis auf Umbenennung gleiche) LambdaAusdrücke in den gleichen SKI-Kombinatorausdruck übersetzt werden, und
dass Namenskonventionen bei der durch SKI simulierten β-Reduktion erhalten
bleiben. Kontextuelle Gleichheit ist übertragbar.
Was auch noch gilt, ist die Eigenschaft, dass irreduzible Lambda-Ausdrücke in
irreduzible SKI-Ausdrücke übersetzt werden.
Die Anzahl der Reduktionsschritte in beiden Kalkülen ist verschieden, da in
SKI auch die Ersetzung im Term als mehrere Reduktionsschritte gezählt werden.
Literaturverzeichnis
[Bar84] H.P. Barendregt. The Lambda Calculus. Its Syntax and Semantics.
North-Holland, Amsterdam, New York, 1984.
74
Herunterladen