Fakultät für Informatik

Transcrição

Fakultät für Informatik
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