Fakultät für Informatik Technische Universität München Die Programmiersprache Clean und Uniqueness Typing Author: Julian Biendarra Betreuer: Peter Lammich Seminar: Fortgeschrittene Konzepte der funktionalen Programmierung Semester: Sommersemester 2015 Datum: 26. Mai 2015 Inhaltsverzeichnis Inhaltsverzeichnis 1 1 Die Programmiersprache Clean 2 2 Syntaxvergleich mit Haskell 2.1 Hauptprogramm . . . . . . . . . . 2.2 Mehrere Funktionsparameter . . . 2.3 List Comprehension . . . . . . . . 2.4 Definition von Operatoren . . . . . 2.5 Kleinere Syntaktische Unterschiede 2.6 Definition neuer Typen . . . . . . . 2.7 Spezielle Typklassen . . . . . . . . . . . . . . . 2 2 3 3 3 4 4 4 3 Uniqueness Typing 3.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Theorie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Uniqueness Typing in Clean 4.1 Uniqueness-Attribute . . . . . . . . . . . . . . . . . 4.2 Uniqueness-Variablen . . . . . . . . . . . . . . . . . 4.3 Uniqueness Propagation . . . . . . . . . . . . . . . 4.4 Uniqueness bei Definition algebraischer Datentypen 4.5 Uniqueness bei curried Anwendung von Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 8 8 9 10 10 5 I/O in Clean 5.1 I/O Beispiel: cat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2 Hash Lets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 12 6 Zusammenfassung 13 Literaturverzeichnis 14 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Die Programmiersprache Clean Clean1 wurde im Jahre 1984 an der University of Nijmegen in den Niederlanden aus einer Teilmenge der Programmiersprache Lean2 entwickelt [5, 10]. Wie Haskell ist Clean eine rein funktionale Programmiersprache. D. h. sie erfüllt insbesondere das Prinzip der referenziellen Transparenz (Definition siehe Abschnitt 3.1) und damit auch das Prinzip der einheitlichen Substitution (engl. uniform substitution). Dieses besagt, dass in einem Ausdruck ein Teilausdruck (z. B. ein Argument einer Funktion) durch seine Definition ersetzt werden kann, ohne den Wert des Gesamtausdrucks zu verändern. Insbesondere werden durch dieses Prinzip Seiteneffekte verhindert. Um dennoch beispielsweise Dateizugriffe zu ermöglichen, verwendet Clean das so genannte Uniqueness Typing. Das Uniqueness Typing ist eine Erweiterung des klassischen Hindley/Milner/Mycroft Typsystems, die die Möglichkeit bietet, alleinigen Zugriff auf Variablen zu haben. Dieser alleinige Zugriff ermöglicht dann destruktive Veränderungen an dem Objekt, z. B. Dateizugriffe. Die Semantik von Clean basiert auf Term Graph Rewriting Systems [10]. D. h. Clean Programme werden intern als Regeln zum Umschreiben von Graphen (engl. graph rewrite rules) betrachtet. Die Graphen sind dabei die Ausdrücke, die ausgehend vom initialen Ausdruck mit Hilfe der graph rewrite rules (den Funktionsdefinitionen) entwickelt werden. Durch die interne Repräsentation als Graph ist es möglich, Objekte zu teilen bzw. die Zahl der Referenzen zu beschränken (siehe Kapitel 3 und 4) oder zyklische Strukturen zu erstellen [10]. Für eine genauere Einführung des Term Graph Rewriting Systems sei auf [6, 9] verwiesen. In der vorliegenden Arbeit soll zu Beginn die Syntax von Clean, insbesondere in Abgrenzung zu Haskell behandelt werden (Kapitel 2). Schwerpunkt der Arbeit werden die Einführung des Uniqueness Typing (Kapitel 3) und dessen Umsetzung in Clean (Kapitel 4) sein. Als Anwendung des Uniqueness Typing wird ein Beispiel der Verwendung der I/O Library von Clean betrachtet (Kapitel 5). 2 Syntaxvergleich mit Haskell Die Syntax von Clean (Referenz: [10]) ist der von Haskell sehr ähnlich. Beispielhaft sei hier die rekursive Berechnung der Fibonacci-Zahlen angegeben3 : 1 2 3 4 fib fib fib fib :: Int -> Int 0 = 0 1 = 1 n = fib (n − 1) + fib (n − 2) Die Syntax für diese Funktion ist sowohl mit Clean als auch mit Haskell kompatibel. Im Folgenden werde ich auf die wichtigsten syntaktischen Unterschiede zwischen Clean und Haskell eingehen. Der Code in Clean steht dabei immer links und der äquivalente Code in Haskell steht rechts. Eine kurze Übersicht ist auch in [1] zu finden. 2.1 Hauptprogramm In Clean heißt das Hauptprogramm, das beim Start der Datei ausgeführt wird, Start. Anders als main in Haskell kann Start einen beliebigen Rückgabetyp haben, der dann für die Darstellung 1 Clean Wiki: http://clean.cs.ru.nl/Clean Language of East-Anglia and Nijmegen 3 Dies ist eine naive rekursive Implementierung und kann natürlich z. B. mit Hilfe von Dynamischer Programmierung effizienter implementiert werden. 2 2 auf dem Bildschirm in einen String umgewandelt wird4 : Clean 1 2 Start :: Int Start = fib 4 Haskell 1 2 main :: IO () main = print (fib 4) Wie man an diesem Beispiel sieht, können Funktionen in Clean auch mit einem Großbuchstaben beginnen. Variablen müssen allerdings immer mit einem Kleinbuchstaben beginnen. 2.2 Mehrere Funktionsparameter In Clean wird der Typ einer Funktion üblicherweise5 in der „uncurried“ Form hingeschrieben, d. h. alle Typen der Parameter werden mit Leerzeichen getrennt und der Rückgabetyp wird davon mit -> getrennt. Diese Syntax soll die interne Implementierung von Funktionen widerspiegeln. Eine partielle Anwendung (currying) ist unabhängig davon auch in Clean möglich. 1 2 add :: Int Int -> Int add a b = a + b 2.3 1 2 add :: Int -> Int -> Int add a b = a + b List Comprehension Bei der List Comprehension in Clean werden die Generatoren (z. B. a <- as) und die Filter (test a) getrennt. Die Grundstruktur ist [f a \\ a <- as | test a] (siehe auch Z. 1). Außerdem können mehrere Generatoren sowohl unabhängig voneinander (mit , getrennt, Z. 2) oder gleichzeitig durchlaufen werden (mit & getrennt, Z. 3). Letzteres ist äquivalent zu zip. 1 2 3 [i * i \\ i <- [1..10] | isOdd i] [(a, b) \\ a <- [1..2], b <- [1..3]] [(a, b) \\ a <- [1..2] & b <- [1..3]] 1 2 3 [i * i | i <- [1..10], odd i] [(a, b) | a <- [1..2], b <- [1..3]] zip [1..2] [1..3] Die Werte der einzelnen Ausdrücke sind: 1 [1, 9, 25, 49, 81] 2 [(1,1), (1,2), (1,3), (2,1), (2,2), (2,3)] 3 [(1,1), (2,2)] 2.4 Definition von Operatoren Die Definition von Operatoren ist in beiden Sprachen sehr ähnlich. Lediglich die Priorität sowie die Assoziativität werden an unterschiedlichen Stellen im Code angegeben. Bei Haskell normalerweise am Beginn der Datei und bei Clean bei der Typdeklaration des Operators. Das folgende Beispiel des Potenz-Operators wurde [7] entnommen: 1 2 3 4 (^) infixr 8 :: Int Int -> Int (^) x 0 = 1 (^) x n = x * x ^ (n-1) 1 2 3 4 4 infixr 8 ^ (^) :: Int -> Int -> Int (^) x 0 = 1 (^) x n = x * x ^ (n-1) Es gibt in Clean noch eine weitere Variante von Start, in der Seiteneffekte wie bei main :: IO () möglich sind. Diese wird in Kapitel 5 behandelt. 5 Die „curried“ Form wie in Haskell ist auch in Clean möglich, aber eher unüblich. 3 2.5 Kleinere Syntaktische Unterschiede Die folgende Tabelle zeigt kleinere, rein syntaktische Unterschiede zwischen Clean und Haskell: Alias beim Pattern Matching func pair=:(a, b) = ... func pair@(a, b) = ... Cons Operator [x:xs] x:xs Funktionskomposition f o g f . g Typklassen max :: a a -> a | Ord a max :: (Ord a) => a -> a -> a Unäres Minus ~ 1 - 1 2.6 Definition neuer Typen Neue Typen können in Clean mit :: definiert werden. Die Unterscheidung zwischen der Definition von Typsynonymen und von neuen (algebraischen) Datentypen findet über unterschiedliche Zuweisungsoperatoren statt. Bei Typsynonymen wird :== und bei der Definition algebraischer Datentypen wird wie üblich = verwendet. Bei der Record Syntax ist zu beachten, dass die Definition keinen Konstruktor enthält. Daher wird auch bei der Erzeugung eines Records kein Konstruktor verwendet. Typsynonyme 1 :: Text :== String 1 Algebraische Datentypen 1 2 :: Tree a = Empty | 1 Node a (Tree a) (Tree a) 2 Record Syntax 1 2 3 4 5 6 7 :: Student = { name :: String, matrikelNr :: Int} 2.7 s :: Student s = {name = "Name", matrikelNr = 42} 1 2 3 4 5 6 7 type Text = String data Tree a = Empty | Node a (Tree a) (Tree a) data Student = Student { name :: String, matrikelNr :: Int} s :: Student s = Student {name = "Name", matrikelNr = 42} Spezielle Typklassen In Clean werden im Modul StdOverloaded verschiedene Typklassen bereitgestellt. Anders als in Haskell stellen diese in der Regel nur eine überladene (engl. overloaded) Funktion (z. B. +) zur Verfügung und werden nach dieser benannt. Zusätzlich werden im Modul StdClass einige dieser Typklassen zusammengefasst. Beispielsweise ist class PlusMin a | +, -, zero a die Zusammenfassung für die Addition und Subtraktion. zero ist eine spezielle Typklasse, die ein polymorphes Objekt zero :: a bereitstellt. Dieses bildet das neutrale Element der Addition, d. h. für Int wäre zero = 0. Nützlich ist dies, wenn man beispielsweise eine rekursive, polymorphe Funktion mit + definieren möchte. Beim folgenden Beispiel ist zu beachten, dass die Haskell-Funktion nicht äquivalent zur Clean-Funktion ist: Da es kein direktes Äquivalent der Typklassen + und zero in Haskell gibt, wurde auf die Typklasse Num zurückgegriffen. Für diese ist gibt es den polymorphen Wert (Num a) => 0 :: a. 4 1 2 3 sum :: [a] -> a | +,zero a sum [] = zero sum [x:xs] = x + sum xs 3 3.1 1 2 3 sum :: (Num a) => [a] -> a sum [] = 0 sum (x:xs) = x + sum xs Uniqueness Typing Motivation Eine wichtige Eigenschaft, die rein funktionale Programmiersprachen (wie Haskell oder Clean) erfüllen, ist die referenzielle Transparenz. Referenzielle Transparenz (engl. referential transparency) bedeutet, dass der gleiche Ausdruck bei jeder Auswertung das gleiche Ergebnis haben muss [11]. Zum Beispiel kann man davon ausgehen, dass beim Aufruf von add 1 2 stets 3 herauskommt, egal an welcher Stelle im Code dieser Term steht. Wenn eine Programmiersprache referenzielle Transparenz erfüllt, erleichtert dies beispielsweise Korrektheitsbeweise von Programmen dieser Sprache. Denn in der Beweisführung kann ein Teilausdruck, z. B. ein Funktionsaufruf, durch dessen Wert ersetzt werden, ohne etwas am Wert des Gesamtausdrucks zu verändern. Insbesondere bei der Ein-/Ausgabe ist Implementierung von referenzieller Transparenz aber eine Herausforderung, da I/O-Befehle von Natur aus Seiteneffekte haben und somit auch andere Ausführungen von I/O-Befehlen beeinflussen können6 . Beispielsweise beeinflusst das Schreiben in eine Datei, das Lesen aus derselben Datei. Ein Beispiel, wo diese Verletzung der referenziellen Transparenz auftritt und wie diese umgangen werden kann, wird im Abschnitt 3.2 behandelt. Wie referenzielle Transparenz (und speziell der I/O-Zugriff) umgesetzt wird, hängt stark von der jeweiligen Programmiersprache ab. Haskell verwendet zum Beispiel Monaden für die Kapselung von I/O Zugriffen, Clean hingegen verwendet das sogenannte Uniqueness Typing, welches in diesem Kapitel behandelt wird. 3.2 Einführung Das folgende Beispiel zur Einführung von Uniqueness Typing basiert auf [11]. Angenommen wir möchten zwei Zeichen aus einer gegebenen Datei auslesen. In C kann dafür die Funktion fgetc verwendet werden. Die folgende C-Funktion bekommt als Parameter einen Zeiger auf eine bereits geöffnete Datei und liest aus dieser nacheinander mit fgetc insgesamt zwei Zeichen aus: 1 int fget2c(FILE∗ file) 2 { 3 int a = fgetc(file); 4 int b = fgetc(file); 5 return a + b; 6 } In Zeile 3 und 4 wird zweimal die Funktion fgetc mit dem selben Parameter (file) aufgerufen. Wäre referenzielle Transparenz gegeben, müsste bei beiden Funktionsaufrufen derselbe Wert 6 Diese Arbeit beschränkt sich auf die Erhaltung der referenziellen Transparenz bei I/O Zugriffen. Es sei aber darauf verwiesen, dass das Uniqueness Typing auch an anderen Stellen nützlich ist. Z. B. kann durch in-place Updates bei Arrays das Laufzeitverhalten und der Speicherbedarf entschieden verbessert werden. 5 zurückgegeben werden. Dies ist in der Implementierung von fgetc allerdings nicht gegeben7 , da diese den Pointer, der auf das nächste zu lesende Zeichen in der Datei verweist, auf das nächste Zeichen setzt, nachdem das Zeichen gelesen wurde. Angenommen, es wird nur an einer einzigen Stelle im Programmablauf auf file zurückgegriffen. Dann kann auch es keinen zweiten Aufruf derselben Methode mit file als Parameter geben, d. h. die referenzielle Transparenz wird nicht verletzt. Um weiterhin mit der Datei arbeiten zu können, wird eine neue Referenz auf die Datei zurückgegeben. Die folgende Methode erfüllt die referenzielle Transparenz: 1 fget2c file0 = 2 let 3 (a, file1) = fgetc file0 4 (b, file2) = fgetc file1 5 in 6 (a + b, file2) file0, file1 und file2 zeigen zwar alle auf die selbe Datei unterscheiden sich allerdings im Wert des Pointers, der auf das nächste zu lesende Zeichen zeigt. Eine Verletzung der referenziellen Transparenz wie 1 let (a, file1) = fgetc file0 2 (b, file2) = fgetc file0 3 in ... führt zu einem Compilerfehler, da der Clean-Compiler bei der Typüberprüfung merkt, dass file0 nicht erneut verwendet werden darf. Wie wir dem Compiler mitteilen, dass wir alleinigen (engl. unique) Zugriff auf eine Variable haben möchten, und wie der Compiler Verletzungen dieser Eigenschaft erkennt, wird in Abschnitt 3.3 allgemein und in Kapitel 4 für Clean behandelt. 3.3 Theorie Die in diesem Abschnitt vorgestellte Theorie basiert im Wesentlichen auf dem Uniqueness Typing von Vries et al. [11]. Dies stellt in manchen Punkten eine Vereinfachung gegenüber dem Uniqueness Typing in Clean dar. Das Uniqueness Typing in Clean wird anschließend in Kapitel 4 behandelt. Wie im vorherigen Abschnitt gesehen, kann die referenzielle Transparenz dadurch sichergestellt werden, dass zu schützende Variablen als unique markiert werden. Der Compiler achtet dann darauf, dass die Variable im Code maximal einmal verwendet wird, d. h. dass stets nur eine Referenz auf die Variable besteht. Die Information, ob eine Variable erneut verwendet werden darf, ist eine Eigenschaft bzw. ein Attribut des entsprechenden Typs der Variable. In Konsistenz mit [11] werde ich in diesem Abschnitt type• für einen unique Typ und type× für einen non-unique Typ verwenden. Wenn noch nicht festgelegt ist, ob der Typ unique oder non-unique ist, wird eine Variable verwendet, z. B. typeu . Auch Funktionen haben ein Uniqueness-Attribut, dargestellt über dem Pfeil. Für die Funktion fgetc wäre der Typ beispielsweise [11]8 : 7 Das heißt insbesondere, dass C wie die meisten nicht-funktionalen Programmiersprachen referenzielle Transparenz nicht erfüllt. 8 Das Äquivalent zu fgetc in Clean ist die Funktion freadc ::∗ File -> (Bool, Char,∗ File) [8]. Dabei ist ein mit ∗ markierter Typ unique (•) und ein unmarkierter Typ non-unique (×). In diesem Fall wurde also u auf unique festgelegt sowie ein zusätzlicher Boolean-Rückgabewert eingeführt, der den Erfolg des Zugriffs angibt (Details siehe Beispiel in Kapitel 5). 6 × fgetc :: File• −→ (Char× , Fileu )v Die Funktion nimmt also eine unique Datei und gibt ein Paar bestehend aus einem Zeichen und einer Datei zurück. Die zurückgegebene Datei kann anschließend entweder als unique oder non-unique behandelt werden, dies steht dem Programmierer frei. Beim Zeichen macht es hingegen keinen Sinn dieses als unique weiter zu verwenden, da ein Zeichen unveränderlich ist und damit ohne Probleme mehrfach eingesetzt werden kann. Anders ausgedrückt, es wird keine (sinnvolle) Funktion vom Typ Char• −→ . . . geben. Die Funktion fgetc als Ganzes ist non-unique, da sie ansonsten nur ein einziges Mal im gesamten Programm angewendet werden könnte, d. h. selbst die Funktion fget2c von oben wäre nicht möglich gewesen. Damit der Type Checker des jeweiligen Compilers das Uniqueness Typing überprüfen kann, muss das Uniqueness-Attribut im Typ codiert sein. Die in [11] vorgestellte Variante, behandelt das Uniqueness-Attribut als eigenen Typ, d. h. sowohl • als auch × sind Typen wie auch Int oder Char. Um zu verhindern, dass Werte vom Typ • (unique) oder auch vom Typ Int (d. h. ohne Uniqueness-Attribut) erzeugt werden können, wird auf das Konzept der Kinds (verwendet z. B. in Haskell) zurückgegriffen und dieses erweitert. Kinds (dt. Arten) sind vereinfacht ausgedrückt die Typen der Typen. Konkrete Datentypen, von denen Werte erzeugt werden können, werden mit ∗ bezeichnet. Zum Beispiel wäre im einfachen Kind System Char :: ∗ (zu lesen als Char hat den Kind ∗). Typkonstruktoren (wie Tree), die noch einen oder mehrere Typen als Parameter bekommen können, werden als Funktionen mit ∗ als Parameter und ∗ als Rückgabetyp betrachtet. Zum Beispiel hat Tree :: ∗ -> ∗, aber Tree Int :: ∗. Das erweiterte Kind Systems für Uniqueness Typing ist wie folgt aufgebaut: • ∗ bezeichnet konkrete Typen (z. B. Int× ). • T bezeichnet Typen ohne Uniqueness-Attribute (z. B. Int). • U bezeichnet die Uniqueness-Attribute (• und ×). • Attr :: T -> U -> ∗ nimmt einen noch unmarkierten Typen und ein Uniqueness-Attribut und konstruiert den zugehörigen konkreten Typ. • Werden die Uniqueness-Attribute als Bool’sche Werte modelliert (• = true, × = false), können auch Bool’sche Operatoren darauf definiert werden: ∨, ∧ ¬ :: U -> U -> U :: U -> U Durch die Verwendung beliebiger Bool’scher Ausdrücke als Uniqueness-Variablen, können auch Bedingungen an die Abhängigkeit mehrerer Uniqueness-Variablen in einem Ausdruck modelliert werden. Betrachten wir hierzu das Beispiel der Funktion const x y = x, die stets den Wert des ersten Arguments annimmt. Der vollständige Typ dieser Funktion lautet [11]: × w const :: tu −→ sv −→ tu [w ≤ u] Die Bedingung [w ≤ u]9 besagt, dass w unique sein muss, wenn u unique ist (unique ≤ nonunique). Dies kann mit Hilfe Bool’scher Ausdrücke geschrieben werden als 9 Wieso diese Bedingung notwendig ist, wird in Abschnitt 4.5 erklärt. 7 × w∨u const :: tu −→ sv −−−→ tu Denn w ∨ u ist true (unique), wenn u true (unique) ist. Aber wenn u non-unique ist, muss w ∨ u nicht zwangsläufig ebenfalls non-unique sein. Dies war genau die Beziehung, die wir oben gefordert hatten. Durch diese Definitionen kann der Type Checker soweit erweitert werden, dass er auch auf Uniqueness Typing überprüft. Für eine weiterführende Erklärung, wie die Typing Regeln erweitert werden müssen, um dies zu ermöglichen, verweise ich auf [4, 11]. 4 Uniqueness Typing in Clean Der Überblick in diesem Kapitel bezieht sich auf die (im Wesentlichen syntaktischen) Einführungen in [8, 10]. Für die in Clean verwendete Theorie sei auf Barendsen et al. [3, 4] verwiesen. 4.1 Uniqueness-Attribute In Clean wird zwischen den Uniqueness-Attributen non-unique, unique und necessarily unique unterschieden. Um eine Variable als unique oder necessarily unique zu kennzeichnen, wird deren Typ mit ∗ (z. B. ∗ File) gekennzeichnet, Typen von non-unique Variablen bekommen kein Uniqueness-Attribut. Dabei können unique Variablen auch als non-unique übergeben werden, necessarily unique Variablen allerdings nicht. In Clean wird also anders als beim vereinfachten Uniqueness Typing (Kapitel 3.3) unique als Unterklasse von non-unique behandelt. Um zu verhindern, dass jede unique Objekte non-unique werden kann, gibt es das Uniqueness-Attribut necessarily unique. Dieses wird allerdings nur intern unterschieden, im Code wird es ebenfalls mit ∗ bezeichnet. Wann das Attribut necessarily unique benötigt wird, wird in Abschnitt 4.5 behandelt. In Bezug auf die Semantik von Clean (siehe Kapitel 1) können die Uniqueness-Attribute so interpretiert werden, dass (necessarily) unique Objekte genau eine Referenz auf sich haben und non-unique Objekte auch mehrere Referenzen haben können. 4.2 Uniqueness-Variablen Uniqueness-Variablen werden in Clean mit u:type bezeichnet, zum Beispiel: 1 id :: u:a -> u:a 2 id x = x Wenn id eine unique Variable übergeben bekommt, bleibt die Variable unique. Umgekehrt, wenn eine non-unique Variable übergeben wird, wird sie auch non-unique zurückgegeben. Neben benannten Variablen (u:type) gibt es auch anonyme Variablen (.type). Alle anonymen Variablen in einem Ausdruck, werden intern durch neue (echte) Variablen ersetzt. Dabei wird bei gleichen Typvariablen auch die gleiche Uniqueness-Variable verwendet, alle anderen anonymen Uniqueness-Variablen werden jeweils durch neue ersetzt. Wie im vereinfachten Uniqueness Typing von [11] können auch in Clean Bedingungen zwischen Uniqueness-Variablen definiert werden. Diese werden als [u <= v] geschrieben und bedeuten in diesem Fall, dass u unique ist, wenn v unique ist. Mehrere Bedingungen werden mit Komma getrennt, z. B. [u <= v, w <= v]. 8 Uniqueness-Variablen von Funktionen werden durch Klammerung zugeordnet. Beispielsweise ist der Typ von const10 : const :: u:t -> w:(v:s -> u:t), [w <= u] 4.3 Uniqueness Propagation Wir betrachten die folgende Implementierung der Funktion head [10]: 1 head :: [∗ a] -> ∗ a 2 head [x:xs] = x Durch Pattern Matching bekommt man Zugriff auf das erste Element der Liste x (und den Rest der Liste xs). In dieser Variante sind die Elemente in der Liste unique und damit auch der Rückgabewert. Die Liste selber ist nicht unique und kann daher auch mehrere Referenzen haben. Wir betrachten nun die folgende Funktion, die ausnutzt, dass list nicht unique ist. 1 heads :: [∗ a] -> (∗ a, ∗ a) 2 heads list = (head list, head list) Wie man am Typ des Rückgabewerts sehen kann, wird ein Tupel mit zwei unique Elemente zurückgegeben. Aus der Funktionsdefinition wird klar, dass dies das selbe Element ist (nämlich das erste Element von list). Dies ist eine Verletzung der Uniqueness von ∗ a. Das Problem entsteht, weil durch das Teilen der Liste auch das erste Element der Liste geteilt wird. Mit anderen Worten, um das Uniqueness-Attribut von ∗ a nicht zu verletzen, muss auch die Liste selber unique sein. Dieses Prinzip nennt man allgemein Uniqueness Propagation. Der Typ von head müsste also lauten: head :: ∗ ∗ [ a] -> ∗ a Oder allgemeiner mit Uniqueness-Variablen11 : head :: v:[u:a] -> u:a, [v <= u] Clean wendet Uniqueness Propagation automatisch an, d. h. die folgende Definition (mit anonymen Uniqueness-Variablen) ist äquivalent zur obigen Definition: head :: [.a] -> .a Zusammenfassend kann man sagen, dass Objekte, die innerhalb einer Datenstruktur gespeichert werden, nur dann unique sein können, wenn die Datenstruktur selber unique ist [8]. 10 Wie man diesem Beispiel auch sieht, kann man den Typ einer Funktion auch komplett „curried“ angeben (vgl. Abschnitt 2.2) 11 Dies ist der allgemeinste Typ für die Funktion head. Grundsätzlich steht es dem Programmierer frei, die Funktion durch die Angabe eines spezielleren Typen (z. B. wie oben auf unique Variablen) zu beschränken 9 4.4 Uniqueness bei Definition algebraischer Datentypen Bei der Definition algebraischer Datentypen werden die zugehörigen Konstruktoren automatisch erzeugt. Eine explizite Typangabe für diese Funktionen ist also nicht möglich. Bei der Definition von Tree (Z. 1) werden die Konstruktoren Empty und Node mit den angegebenen Typen erzeugt (Z. 3–4): 1 2 3 4 :: Tree a = Empty | Node a (Tree a) (Tree a) Empty :: Tree a Node :: a (Tree a) (Tree a) -> Tree a Das bedeutet allerdings nicht, dass in Tree nur non-unique Objekte gespeichert werden können. Vielmehr konstruiert Clean intern einen allgemeineren Typ (Z. 5–6) anhand der Uniqueness Propagation Regel sowie weiteren Regeln (siehe [10]): 5 6 Empty :: v:Tree u:a, [v <= u] Node :: u:a v:(Tree u:a) v:(Tree u:a) -> v:Tree u:a, [v <= u] Wenn man allerdings eine Tree-Struktur definieren möchte, die nur unique Objekte speichern kann, kann man dies in der Definition von Tree direkt angeben (Z. 7): 7 :: Tree ∗ a = Empty | Node ∗ a (Tree ∗ a) (Tree ∗ a) Clean inferiert dann wieder die Typen der Konstruktoren (Z. 8–9): 8 9 Empty :: ∗ Tree ∗ a Node :: ∗ a ∗ (Tree ∗ a) ∗ (Tree ∗ a) -> ∗ Tree ∗ a Zu beachten ist, dass die Verwendung von Uniqueness-Variablen bei der Definition von algebraischen Datentypen nicht möglich ist, lediglich anonyme Variablen (.) können verwendet werden. 4.5 Uniqueness bei curried Anwendung von Funktionen Wir betrachten folgende Funktion12 : fwritec :: ∗ File Char -> ∗ File Wir betrachten nun die curried Anwendung dieser Funktion auf eine unique Datei somefile. Der entstehende Ausdruck fwritec somefile hat den Typ u:(Char -> ∗ File), wobei der Wert von u noch bestimmt werden muss. Betrachten wir zunächst, was passiert wenn wir u auf non-unique setzen. Wir betrachten hierfür die Funktion: writeParallel :: (Char -> .File) -> (.File, .File) writeParallel f = (f 'a', f 'b') und wenden diese auf fwritec somefile an. Der entstehende Ausdruck writeParallel (fwritec somefile) ist äquivalent zu (fwritec somefile 'a', fwritec somefile 'b') 12 Die Funktion fwritec ist in Clean in Wirklichkeit fwritec :: Char ∗ File -> ∗ File (siehe auch Kapitel 5) 10 mit umgekehrten Parametern definiert: Dies bedeutet aber, dass das Argument somefile nicht länger unique ist. Wie im Typ von writeParallel zu sehen, muss der erste Parameter f non-unique sein. Um also das Problem zu beheben, könnte man die Uniqueness-Variable u des Ausdruckes fwritec somefile auf unique setzten. Die Anwendung von writeParallel (fwritec somefile) würde dann vom Typsystem zurückgewiesen werden. In Abschnitt 4.1 wurde gesagt, dass unique Objekte jederzeit als non-unique Objekte an Funktionen übergeben werden können und damit ihre Uniqueness verlieren. Dies darf im vorliegenden Fall nicht geschehen, deshalb werden Funktionen wie fwritec somefile als necessarily unique bezeichnet und intern als solche behandelt, um die geschilderte Verletzung der Uniqueness von somefile zu verhindern. 5 I/O in Clean 5.1 I/O Beispiel: cat Die Grundlegenden I/O-Funktionen werde ich anhand des folgenden Beispiels (basierend auf [8]) erklären. Für weiterführende Einführung in die I/O-Bibliotheksfunktionen von Clean verweise ich auf [2, 8]. Das folgende Beispiel Programm soll das Verhalten von cat imitieren13 : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Start :: ∗ World -> ∗ World Start world = catFile "test.txt" world readCharList :: ∗ File -> ([Char], ∗ File) readCharList file0 | not readok = ([], file1) | otherwise = ([c : cs], file2) where (readok, c, file1) = freadc file0 (cs, file2) = readCharList file1 catFile :: String ∗ env -> ∗ env | FileSystem env catFile filename filesystem0 | readok && closeok0 && closeok1 = filesystem4 | otherwise = abort "I/O Error" where (readok, file0, filesystem1) = fopen filename FReadText filesystem0 (charList, file1) = readCharList file0 (closeok0, filesystem2) = fclose file1 filesystem1 (console0, filesystem3) = stdio filesystem2 console1 = fwrites (toString charList) console0 (closeok1, filesystem4) = fclose console1 filesystem3 Wie in Clean üblich steht das Hauptprogramm in der Funktion Start. Anders als in Kapitel 2.1 ist Start hier eine Funktion mit einem Parameter, dem Weltzustand (World). In World werden alle Seiteneffekte auf das System gekapselt. Die Funktion Start gibt dann den neuen Weltzustand zurück, in dem alle Änderungen z. B. an Dateien, an der Konsole etc. gespeichert sind. Um die referenzielle Transparenz zu gewährleisten, muss World natürlich unique sein. Die eigentlich Programmlogik steht in catFile. Hier wird die World als FileSystem verwendet. In der Clean-Bibliothek ist World als Instanz von der Type Class FileSystem implementiert, so dass das Dateisystem nicht aus dem Weltzustand extrahiert werden muss. Bei jedem Aufruf der Dateioperationen (fopen, freadc, stdio, fwrites, fclose), wird eine neue unique Instanz des Dateisystems zurückgegeben. 13 Um das Programm zu vereinfachen, habe ich das Einlesen des Dateinamens weggelassen. 11 Im Einzelnen wird in der Funktion catFile die Datei filename mit fopen geöffnet (Z. 17). Der Inhalt der Datei wird in der Funktion readCharList zeichenweise mit freadc eingelesen (Z. 18, 9). Nach dem Einlesen wird die Datei mit fclose wieder geschlossen (Z. 19). In Clean kann die Konsole wie eine Datei verwendet werden. Das Öffnen des Ein-/ Ausgabestroms (stdin/stdout) erfolgt über die Funktion stdio (Z. 20). Mit fwrites wird der gesamte Inhalt der Datei auf die Konsole geschrieben (Z. 21). Zum Schluss wird auch die Konsole wieder geschlossen (Z. 22). Wenn bei den Operationen ein I/O-Fehler auftritt, wird dies über die zurückgegebene BooleanVariable ok angezeigt. Das Programm gibt in einem solchen Fall eine Fehlermeldung zurück. 5.2 Hash Lets Um die Struktur des Programms zu vereinfachen und um die ständige Umbenennung der veränderten unique Variablen zu umgehen, kann man in Clean das so genannte Hash Let verwenden. Hash Lets sind Syntactic Sugar für eine Defintion mit where, die es aber ermöglichen Variablen-Definitionen zwischen Guards zu schreiben und nicht nur am Ende der Funktion. Eine Anwendung ist z. B. bei der Funktion readCharList in Zeile 6–8 zu sehen. Bei einem Hash Let beginnt der Sichtbarkeitsbereich (engl. scope) der definierten Variable erst in der nächsten Zeile und erstreckt sich über alle folgenden Guards und Hash Lets. Dadurch kann bei der Definition derselbe Variablenname erneut verwendet werden. Z. B. wird der Name file in Z. 6 sowohl auf der linken als auch auf der rechten Seite verwendet. Allerdings sind die beiden Vorkommen von file in Z. 6 zwei unterschiedliche Variablen mit verschiedenen Werten. Durch Hash Lets lässt sich der Programmfluss wie ein imperatives Programm oder ein Haskell-Programm mit do-Notation lesen, was den Code meist lesbarer macht. Der folgende Code mit Hash Lets ist äquivalent zum Code aus Kapitel 5.1: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Start :: ∗ World -> ∗ World Start world = catFile "test.txt" world readCharList :: ∗ File -> ([Char], ∗ File) readCharList file # (readok, c, file) = freadc file | not readok = ([], file) # (cs, file) = readCharList file | otherwise = ([c : cs], file) catFile :: String ∗ env -> ∗ env | FileSystem env catFile filename filesystem # (readok, file, filesystem) = fopen filename FReadText filesystem | not readok = abort "I/O Error" # (charList, file) = readCharList file # (closeok, filesystem) = fclose file filesystem | not closeok = abort "I/O Error" # (console, filesystem) = stdio filesystem # console = fwrites (toString charList) console # (closeok, filesystem) = fclose console filesystem | not closeok = abort "I/O Error" | otherwise = filesystem 12 6 Zusammenfassung Clean hat eine ähnliche Syntax wie andere funktionale Programmiersprachen, besonders Haskell. Ein entscheidender Unterschied in der Semantik ist die Implementation von Clean als Manipulation von Graphen. Dieser Unterschied war nicht Gegenstand dieser Arbeit. Eine Abhandlung dieses Themas findet der Leser in [6, 9]. Eine weitere Abgrenzung zu Haskell und verwandten Sprachen ist das Uniqueness Typing. Dies wurde zuerst durch eine vereinfachte Variante und schließlich an Hand von Clean eingeführt. Wie am Rande erwähnt, kann Uniqueness Typing nicht nur für I/O-Zugriffe (wie im letzten Kapitel beschrieben), sondern beispielsweise auch für das Zeit und Speicher effizienten Update von Arrays verwendet werden. Eine Behandlung dieser und weiterer Features von Clean hätte diese Arbeit gesprengt. Deshalb sein an dieser Stelle noch einmal auf die Referenz von Clean [10] und auf das Buch von Koopman et al. [8] verwiesen. In letzterem findet sich auch eine Aufstellung der wichtigsten Module mit ihren Funktionen. 13 Literatur [1] Achten, Peter: Clean for Haskell98 Programmers – A Quick Reference Guide. http://www.mbsd.cs.ru.nl/publications/papers/2007/ achp2007-CleanHaskellQuickGuide.pdf, 2007. Abgerufen am 09.05.2015. [2] Achten, Peter und Martin Wierich: A Tutorial to the Clean Object I/O Library – Version 1.2. http://clean.cs.ru.nl/download/supported/ObjectIO.1.2/doc/ tutorial.pdf, 2004. Abgerufen am 17.04.2015. [3] Barendsen, Erik und Sjaak Smetsers: Conventional and uniqueness typing in graph rewrite systems. In: Shyamasundar, Rudrapatna K. (Herausgeber): Foundations of Software Technology and Theoretical Computer Science, Band 761 der Reihe Lecture Notes in Computer Science, Seiten 41–51. Springer Berlin Heidelberg, 1993. [4] Barendsen, Erik und Sjaak Smetsers: Uniqueness typing for functional languages with graph rewriting semantics. Mathematical Structures in Computer Science, 6:579–612, 1996. [5] Brus, Tom, Marko van Eekelen, Maarten van Leer und Rinus Plasmeijer: Clean – A language for functional graph rewriting. In: Kahn, Gilles (Herausgeber): Functional Programming Languages and Computer Architecture, Band 274 der Reihe Lecture Notes in Computer Science, Seiten 364–384. Springer Berlin Heidelberg, 1987. [6] Eekelen, Marko van, Sjaak Smetsers und Rinus Plasmeijer: Graph rewriting semantics for functional programming languages. In: Dalen, Dirk van und Marc Bezem (Herausgeber): Computer Science Logic, Band 1258 der Reihe Lecture Notes in Computer Science, Seiten 106–128. Springer Berlin Heidelberg, 1997. [7] Koopman, Pieter: Functional Programming in Clean – An Appetizer. http://www. inf.ufsc.br/~jbosco/tutorial.html. Abgerufen am 17.04.2015. [8] Koopman, Pieter, Rinus Plasmeijer, Marko van Eekelen und Sjaak Smetsers: Functional Programming in Clean. http://www.mbsd.cs.ru.nl/papers/ cleanbook/CleanBookI.pdf, 2002. Part 1. [9] Plasmeijer, Rinus und Marko van Eekelen: Functional Programming and Parallel Graph Rewriting. Addison-Wesley, 1993. [10] Plasmeijer, Rinus, Marko van Eekelen und John van Groningen: Clean language report – Version 2.2. http://clean.cs.ru.nl/download/doc/CleanLangRep.2. 2.pdf, 2011. Abgerufen am 17.04.2015. [11] Vries, Edsko de, Rinus Plasmeijer und David M. Abrahamson: Uniqueness Typing Simplified. In: Chitil, Olaf, Zoltán Horváth und Viktória Zsók (Herausgeber): Implementation and Application of Functional Languages, Band 5083 der Reihe Lecture Notes in Computer Science, Seiten 201–218. Springer Berlin Heidelberg, 2008. 14