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, 25. Januar 2007 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: E ::= V V sind Variablen | \V. EXP wobei V eine Variable ist. | (E1 E2 ) | (c E1 . . . En ) wobei n = ar (c) | (case E {P at1 → E1 ; . . . ; P atN +1 → EN +1 }) Hierbei ist P ati Pattern zum Konstruktor i, und P atN +1 das Pattern lambda. (P ati → Ei ) 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, 25. Januar 2007 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+1} -> 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) Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 5.1.3 4 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 ·= == == == = c ·< f << << << < a f b Der erste Term in einer Liste von Termen ist die Funktion, der Rest sind die Argumente: Man kann eine kompaktere Darstellung wählen: a f· === == == = c f· @ @@ @@ @@ @ b Bei Termen als Bäumen werden wir auch die Adressierungsmethode in Bäumen verwenden: f· === 1 ==2 == = c f· @ @@ 1 @@ 2 @@@ a b 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, 25. Januar 2007 f f 5.1.4 5 @· } ??? } ?? } ?? }} ? }~ } c @@ @@ @@ @@ @ @? ~~ ??? ~ ~ ?? ~~ ?? ~~~ @@ b @@ @@ @@ @ a 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 +1 → tN +1 }) | (case t {p1 → t1 ; . . . ; pi → C; . . . ; pN +1 → tN +1 }) 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 haben, und diese freien Variablen dann nach dem Einsetzen gebunden sind, d.h. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 6 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 +1 → tN +1 }). Einsetzen von e in den Kontext ergibt (case (x e) {p1 → t1 ; . . . ; pN +1 → tN +1 }) • Mit C := λx.([·] x) ergibt sich C[x] = λx.(x x) 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 7 Definition 5.1.7 Reduktionskontexte sind: R ::= [] | (R e) | (case R {p1 → t1 ; . . . ; pN +1 → tN +1 } 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 konvergiert bzw. 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. Alternative Definition des Normalordnungsredex: Sei R das Label: Starte mit tR und wende die Regeln solange an, bis keine Anwendung mehr möglich ist. C[(s t)R ] → C[(sR t)] C[(case s alts)] → C[(case sR alts)] Normalordnungsreduktion; mit R-Label definiert: Beta ((λx.t)R s) t[s/x] Case (case (c t1 . . . tn )R {. . . ; c x1 . . . xn → s; . . .}) s[t1 /x1 , . . . , tn /xn ] (case (λx.t)R {. . . ; lambda → s}) s Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 8 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 5.2 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. Man kann im Prinzip auch andere Formen von Tests nehmen. 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 9 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 weiterer, dass man viele Schlüsse und Methoden unabhängig von der Sprache entwickeln kann. 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. 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 10 Ein Vorteil dieser Herangehensweise ist die Maximalität der kontextuellen Gleichheit. Bei Definition einer zu schwachen Gleichheit ∼weak , d.h. die zuviele Terme unterscheidet, kommt es vor, dass man man bei bestimmten Programmtransformationen dann doch für deren Korrektheit argumentiert, obwohl die Programme nach der Transformation verschieden sind (unter ∼weak ),. Diese sind aber i.a. noch gleich unter ∼c . Es gilt, dass ∼c maximale Relation ist mit folgenden Eigenschaften: • ∼c ist Kongruenz: D.h. Äquivalenzrelation und kompatibel mit Kontexten. • s → t =⇒ s ∼c t • λx.s 6∼c (c . . .) • c1 . . .) 6∼c (c2 . . .) • bot 6∼c (c . . .) • bot 6∼c λx.s Der Nachweis der Maximalität ist relativ einfach: Sei ∼max diese Relation. • ∼c hat alle diese Eigenschaften, also ist ∼c ⊆ ∼max . • Sei s ∼max t und C ein Kontext, und sei C[s]⇓. Da die Relation ∼max kompatibel mit Kontexten ist, gilt C[s] ∼max C[t]. Aus C[s]⇓ folgt, das C[s] ∼max s0 und s0 ist eine Abstraktion oder eine Konstruktoranwendung. Damit gilt aber auch, dass C[t] ∼max s0 . Wenn die (eindeutige) Normalordnung von C[t] nicht konvergiert, dann gilt C[t] ∼max bot, Das ergibt einen Widerspruch, also gilt C[t]⇓. Das gilt für alle Kontexte, also gilt s ∼max t =⇒ s ∼c t. 5.3 KFP: Invarianz der Terminierung, Standardisierungssatz Wir behandeln jetzt die Sprache KFP etwas formaler. Das Ziel dieses Kapitels ist der Nachweis, dass die kontextuelle Äquivalenz erhalten bleibt beim Reduzieren. Dazu wird als Hilfsmittel gezeigt, dass Terminierung von Ausdrücken erhalten bleibt, wenn man Reduktionsregeln anwendet. 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 aufwändig, aber doch elementar. Da diese Aussagen die Basis für die Korrektheit von Programmtransformationen sind, lohnt es sich auch, diese etwas genauer durchzugehen. 11 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 Zuerst eine informelle Begründung, warum man mit einfacher Induktion nicht zeigen kann, dass s → t =⇒ (s⇓ ⇔ t⇓). Das Problem liegt in der Verdopplung der Beta-Reduktion: / (λx.(x x)) a (λx.(x x)) ((λy.y)a) no no /aa / ((λy.y)a) a ((λy.y)a) ((λy.y)a) Mit Induktion nach der Länge der Normalordnung kann man damit, nachdem man alle Überlappungsfälle analysiert hat, zumindest die Richtung s → t =⇒ (s⇓ =⇒ t⇓) zeigen. Die andere Richtung ist das Problem. Denn das obere Bild hat Varianten: es könnte auch / (λx.(x x)) a (λx.(x x)) ((λy.y)a) no ((λy.y)a) ((λy.y)a) no no / ((λy.y)a) a /aa Damit hat man als allgmein Vertauschungsregel nur: /t s no,∗ no ∗ s0 _ _ _/ t0 die keine Induktion mehr zulässt, da man keine oberen Schranken für die Anzahl der Reduktionen nach Vertauschung hat. Für die Induktion gibt es eine Lösung, die wir aber für beide Richtungen verwenden: Wir benötigen dafür die sogenannte 1-Reduktion (siehe [?]), deren Bedeutung die parallele 1-Schritt-Reduktion ist. D.h. zwei Terme s, t stehen in der 1Relation, 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. 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 ). 12 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 • 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 ) − → ∗ ((λx.t1 ) t2 ) → t1 [t2 /x], also insgesamt s − → t. 2 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 . 1 t 1 no s / t0 @ / t0 no no 1 s _ _ _/ s0 t 1 Beweis. Dies zeigt man mit Induktion über die Struktur des Ausdrucks; Genauer über die Tiefe des Normalordnungsredex: Dazu braucht man eine Fallunterscheidung: • Sei t ≡ t1 t2 und t1 ist keine Abstraktion. Dann ändert die Normalordnungsreduktion den Ausdruck t2 nicht. Für den Ausdruck t1 ist die no Tiefe des Normalordnungsredex kleiner, also gilt mit t1 −→ s1 , auch: no 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: 13 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 – s1 →1 t01 . Dann gilt auch s1 t2 →1 t01 t02 . 1 t1 t2 no x s1 t2 1 x x / t01 t02 x; 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 . / t01 t02 no no 1 _/ 0 0 _ _ s1 t2 s1 t2 t1 t2 1 • 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 ]. 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 14 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 no,k Aussage 5.3.5 Sei t ein (evtl. offener) Ausdruck. Wenn t −−−→ t2 und t2 ist no,k0 eine WHNF, und t →1 t0 . Dann gilt t0 −−−→ t02 , t2 →1 t02 , wobei k ≥ k 0 und t02 eine WHNF ist. Somit terminiert dann die no-Reduktion von t0 . / t0 0 no,k no,k 1 t2 _ _ _/ t02 t 1 ≤k 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 ≥ k0 . 1 t 1 no 1 t2 t no,k0 0 ? t2 t1 no,k−1 / 0 ?t 1 / t0 no t1 1 no,k−1 t2 1 no / t01 no,k0 −1 / t0 2 2 Da jede 1-Schritt-Reduktion auch als 1-Relation darstellbar ist, folgt unmittelbar: Satz 5.3.6 Sei t ein (evtl. offener) Ausdruck. Wenn t⇓, und t → t0 mit einer anderen Reduktion. Dann gilt auch 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. 15 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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. 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): 16 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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 0 Reduktion u →1 s intern ist. /u t no,+ no 1,int u0 _ _ _/ s 1 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 ; . . .})] −→ 0 0 0 0 0 R [tn+1 [t1 /x1 , . . . , tn /xn ]] wobei R Reduktionskontext, ti →1 ti , 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 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 17 Beweis. Das Lemma 5.3.9 kann man zum Verschieben der 1-Reduktion nach no no rechts verwenden. s →1 s2 −→ . . . −→ sn wird damit im ersten Schritt zu no,∗ no 0 s −−−→ s2 →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. 1 / s2 s no,∗ no 1 s02 _ _ _/ s3 no,∗ no 1 s03 _ _ _/ s4 no . 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 . t> ∗ > no,∗ > > ∗ t1 _ _ _ _ _ _/ t2 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 . 18 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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. Definition 5.3.14 Die durch Reduktion erzeugte 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. Für ←− −→ gilt das ChurchRosser-Theorem, das man auch mit det 1-Reduktion beweisen kann: ∗ Satz 5.3.15 (Church-Rosser) Wenn s ←− −→ t, dann existieren s0 , t0 mit ∗ ∗ 0 0 0 s ≡α t und s − → s und t − → t . Als Diagramm: s o ∗ / t ∗ ∗ α _ _ 0 _ _ _ _ s t0 ∗ Satz 5.3.16 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.17 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) a y -> 1 : r2 b) 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. Aus dem Satz von Church-Rosser kann man schließen, ∗ dass somit nicht gilt: rp1 ←− −→ 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. 19 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 Beispiel 5.3.18 Ein weiteres Beispiel ist: s = \x .x t = \x -> case x of {p1 -> x; ....; p_n -> x; lambda -> x} Das ist gerade \x -> seq x x. Diese beiden Ausdrücke können als gleich ge∗ zeigt werden, aber sind nicht gleich bzgl der Konversion ←− −→, da nach obiger Überlegung dann s, t auf α-äquivalente Terme reduzieren müssten, diese aber bereits in Normalform sind. 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: 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 20 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 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 21 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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.16, 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. 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]⇓. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 22 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.16. 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; d.h. die Komponenten des Ergebnisses zu reduzieren oder falls es eine Funktion ist, diese auf ein Argument anzuwenden. Dies leistet die sogenannte Simulation bzw. Bisimulation. Definition 5.4.9 Wir definieren ≤b als den größten Fixpunkt des folgenden Operators [.] auf Relationen R auf geschlossenen Termen. Sei R eine Relation. Die Relation s [R] t für geschlossene Terme s, t gilt, wenn folgendes erfüllt ist: 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 geschlossenen r: ((λx.s0 ) r) R ((λx.t0 ) r) 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. Einschub Knaster-Tarski Fixpunktsatz Ein vollständiger Verband A ist ein Verband, d.h. eine Menge mit einer partiellen Ordnung ≤, so dass glb und lub von zwei Elementen existieren. Vollständig bedeutet, dass man zu jeder Teilmenge S ⊆ A den glb und lub bilden kann. Ein wichtiges Beispiel zu vollständigen Verbänden ist der VerbandSder Teilmengen einer T Menge M geordnet mit ⊆. Hier kann man lub(S) = (S) und glb(S) := S definieren. Wir nennen a ist ein Fixpunkt einer Funktion f , gdw. f (a) = a; a ist ein Post-Fixpunkt einer Funktion f , gdw. a ≤ f (a). Entsprechend ist a ist ein Pre-Fixpunkt einer Funktion f , gdw. f (a) ≤ a. Der Fixpunktsatz von Knaster-Tarski: Sei A ein vollständiger Verband und f einen monotone Funktion. Dann existiert Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 23 ein Fixpunkt von f . Der größte Fixpunkt von f ist lub{x ∈ A | x ≤ f (x)}. Der kleinste Fixpunkt von f ist glb{x ∈ A | f (x) ≤ x}. Da man die Relationen auf den Ausdrücken als Mengen von Paaren ansehen kann, hat man einen vollständigen Verband von Relationen, und man kann den Fixpunktsatz von Knaster-Tarski verwenden. 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 = νf = {R ∈ L | R ⊆ f (R)} ein größter Fixpunkt, und auch ein größter Post-Fixpunkt. \ k = µf = {R ∈ L | f (R) ⊆ R} ein kleinster Fixpunkt, und auch ein kleinster Pre-Fixpunkt. Die Funktion [·] ist monoton auf Relationen, denn bei einer kleineren Relation R stehen auch weniger Paare von Ausdrücken in der [R]-Relation. Deshalb kann man den Satz von Knaster-Tarski auf Relationen und die Funktion [·] anwenden. (Satz von Kleene) Wenn die Funktion f für die man den Fixpunkt bilden will, die Eigenschaft der (glb)-Stetigkeit hat: D.h. für alle absteigenden Folgen s1 ≥c s2 ≥c . . ., die einen glb s haben, gilt: glb(f (si )) = f (glb(si )). Dann kann man den größten Fixpunkt von f auch definieren über eine Folge von Relationen ≤b,i mit folgender Eigenschaft: Entsprechendes gilt für lub-stetige Funktionenund den kleinsten Fixpunkt. Beim oben in Def. 5.4.9 definierten Operator [·] auf Relationen kann man die Eigenschaft der Stetigkeit nicht nachweisen, man kann eine entsprechende Folge und deren glb aber als eine Näherung verwenden: 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: ((λx.s0 ) r) ≤b,i ((λx.t0 ) r) Die Relation s ≤b t ist dann enthalten in der Relation: ∀i : s ≤b,i t. 24 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 Man braucht noch die offene Erweiterung ≤ob : von ≤b : s ≤ob t gdw. für alle Einsetzungen σ von geschlossenen Termen: σ(s) ≤b σ(t) gilt. Entsprechend ist ∼ob definiert. Es gilt folgende Aussage zum Zusammenhang zwischen ≤b und ≤c : Satz 5.4.11 (siehe Howe-1989, 1996). Es gelten die Aussagen: ≤ob = ≤c ∼ob = ∼c Es folgt sofort, dass ⊥ ≤c t ist für alle t, denn für alle geschlossenen t: ⊥ ≤b t ist offensichtlich. Für offene t folgt es dann auch aus der Definition von ≤ob . 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: Aussage 5.4.12 Es gilt ≤c ⊆ ≤ob Beweis. Es genügt zu zeigen, dass ≤c,0 (≤c,0 ist ≤c eingeschränkt auf geschlossene Terme) ein Post-Fixpunkt des Operators [.] ist, d.h. dass ≤c,0 ⊆ [≤c,0 ], denn nach dem Satz von Knaster-Tarski ist ≤b ein größter Post-Fixpunkt von [·]. Das ist aber gerade der Inhalt von Aussage 5.4.3, denn aus s ≤c,0 t folgt einer der drei Fälle in 5.4.3, und daraus folgt, dass s [≤c,0 ] t. Für offene Terme folgt es dann aus der offenen Erweiterung. 2 Der Unterscheidung offen / geschlossen bietet auch keine Überraschungen: Aussage 5.4.13 Seien s, t KFP-Ausdrücke mit den freien Variablen x1 , . . . , xn . Dann gilt s ≤c t gdw. für alle Einsetzungen σ von geschlossenen Termen für freie Variablen σ(s) ≤c σ(t). Beweis. Die eine Richtung folgt leicht aus der Aussage 5.4.8 oben. Die andere Richtung folgt aus dem Satz von Howe: ≤c,0 ⊆ ≤ob und da ≤b = ≤c,0 ist, wobei ≤c,0 die Einschränkung von ≤c auf geschlossene Terme ist. 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 (CoInduktion). Lemma 5.4.14 Wenn für eine Relation R gilt: R R ⊆ ≤b = ≤c . ⊆ [R], dann gilt Das einfache Prinzip des Kleene-Satzes kann man leicht durch die abstraktere Methode der Co-Induktion ersetzen, um konkrete Gleichheiten zu zeigen: Lemma 5.4.15 Gegeben zwei Terme s, t. Sei R1 so definiert, dass s0 R1 t0 gdw. (s0 , t0 ) Grundterme sind, die eine gemeinsame Einsetzung von Termen für die Variablen in (s, t) ist. Wenn R1 ⊆ [R1 ∪ ≤b ], dann gilt R1 ⊆ ≤b , und somit s ≤b t, also auch s ≤c t, Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 25 Beweis. Aus R1 ⊆ [R1 ∪ ≤b ] folgt (R1 ∪ ≤b ) ⊆ [R1 ∪ ≤b ]. Damit gilt die Behauptung, da ≤c und ≤b übereinstimmen, und da ≤b der größte Fixpunkt von [·] ist, und somit R1 ∪ ≤b ⊆ ≤b , also R1 ⊆ ≤b = ≤c . Die Relation s ≤c t folgt aus der Aussage 5.4.13. 2 Aus dem bisherigen folgt auch: Lemma 5.4.16 Gegeben zwei Terme s, t. Sei R1 so definiert, dass s0 R1 t0 gdw. (s0 , t0 ) Grundterme sind, die eine gemeinsame Einsetzung von Termen für die Variablen in (s, t) ist. Wenn R1 ⊆ [R1 ∪ ∼b ], dann gilt R1 ⊆ ∼b , also auch R1 ⊆ ∼c und s ∼c t. Diese Aussagen helfen, die Gleichheit von Ausdrücken zu prüfen: Beispiel 5.4.17 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) Diese Terme haben keine freien Variablen. Die Relation R1 besteht genau aus (s, t). Man muss also nur zeigen, dass (s, t) ∈ [{(s, t)}∪ ∼b ]. Dazu muss man s [{(s, t)}∪ ∼b ] t testen. das gilt, wenn man (nach zwei Schritten) für alle (geschlossenen) Ausdrücke r1 , r2 : die Gleichheit von s r1 r2 und t r1 r2 testet, d.h. {(s, t)}∪ ∼b 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 s, t nicht kontextuell äquivalent zu λx, y.(Cons True y) sind. Beispiel 5.4.18 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.16 anwenden. Damit folgt 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 (i.a. call-by-need-Kalküle) als gleichwertiger oder überlegener Weg zu den Aussagen wie Korrektheit von Programmtransformationen und Standardisierungslemma verwendet werden. Wir erwähnen das Kontextlemma wegen seiner herausragenden Rolle in anderen Reduktionskalkülen; es lässt sich auch in KFP unabhängig nachweisen, allerdings scheint es nicht der richtige Weg Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 26 zu sein, die Korrektheit der KFP-Kalkülregeln als Programmtransformationen nachzuweisen. Mögliche Gründe sind: KFP-Reduktionskontexte können offene Terme nicht abschließen, was in anderen Kalkülen (mit let) möglich ist; In KFP wird call-by-name verwendet (d.h. die Beta-Reduktion setzt beliebige Ausdrücke ein). In call-by-need Kalkülen 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.19 (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, und auch unabhängig von den Bsimulationssätzen. 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 27 derselben Normalordnungsreduktion reduziert. Das liegt daran, dass das Finden 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.20 Dieses Beispiel zeigt, dass der Beweis des Kontextlemma zu KFP nicht auf offene Terme 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 alle eingesetzten Ausdrücke unverändert im Redukt wiederfinden, z.B. ist [x] zu [1] geworden. 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. Wir beweisen diese Aussage, die oben aus dem Howe-Satz folgt, nochmal direkt. Aussage 5.4.21 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, 25. Januar 2007 5.4.3 28 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.22 Eine Programmtransformation ist eine Relation T auf Ausdrücken. Man sagt zu e1 T e2 , dass T den Ausdruck e1 zu einem Ausdruck e2 transformiert. Die Programmtransformation T ist korrekt, wenn aus e1 T e2 stets e1 ∼c e2 folgt. Die Programmtransformation T ist modular, wenn für jeden Kontext C aus e1 T e2 auch C[e1 ] T 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 darf eine Transformation nur verwendet werden, wenn man darauf achtet, dass sich die Terminierungseigenschaften dadurch 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.23 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.24 Aus dem Satz 5.3.16 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.25 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 29 Aussage 5.4.26 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.27 Sei e ein Ausdruck, der die Variable x nicht frei enthält. Dann ist die η-Transformation definiert als: η → e λx.(e x) − Lemma 5.4.28 Die η-Transformation ist i.a. 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 η λx.bot x − → bot, und bot hat keine WHNF. 2 Die folgende Variante der η-Reduktion ist korrekt: Definition 5.4.29 Sei e ein Ausdruck, der die Variable x nicht frei enthält, und der eine WHNF hat, die eine Abstraktion ist (bzw. von Typ“a → b ist). ” Dann ist die η 0 -Transformation definiert als: η0 λx.(e x) −→ e Aussage 5.4.30 Die η 0 -Transformation ist korrekt. Beweis. Hierzu genügt es, den Satz zur Bisimulation zur verwenden: Da beide eine WHNF haben, genügt es, beide Ausdrücke auf allen geschlossenen Argumenten r zu testen: ((λx.(e x)) r) reduziert zu (e r). 2 Man kann diese Transformation noch etwas allgemeiner fassen: Die ηTransformation ist korrekt, wenn man sie im richtigen Kontext verwendet: η 00 ((λx.(e x)) t) −−→ (e t) . Diese Transformation ist ebenfalls mittels des Bisimulationssatzes leicht als korrekt zu erkennen. 5.4.4 Kleinste und Größte Elemente bzgl. ≤c Wir betrachten kleinste und größte Elemente bzgl. ≤c , wobei wir das modulo ∼c meinen. Was wir im folgenden zeigen können, ist die kontextuelle Äquivalenz aller nichtterminierenden Ausdrücke und aller ungetypten Ausdrücke, insbesondere ⊥. 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: 30 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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 ⊥ das kleinste Element ist (modulo ∼c ): Lemma 5.4.31 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 Vorsicht bei offenen Termen! z.B. gilt x⇑, aber x 6∼c ⊥. Lemma 5.4.32 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.21 zeigt dann auch s ≤c t. 2 Damit gibt es ein kleinstes Element in der ≤c -Ordnung in der Menge der geschlossenen Ausdrücke (modulo ∼c ), nämlich bot, auch als ⊥ geschrieben. Aussage 5.4.33 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.34 Wenn es mindestens einen Konstruktor mit Stelligkeit ≥ 1 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 ⊥ . . ⊥} 6≤c s: Nehme den Kontext | .{z n case [] {(c x1 . . . xn ) → c; lambda → ⊥; . . .}. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 31 • 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 (d.h. eine Implementierung, die die kontextuelle Ordnung erhält) von KFP in den einfachen ungetypten LambdaKalkü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 bzgl. ∼c zu verändern: (Y K) t ∼c (Y K), wobei ∼c hier die kontextuelle Ordnung im lazy Lambda-Kalkül ist. Der Ausdruck Y K entspricht dem Superkombinator f x = f . 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 für Ausdrücke mit WHNF. Definition 5.4.35 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 . Wir nehmen an, dass S-Normalformen auch WHNFs sind. • Seien S1 , S2 zwei Strategien. Die Strategie S1 ist strikter als S2 , gdw. für alle Terme t: t⇓S1 ⇒ t⇓S2 , • Eine Strategie S ist korrekt, gdw. für alle Terme t: t⇓S ⇒ t⇓. D.h., wenn sie strikter als die Normalordnungsreduction ist. Aus unseren Sätzen folgt sofort, dass alle Strategien korrekt sind. Folgende Normalform für strikte Auswertung entspricht der Auswertung der Argumente einer Anwendung vor dem Einsetzen der Argument. Definition 5.4.36 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, 25. Januar 2007 32 Beachte, dass in dieser Definition der kleinste Fixpunkt gebildet wird. 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. Ebenso, dass der zu analysierende Ausdruck im case eine strikte Normalform ist. Die zur Strategie gehörige Normalform ist die strikte Normalform. 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.37 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. Beispiel 5.4.38 Ein weiteres Beispiel ist hd (map f (map g [1..100])), wobei f,g Funktionen sind, die Zahlkonstanten als Ein- und Ausgabe haben. In der Normalordnung wird vom Ausdruck [1..100] nur das erste Element ausgewertet, d.h. bis 1: [2..100], usw. Das Ergebnis ist hd ((f (g 1)) : (map f (map g [2..100]))). Das ergibt (f (g 1)) und danach wird dieser Ausdruck ausgewertet. In der Anwendungsordnung wird zunächst der Ausdruck [1..100] zur Liste der Länge 100: [1,2,3,...,100] ausgewertet. Am Ende hat man den Ausdruck hd [a1,...,a100] wobei ai das Ergebnis von f (g i) ist. Das Ergebnis ist ai. 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.39 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: Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 33 Aussage 5.4.40 Wenn für einen Term t die Reduktion bzgl. irgendeiner Strategie (z.B. Anwendungsordnung) terminiert, dann auch die Reduktion in Normalordnung. D.h. jede Strategie (auch die Anwendungsordnung) ist strikter als die Normalordnung. Beweis. Folgt aus der Annahme, dass S-Normalformen auch WHNFs sind, dass Reduktionen ∼c erhalten, und aus dem Standardisierungs-Satz. 2 Aussage 5.4.41 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. Beispiel 5.4.42 Zu verschiedenen Reihenfolgen der Auswertung von Ausdrücken. Wertet man (quadrat(3 ∗ 3)) aus, so kann man das auf drei Weisen machen: (quadrat(3 ∗ 3)) → quadrat 9 → 81 oder (quadrat(3 ∗ 3)) → ((3 ∗ 3) ∗ (3 ∗ 3)) → (9 ∗ (3 ∗ 3)) → (9 ∗ 9) → 81 oder (quadrat(3 ∗ 3)) → ((3 ∗ 3) ∗ (3 ∗ 3)) → ((3 ∗ 3) ∗ 9) → (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.5 Übersetzungen von Haskell nach KFPTS nach KFPT nach KFP Wir betrachten jetzt die Übersetzungen Haskell → KFPTS → KFPT → KFP 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 als Index weglassen. Definition 5.5.1 Sei τ eine Übersetzung von P1 nach P2 , von der wir annehmen, dass sie Kontexte und Terme übersetzt, und dass sie modular ist, d.h. τ (C[s]) = τ (C)[τ (s)] für alle C, s. Insbesondere heißt das, dass für alle Zerlegungen eines Terms t in C[s] diese Eigenschaft gilt, so dass eine Übersetzung jedes Konstrukt getrennt übersetzen muss. Wir betrachten folgende Eigenschaften: 34 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 • τ erhält Terminierung, gdw. für alle s, t in P1 gilt: s⇓1 ⇔ τ (s)⇓2 • τ ist korrekt bzgl. der kontextuellen 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) • τ erhält kontextuelle Ordnung, gdw. wenn τ korrekt und vollständig bzgl der kontextuellen Ordnung ist, d.h. wenn ü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. Folgendes Bild zeigt den Normalfall bei Implementiewrungen. Sie sind nicht surjektiv, d.h. ewrreichen nicht jedes mögliche Programm. Das ist normalerweise auch zwingend, denn in der Kernsprache hat man meist mehr Manipulationsund Testtmöglichkeiten. Das bedeutet auch, dass man z.B. mit den Mitteln von P2 evtl. implementierte P1 -Programme unterscheiden kann, die innerhalb des Bildes von P1 , d.h. mit den implementierbaren Tests nicht zu unterscheiden sind. P1 P2 P3 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 35 Wir betrachten auch die Eigenschaften, die sich von der Sprache P1 ins Bild von P1 in der Sprache P2 übertragen: Hier gilt, dass die Erhaltung der Terminierung eine einfache, aber wirkungsvolle Bedingung ist: Aussage 5.5.3 Erhält die Übersetzung τ von P1 nach P2 die Terminierung, dann gilt, wenn man τ 0 als die Übersetzung von P1 in das Bild τ (P2 ) definiert: τ 0 erhält die kontextuelle Ordnung (bzgl. τ (P2 )). Beweis. Seien s, t P1 -Terme mit s ≤c,1 t, sei C2 ein Kontext in τ (P2 ), so dass C2 [τ (s)]⇓. Dann existiert ein Kontext C1 mit τ (C1 ) = C2 . Damit gilt wegen der Modularität von τ : τ (C1 [s]) = C2 [τ (s)], und wegen der Erhaltung der Terminierung: C1 [s])⇓. Da s ≤c,1 t, gilt auch C1 [t]⇓, und somit auch τ (C1 [t])⇓, und wegen C2 = τ (C1 ) auch Cs [τ (t)])⇓. Seien s, t P1 -Terme mit τ (s) ≤c,1 τ (t) und sei C1 ein Kontext mit C1 [s]⇓. Dann gilt τ (C1 )[s])⇓ wegen der Erhaltung der Terminierung. Wegen τ (s) ≤c,1 τ (t) und τ (C1 )[τ (s)])⇓ auch τ (C1 )[τ (t)])⇓ und somit auch τ (C1 [t])⇓. Erhaltung der Terminierung impliziert dann C1 [t])⇓. 2 Normalerweise kann man die Erhaltung der Terminierung leicht nachweisen, wenn die Reduktionen in P1 und P2 unter τ (fast) erhalten bleiben: ∗ D.h. wenn s → t auch τ (s) − → τ (t) impliziert und τ (s) − → τ (t) auch s → t impliziert, und für die entsprechenden Normalformen (WHNFs) gilt, dass wenn s eine WHNF in P1 ist, dann gilt τ (s)⇓ und wenn τ (s) eine WHNF in P2 ist, dann gilt auch s⇓. Diese Kriterien sind normalerweise erfüllt, wenn die Übersetzung aus operationaler Sicht in Ordnung ist“. Sind diese Kriterien nicht erfüllt, dann braucht ” man eine kompliziertere Argumentation. 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}. Diese Übersetzung ist modular. Aussage 5.5.4 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), Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 36 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.5 Die Übersetzung von KFPT nach KFP ist nicht vollständig bzgl. der kontextuellen Ordnung. Beweis. Betrachte den KFPT-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-case als 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 C := ([] (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 Der obige Fall ist eigentlich der allgemeine Fall von Übersetzungen. Der Unterschied zwischen den beiden Sprachen ließe sich beheben durch Veränderung der Sprache KFPT insbesondere ein anderes case. Diese caseVerä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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 5.5.2 37 KFPT+seq nach KFP Wenn wir seq zu KFPT hinzunehmen, gilt immer noch die Korrektheit der Übersetzung τ bzgl kontextueller Äquivalenz. Als Übersetzung nehmen wir wieder die oben angegebene und die Darstellung von seq in KFP. Aber obiges Gegen-Beispiel zur vollen Korrektheit wird ungültig. Trotzdem ist (das angepasste) τ nicht vollständig bei der Übersetzung von KFPT+seq nach KFP: Beispiel 5.5.6 Sei 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 True) 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, und haben sie jeweils das gleiche Verhalten. Wenn die Funktion f unterschiedliche Werte liefern soll, dann muss sie das Argument mittels eines caseanalysieren. Dann muss aber eins der Ergebnisse bot sein, denn das case in KFPT ist getypt. In KFP sind sie unterscheidbar, da man ein solches f f leicht definieren kann: das case in KFP kann bei True den Wert True zurückgeben, und bei Nil den Wert False. In diesem Fall liefert (s0 f f ) den Wert True, während (t0 f f ) zu bot auswertet, d.h. keinen Wert ergibt. Das bedeutet, dass die Definitionsmöglichkeit für den Operator strict bzw. seq nicht der eizige Unterschied ist, sondern dass die Typisierung auch noch einen Unterschied ausmacht: 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. 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 38 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. 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 39 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. Die Vergleichbarkeit der Normalordnungsreduktionen ist folgendermaßen: Eine SK-Beta-Reduktion lässt sich ausdrücken durch mehrere Beta-Reduktionen 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 40 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. 5.6.2 Ü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)) Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 41 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)) Beachte, dass ein Fixpunktkombinator in strikten funktionalen Programmiersprachen leicht abgewandelt werden muss: Y_str = \f. (\x . f (\z. x x z)) (\x . f (\z. x x z)), da sonst die strikte Argumentauswertung ein nichtterminierendes Auswerten erzwingt ∗ ∗ nach folgendem Schema: (Y F ) a − → F (Y F )a − → F (F (Y F ))a . . ., was durch ∗ obige Abänderung vermieden wird: (Ystr F ) a − → F (λz.Ystr F z)a . . .. 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. 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 42 ü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, d.h. das Programm ist: S1 x1 ... x_m_1 ... Sn x1 ... x_m_n main = e1 = en = e Wir nehmen wir an, dass die Zahlen 1, . . . , n, evtl. als extra Konstanten, verfügbar sind. Man macht eine erste Übersetzung, indem man alle Kombinatoren in eine rekursive Funktion einbaut: F i := case_int i of {1 -> \x1,...,x_m_1.e1[F 1/S1, ... F n/Sn]; ... ; n -> \x1,...,x_m_n.en[F 1/S1, ... F n/Sn]} Danach kann man die Rekursion von F eliminieren, indem man das obige Verfahren anwendet und Y (λf, i.ef [f /F ]) bildet, wobei ef der Rumpf des Kombinators F ist. Das funktioniert allerdings nur in einem weitgehend ungetypten Kalkül. Am Ende wird im Rumpf e des Kombinators main die entsprechende Ersetzung gemacht, d.h. Si wird ersetzt durch Y (\lambda f,i. e_f[f/F])~i. Diese Methode ist nicht elegant, da sie einerseits neue Konstruktoren einführt, und andererseits beim Hinzufügen von unabhängig definierten Kombinatoren die Übersetzung ändert, da jeweils das ganze Programm übersetzt werden muss. Beispiel 5.6.10 Angenommen, wir haben zwei verschränkt rekursiv definierte Funktionen f, g. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 43 f x = f (f x) (g x) g x = g x (f x) main = e Dann definieren wir eine Funktion G: G i = case_int i {1 -> \x. (F 1 (F 2 -> \x. (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) τ / τ (t0 ) no,∗ no t0 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) Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 44 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: 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 τ . 5.6.3 Übersetzung von Haskell nach KFP: Zusammenfassung Die Übersetzung von Haskell nach KFPTS+seq ist durch die reiche Syntax und die Möglichkeiten in Haskell etwas aufwändig. Wir machen die folgenden Vereinfachungen: • Basisdatentypen und Funktionen nehmen wir als in Haskell implementiert • List comprehensions nehmen wir an als übersetzt mittels Listenfunktionen. • Patterns sollen als geschachtelte cases aufgelöst sein. • monadische Programmteile ebenfalls als in purem Haskell Die dann erzeugten Programme kann man dann durch Weglassen der Typen nach KFPTS+seq übersetzen. Es ergeben sich die gleichen Eigenschaften in der Übersetzungskette Haskell → KFPTS+seq → KFPT+seq → KFP Haskell → KFPTS+seq D.h. KFPTS+seq → KFPT+seq KFPT+seq → KFP korrekt, aber vermutlich nicht vollständig vollständig korrekt, aber nicht vollständig Beispiel 5.6.14 Wir geben ein Gegenbeispiel zur Vollständigkeit der ersten Transformation an, wenn man statt Haskell KFPTS nimmt und nur polymorphe Typisierung verlangt, und keine Milner-Typisierung: Wir haben die kontextuelle Gleichheit in Haskell nicht formal exakt genug definiert, so dass wir ein Beispiel angeben, das robust ist unter verschiedenen Varianten. Die Idee ist, dass g testet, ob die eingegebene Funktion f die Argumente True, Nil und True, (Cons bot bot) als auch False, True und False, False unterscheiden kann. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 45 g f = if (if (f True Nil) then if (f True (Cons bot bot)) then bot else True e else bot) && (if (f False True) then if (f False False) then bot else True else bot) then True else bot Die Behauptung ist, dass g in KFPTS+seq+poly“ äquivalent zu ⊥ ist, da man ” keinen polymorph getypten Kontext angeben kann, der beide unterscheidet. Bzw. für alle KFPTS+seq+poly“-Funktionen f, die polymorph getpyt sind, kann sich ” nur bot bei der Auswertung von g f ergeben, d.h. es gibt keinen Unterschied. Aber in KFPT+seq kann man leicht eine Funktion f angeben, so dass g f den Wert True liefert: zwei geschachtelte cases sind dazu nötig. Beispiel 5.6.15 Auch unter Milner-Typisierung gibt es Gegenbeispiele: s x y = t x y if isBool x then = if isBool x then (seq y True) else isBool y isBool y else isBool y isBool x = case_Bool x of True->True, False-> True Dann ist s ∼ t in Milner-getyptem KFPTS + seq, aber nicht in in KFPTS + seq, da man die Argumente True,Nil nehmen kann, bei der sich die Funktionen unterscheiden. In der Summe ergibt sich, dass die Übersetzung von Haskell nach KFP korrekt ist. Wir können somit Gleichungen s ∼c t in Haskell zeigen, indem wir diese nach KFP übersetzen und dort nachweisen. Bei Widerlegungen von Gleichungen in KFP muss man sich vergewissern, dass die Widerlegung auch in Haskell funktioniert, bzw. man muss ein Gegenbeispiel in Haskell angeben: I.a. ist die Bedingung hier nur, dass das Gegenbeispiel polymorph getypt sein muss. 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 46 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. Dieser 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 ). 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[sj ] ≤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) 47 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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 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 .{z . . 2} mit m ≥ n hat. D.h. der Pfad von der Wurzel zur Position hat ein 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. 48 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 • 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. • 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 5.7.1 49 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 )). 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ässige 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, dass x ≤c y ein zulässiges Prä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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 50 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. 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 51 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 ]⇓. 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: take n xs = case n of 0 -> case xs of append xs ys = case xs of {[] length xs = case xs of {[] concat xs = case xs of {[] [], n+1 -> [] -> [];(y:ys)-> y: (take n ys) -> ys; u:us -> u:(append us ys) -> 0 ; y:ys -> 1 + (length ys)} -> []; u:us -> append u (concat us)} Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 52 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)} Aussage 5.7.21 (Take-Lemma) Gegeben zwei Listenausdrücke s, t. Dann gilt s ∼c t gdw. für alle n > 0: take n s ∼c take n t Beweis. Der Beweis kann leicht mittels Bisimulation geführt werden: Es folgt aus der Voraussetzung, dass die beiden Listen äquivalente Elemente haben, sofern die Elemente erreichbar sind. Es gilt auch, dass (drop n s)⇓ ⇔ (drop n t)⇓. 2 Vorsicht: Man kann aus s!!n ∼c t!!n für alle n ≥ 0 nicht schließen, dass s ∼c t ist, da z.B. ⊥ 6∼c [⊥]. Beispiel 5.7.22 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.23 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. • 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.24 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.25 Ein Induktionsschema zum Nachweis von Eigenschaften P endlicher Listen, wobei P nicht stetig zu sein braucht. • Zeige P ([]) Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 53 • Zeige P (xs) ⇒ P (x : xs). Dann kann man P (xs) für alle endlichen Listen xs schließen. Definition 5.7.26 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.27 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 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.28 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. 54 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 • 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 Bemerkung 5.7.29 P (x) ≡ reverse (reverse xs) = xs ist ein Beispiel für ein stetiges (zulässiges) 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.30 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 55 Beispiel 5.7.31 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.32 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. Übungsaufgabe 5.7.33 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: Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 56 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 Wenn man nur ein Halbgruppe hat, kann es auch ein Nullelement geben; ⊥ wirkt bei strikten Operatoren wie ein Nullelement. f bot x = bot Nullelement f x bot = bot Nullelement Erstes Dualitätsgesetz für FOLD Für alle endlichen Listen xs mit Elementen aus dem Monoid oben 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.) Zuerst muss man mit Induktion nachweisen, dass man im Monoid bleibt, d.h. die Ergebnisse des fold und foldr sind wieder im Monoid. Danach die Gleichung Mit Induktionsschema []; x:xs Für die leere Liste gilt: foldr f e [] = e = foldl f e [] 57 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 Für eine Liste [x] gilt: foldr f e [x] = f x e = x Für eine Liste x1:x2:xs gilt: foldr f e (x1:x2:xs) Assoziativität des Operators f ergibt: f (f x1 x2) (foldr f e xs)) = foldr f e (f x1 Dieselbe Rechnung für foldl ergibt: foldl f e (x1:x2:xs) = foldl f (f (f e x1) ergibt foldl f (f e (f x1 x2)) xs = foldl f kann man Induktion über die Länge machen. = f e x = foldl f e [x] = f x1 (f x2 (foldr f e xs)). das ist gleich x2) : xs)) x2) xs. Assoziativität e (f x1 x2):xs. Jetzt Bemerkung 5.7.34 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. Im Fall unendlicher Listen sieht man schnell, dass dann das Ergebnis jeweils ⊥ ist, und deshalb gleich. 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 • x 0 f0 e = e 0 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 [] Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 58 • 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.35 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 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 Für alle endlichen Listen xs gilt: foldr f e xs = foldl (flip f ) e (reverse xs) Für alle Listen xs gilt: 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 59 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) = 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.36 Ist foldr (:) [] = Id ? Beispiel 5.7.37 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.38 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 60 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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: foldl f a . map g = foldl (\x,y -> f x (g y)) a Beispiel 5.7.39 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 61 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 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.40 Wie sieht das Gesetz für foldl aus? foldl f a . concat = ??? FOLD-FILTER Verschmelzung Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 62 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) -- 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 63 • 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. x 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 = randbt (\x,y -> (f x):y) Der Beweis sei als Übungsaufgabe dem Leser überlassen. 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 64 -- 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: foldrgt op a . mapgt f = 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: 2 ein Grundtyp ist ein Typ ohne Variablen x Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 65 • 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 (⊥). • 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 NT x K x y = = 66 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: no,∗ no no,∗ no no no • t −−−→ R[s1 ] −→ R[s2 ] −→ . . . −→ R[sn ], wobei sn in WHNF ist, und danach wird evtl. auch R durch die Normalordnungsreduktion verändert no • 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 67 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. 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 korrekte 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 68 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. 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.10 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 5.8.2 69 Strikte Superkombinatoren Definition 5.8.11 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. 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.12 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, 25. Januar 2007 5.9 70 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 . 71 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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 Simulation kopiert, aber mit gleicher Markierung versehen. Die Beta-Reduktion wird jeweils analog auf allen gleich markierten Termen durchgeführt (und zählt Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 72 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 . { t CCC CC {{ { CC { t CC {{ >>> { ! { } >> lazy >> t00 t0 B > { B lazy { lazy 0 o_ _ _ _ _ _ 00 B t t { B! }{ ∃t000 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 ]]. lazy Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 73 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). tC {{ CCC aos { CC { CC {{ { C! }{{ 0 t B t00 B aos lazy { { B { B! }{ 000 ∃t lazy 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, 74 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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. 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 Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 75 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). 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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 76 • Sie kann einen Term R[xi ] ergeben, wobei R ein Reduktionskontext ist. Das ist Fall 5.9.9. 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) . . .) Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 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 = = = = = = = = = = 77 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))) (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. Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 78 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. 5.10.1 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. Ursprüngliche Motivation war das Finden einer einfacheren Sprache, die mit wenigen Kombinatoren auskommt, volle Ausdrucksstärke hat, aber in der die unangenehme Operation der Ersetzung von Variablen durch Terme (in der betaReduktion) durch etwas einfacheres ersetzt ist. 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) Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007 79 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 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. Die Ausdrücke können sich exponentiell vergrößern bei dieser Übersetzung: λx1 , . . . , xn , xn+1 .xn+1 → [x1 ] . . . [xn ]K → [x1 ] . . . [xn−1 ](K K) → [x1 ] . . . [xn−2 ](S ([xn−1 ]K) ([xn−1 ]K)) → [x1 ] . . . [xn−2 ](S (K K) (K K)) ... Man sieht dass die Anzahl der Vorkommen von K am Ende mindestens 2n ist