A Non-Deterministic Call-By-Need Lambda Calculus: Proving Similarity a Precongruence by an Extension of Howe’S Method to Sharing
Total Page:16
File Type:pdf, Size:1020Kb
A Non-Deterministic Call-by-Need Lambda Calculus: Proving Similarity a Precongruence by an Extension of Howe's Method to Sharing Dissertation zur Erlangung des Doktorgrades der Naturwissenschaften vorgelegt beim Fachbereich 15 der Johann Wolfgang Goethe{Universit¨at in Frankfurt am Main von Matthias Mann aus Frankfurt Frankfurt 2005 (D F 1) vom Fachbereich Informatik und Mathematik der Johann Wolfgang Goethe{ Universit¨at als Dissertation angenommen. Dekan: Prof. Dr.-Ing. Detlef Kr¨omker Gutachter: Prof. Dr. Manfred Schmidt-Schauß Johann Wolfgang Goethe-Universit¨at Fachbereich Informatik und Mathematik Robert-Mayer-Str. 11-15 60325 Frankfurt Prof. David Sands, Ph.D. Chalmers University of Technology and University of G¨oteborg Department of Computing Science S-412 96 G¨oteborg Sweden Datum der Disputation: 17. August 2005 Zusammenfassung Gegenstand der vorliegenden Arbeit ist ein nicht-deterministischer Lambda- Kalkul¨ mit call-by-need Auswertung. Lambda-Kalkule¨ spielen im allgemeinen eine grundlegende Rolle in der theoretischen Informatik, sei es als Basis fur¨ Theorembeweiser im Bereich der Verifikation oder beim Entwurf sowie der se- mantischen Analyse von Programmiersprachen, insbesondere funktionalen. Im Zuge der weiter zunehmenden Komplexit¨at von Hard- und Softwaresyste- men werden funktionale Programmiersprachen eine immer gr¨oßere Bedeutung erlangen. Der Grund liegt darin, daß in der Softwareentwicklung stets eine ge- wisse Diskrepanz entsteht zwischen dem Konzept, welches ein Programmierer entwirft, und der Art und Weise, wie er es in einer bestimmten Programmier- sprache umzusetzen hat. Um robuste und verl¨aßliche Software zu gew¨ahrleisten, sollte es eines der Hauptanliegen der Informatik sein, diese Diskrepanz so klein wie m¨oglich zu halten. Im Vergleich zu imperativen oder objekt-orientierten ist dies bei funk- tionalen Programmiersprachen der Fall, weil sie aufgrund ihrer Herkunft aus der Semantik st¨arker am Ergebnis einer Berechnung orientiert sind. Imperati- ve bzw. objekt-orientierte Programmiersprachen hingegen betonen oft zu stark, welche Schritte notwendig sind, um das gewunsc¨ hte Ergebnis zu erhalten. Lambda-Kalkul¨ und funktionales Programmieren Obwohl der λ-Kalkul¨ die konzeptionelle Grundlage der funktionalen Sprachfa- milie bildet, spiegelt er die Implementierung moderner funktionaler Program- miersprachen nicht angemessen wider. Dies trifft besonders fur¨ die sogenannten verz¨ogert auswertenden funktionalen Programmiersprachen zu, d.h. solche, bei denen Argumente nur dann ausgewertet werden, wenn sie zur Ermittlung des Ergebnisses beitragen. Denn um eine Mehrfachauswertung bei der Anwendung i einer Funktion auf nicht vollst¨andig ausgewertete Argumente zu vermeiden, ver- wenden alle heutzutage relevanten Implementierungen Graphreduktion, d.h. sie operieren nicht auf einem Termbaum sondern auf einem Graphen. Ein Beispiel mag dies verdeutlichen: Die Ausdruc¨ ke λx.(x + x) und λx.(2x) stellen beide ei- ne Funktion dar, die ihr Argument x verdoppelt. Eine Anwendung auf z.B. das noch nicht vollst¨andig ausgewertete Argument (5 − 3) wurde¨ bei der ublic¨ hen call-by-name Auswertung, d.h. die Argumentausdruc¨ ke werden einfach fur¨ die Argumentvariablen eingesetzt, aber (5 − 3) + (5 − 3) bzw. 2(5 − 3) liefern. Offensichtlich wurde¨ dann das Ergebnis des Ausdruckes (5−3) bei der ersten Variante zwei Mal ermittelt, was ub¨ erflussig¨ ist, da das zweimalige Vorkommen des Ausdruckes (5 − 3) ein- und denselben Wert bezeichnet. Indem diese Infor- mation in einem Graphen gespeichert wird, umgeht die Implementierung diese Mehrfachberechnung. Dies ist aber nur zul¨assig, weil in funktionalen Program- miersprachen das Prinzip der referentiellen Transparenz erfullt¨ ist, d.h. ein Aus- druck ver¨andert w¨ahrend der Ausfuhrung¨ eines Programmes nicht seinen Wert1 sondern wird nur in eine andere, ¨aquivalente Form ub¨ erfuhrt.¨ Um die operationale Semantik einer solchen Implementierung mittels Gra- phreduktion entsprechend abzubilden, werden sogenannte call-by-need Kalkule¨ herangezogen. Bei diesen wird durch eine geschickte Wahl der Kalkulregeln¨ si- chergestellt, daß Ausdruc¨ ke erst kopiert werden k¨onnen, wenn sie in einer be- stimmten Form vorliegen, die man als Basiswert bezeichnen k¨onnte. Der Charak- ter der verz¨ogerten Auswertung bleibt hierbei freilich erhalten, d.h. eine Funk- tionsanwendung vermag sofort ein Ergebnis zu liefern, sobald alle relevanten Argumentausdruc¨ ke ausgewertet sind. Fur¨ das genannte Beispiel bedeutet das, daß die Anwendung von λx.(x + x) auf das Argument (5 − 3) zuerst in einen Ausdruck der Form let x = (5 − 3) in (x + x) ub¨ erfuhrt¨ wird, wobei das let- Konstrukt hier die Graphdarstellung, das Sharing, explizit darstellt. Erst wenn das Ergebnis 2 fur¨ den Term (5 − 3) ermittelt ist, wird es fur¨ x in den Rumpf x + x eingesetzt und somit die Mehrfachberechnung vermieden. Nichtdeterminismus und Gleichheit Die mitunter wichtigste Frage in einem Lambda-Kalkul,¨ nicht nur call-by-need betreffend, ist wohl, welche Arten von Termen kopiert werden durfen.¨ Wesent- lich beinflußt wird hierdurch auch die Frage, welche Programmtransformationen zul¨assig sind bzw. welche Gleichheiten auf Programmen gelten sollen. 1 Man beachte, daß diese Aussage fur¨ imperative Programmiersprachen nicht zutrifft. ii Wie das oben angefuhrte¨ Beispiel bereits verdeutlicht hat, sollten die beiden Ausdruc¨ ke λx.(x+x) und λx.(2x) als gleich betrachtet werden. Anderseits kann die im λ-Kalkul¨ ublic¨ he (β)-Regel nicht mehr uneingeschr¨ankt gelten. (λx.M)N = M[N=x] (β) Hierbei bezeichnet M[N=x] die Einsetzung des Argumentes N im Term M fur¨ die Variable x, was, wir im obigen Beispiel gesehen haben, zu Mehrfachauswer- tungen fuhren¨ kann. Wie aber l¨aßt sich nun ein ad¨aquater Gleichheitsbegriff finden, wenn doch auch der Ausdruck (5 − 3) + (5 − 3) dasselbe Ergebnis, n¨amlich 4, liefert? Es genugt¨ also offensichtlich nicht, nur die Ergebnisse von Auswertungen zu be- trachten. Einige Arbeiten verfolgen daher den Ansatz, die Anzahl der Auswer- tungsschritte zu z¨ahlen, was auch erfolgversprechend scheint. Diese Arbeit jedoch schl¨agt einen anderen Weg ein: Es wird ein Sprachkon- strukt pick eingefuhrt,¨ welches nichtdeterministisch eines seiner beiden Argu- menten ausw¨ahlt. Nichtdeterminismus in dieser Form verletzt offensichtlich die referentielle Transparenz, hilft aber im Gegenzug zu erkennen, wann Sharing erhalten bleibt bzw. wann nicht: Die beiden Ausdruc¨ ke pick 5 3 + pick 5 3 und let x = pick 5 3 in (x + x) liefern nun unterschiedliche Mengen von m¨oglichen Resultaten, n¨amlich f 6; 8; 10 g gegenub¨ er f 6; 10 g fur¨ den let-Ausdruck. Darub¨ erhinaus gibt es eine Vielzahl von Anwendungsgebieten fur¨ nichtde- terministische Lambda-Kalkule,¨ u.a. als theoretische Grundlage fur¨ nebenl¨aufige Prozesse oder von deklarativer, Seiteneffekt-behafteter Ein-/Ausgabe. Insbeson- dere im Bezug auf das letztere sind an der Professur schon einige Arbeiten durchgefuhrt¨ worden, z.B. die Dissertation von Arne Kutzner oder der tech- nische Bericht zum Kalkul¨ FUNDIO, zu denen sich diese als eine Erg¨anzung bzw. Fortfuhrung¨ versteht. Denn die Kenntnis ub¨ er die in einem Kalkul¨ geltenden Gleichheiten spielt ei- ne herausragende Rolle fur¨ dessen Verst¨andnis. Die bisherigen Arbeiten stutzen¨ sich dabei haupts¨achlich auf den Begriff der kontextuellen Gleichheit, durch die zwei Terme s und t miteinander identifiziert werden, wenn deren Auswertung in allen m¨oglichen Programmkontexten C[ ] dasselbe Terminierungsverhalten, gekennzeichnet mit +, zeigt: s 'c t () (8C : C[s] + () C[t] +) Die kontextuelle Gleichheit stellt ein m¨achtiges Werkzeug fur¨ die Beurteilung der Korrektheit von Programmtransformationen dar, insbesondere weil sie per iii Definition eine Kongruenz ist, d.h. ihre Gleichheiten wiederum in Kontexte ein- setzbar sind, also die Implikation s 'c t =) (8C : C[s] 'c C[t]) erfullt.¨ Mitunter sind Gleichheiten aber schwierig nachzuweisen, weil ja die Terminie- rung der Auswertung in allen m¨oglichen Kontexten betrachtet werden muß. Daher ist es oftmals sinnvoll, einen anderen Aquiv¨ alenzbegriff zu Hilfe zu nehmen. Die Rede ist von der Methode der Bisimulation, welche einen st¨arker stufenweisen Gleichheitstest beschreibt. Hier werden zwei Terme als gleich be- trachtet, wenn sie auf allen m¨oglichen Stufen dasselbe Verhalten zeigen, solange man also nicht das Gegenteil nachweisen kann. Die Technik fand zuerst im Be- reich der Zustandsub¨ ergangssysteme Anwendung, ist mittlerweile aber auf dem Gebiet der funktionalen Programmierung und Lambda-Kalkule¨ gut etabliert und existiert in verschiedenen Auspr¨agungen. Der Stil, den Abramsky appli- cative bisimulation nennt, ist am einfachsten in Form einer Pr¨aordnung . zu illustrieren. s . t () (s + λx.s0 =) (t + λx.t0 ^ 8r : (λx.s0) r . (λx.t0) r)) Dies ist nicht etwa eine zyklische Definition, sondern als Fixpunktgleichung zu verstehen, ub¨ er welcher der gr¨oßte Fixpunkt gebildet wird. Dadurch wird das Beweisprinzip der Co-Induktion anwendbar. Setzt man ∼ = . \ & so ist leicht zu sehen, daß ∼ ⊆ 'c gefolgert werden kann, vorausgesetzt ∼ ist eine Kongruenz. Gilt die Einsetzbarkeit in Kontexte fur¨ eine Pr¨aordnung, so wird von einer Pr¨akongruenz gesprochen. Es ist also zu zeigen, daß die Relation ., genannt similarity, eine Pr¨akongruenz ist. Nicht nur dieser Beweis sondern bereits der Entwurf der passenden Relation fur¨ einen nichtdeterministischen call-by-need Lambda-Kalkul¨ stellt einen wesentlichen Er- kenntnisgewinn dar. Denn der ublic¨ he Ansatz fur¨ die Similarity-Definition