Having fun with types Hauptseminar im Sommersemester 2011

Werbung
Having fun with types
Hauptseminar im Sommersemester 2011
Lineare Typen
Alexander Aumann
Technische Universität München
22.6.2011
Zusammenfassung
Im Rahmen dieser Arbeit soll auf sogenannte Lineare Typen eingegangen werden. Es handelt sich dabei um ein Konzept von Variablen, die nur
einmal referenziert werden dürfen, was in der Praxis zahlreiche positive
Auswirkung auf die Speicherverwaltung und den Zugriff auf Ressourcen
hat. Im Rahmen dieser Arbeit soll die Idee hinter linearen Typen vorgestellt werden und deren Vor- und Nachteile, sowie deren Praxistauglichkeit vor allem im Zusammenhang mit funktionalen Programmiersprachen
näher beleuchtet werden.
1
Einleitung
Eines der großen Probleme bei der Optimierung moderner Programme ist die
Speicherverwaltung, genauer gesagt die Frage, wann bestimmter reservierter
Arbeitsspeicher wieder freigegeben werden kann. Die Schwierigkeit besteht dabei hauptsächlich darin zu bestimmen, ob es noch irgendwo im Programmcode gültige Referenzen auf diesen Speicherbereich gibt, oder eben nicht. Ein
ähnliches Problem tritt beim Verwalten von Ressourcen auf, zum Beispiel beim
Zugriff auf eine Datei. Sobald eine Datei an einer Stelle im Programm geschlossen wird, sollte es nicht mehr möglich sein beispielweise in diese Datei zu schreiben. Im Idealfall wäre es dem Compiler möglich einen derartigen fehlerhaften
Zugriff zu erkennen, in der Praxis ist es das jedoch aufgrund dessen nicht, dass
es zahlreiche Referenzen auf diese Datei geben kann, und es nicht möglich ist
zur Kompilierzeit alle zu erkennen.
Eine mögliche Lösung dieser Probleme ist die Verwendung sogenannter linearer
Typen, die es nicht erlauben Referenzen auf ein Ressourcenobjekt zu duplizieren. Im nächsten Abschnitt werden lineare Typen vorgestellt, das heißt was sie
ausmacht und welche Eigenschaften sie haben. Der darauf folgende Abschnitt
wird beleuchten, was sie zur Verwaltung von Ressourcen theoretisch so hervorragend geeignet macht und anschließend wird darauf eingegangen wie mit linearen
1
Typen Speicherverwaltung erleichtert wird und worin Nachteile bei linearen Typen bestehen. Im letzten Abschnitt werden Uniqueness-Typen vorgestellt, die
sehr ähnlich zu linearen Typen sind und in der Programmiersprache Clean, die
den Ruf hat äußerst effizient zu sein, implementiert sind.
2
Was lineare Typen eigentlich sind
Lineare Typen basieren auf der von Jean-Yves Girard entworfenen linearen Logik [3], die zahlreiche Eigenschaften der konventionellen Aussagenlogik und intuitioneller Logik vereint. Insbesondere kann sie auch als Logik über Ressourcen
aufgefasst werden, die man nicht ohne weiteres duplizieren oder verwerfen darf.
Darauf basierend sind lineare Variablen Variablen, die nicht mehrfach referenziert werden dürfen. Das bedeutet, dass wenn es eine Variable b gibt, die auf eine
lineare Ressource (zum Beispiel ein bestimmtes im Speicher abgelegtes Datum)
verweist, es keine Aliasvariable auf dieselbe Ressource geben darf, die gleichzeitig Gültigkeit besitzt. Das wird dadurch erreicht, dass b, sobald es auf der
rechten Seite einer Zuweisung auftaucht, aus dem aktuellen Gültigkeitsbreich
(engl. scope) geschoben wird. Dadurch ist b ab diesem Zeitpunkt keine gültige
Variable mehr. Zur Verdeutlichung hier ein kleines Beispiel in einer C++-artigen
Syntax.
1
2
3
4
5
6
7
F i l e ∗ f i l e A = new F i l e ( ” f o o . t x t ” ) ; // L i n e a r e r Typ F i l e
f i l e A −>open ( ) ;
f i l e A −>w r i t e ( ” H a l l o ” ) ;
File ∗ fileB = fileA ;
// f i l e A v e r l i e r t G u e l t i g k e i t
f i l e B −>c l o s e ( ) ;
...
f i l e A −>w r i t e ( ” H a l l o nochmal ” ) ;
// F e h l e r , f i l e A u n g u e l t i g
In diesem Beispiel wird ersichtlich, dass es bei der Verwendung linearer Typen ausgeschlossen ist, dass zwei Referenzen auf die gleiche Datei existieren.
Die Variable fileA würde übrigens auch ungültig, wenn sie einer Funkion als
Argument übergeben würde. Der nächste Abschnitt wird erläutern, wie lineare
Datentypen eine Schnittstelle zur imperativen Programmierung in funktionalen
Programmiersprachen bilden können.
3
Bessere Modellierung der Welt - Änderungen
werden möglich
Das schöne an funkionalen Programmiersprachen im Gegensatz zu imperativen
Programmiersprachen ist ihre Nähe zum mathematischen Modell von Funktionen. So ist es eine wichtige Eigenschaft rein funktionaler Programmiersprachen,
dass Variablen generell unveränderlich sind (das heißt sie haben keinen speziellen, von der Zeit abhängigen Status) und Funktionen frei von sogennanten
Seiteneffekten sind. Das bedeutet, wenn man eine Funktion f zweimal hintereinander auf das gleiche x ausführt, kann man zweimal das identische Ergebnis
2
erwarten. Dadurch, dass es keine Seiteneffekte gibt wird unter anderem die referentielle Transparenz sichergestellt. Referentielle Transparenz bedeutet, dass,
wenn es zwei Funktionen y = f x und g = h y y gibt, die zweite Funktion auch
geschrieben werden kann als g = h (f x) ( f x), ohne das sich am Ergebnis etwas
ändert. Der Nachteil solcher rein funktionaler Programmiersprachen ist, dass sie
dadurch ein wenig realitätsfern sind, da es durchaus von Nutzen sein kann, den
Wert der in einer Speicherzelle steht direkt zu verändern, zum Beispiel in intensiv iterativen numerischen Anwendungen. Hier hatten funktionale Programmiersprachen lange Zeit den Ruf sehr ineffezient zu sein. Manchmal ist es außerdem
nötig die “reale Welt” zu verändern beziehungsweise Seiteneffekte zuzulassen,
das beste Beispiel dafür sind I/O-Operationen, also zum Beispiel das Schreiben
in eine Datei. Wenn man nun solche Seiteneffekte zulässt, verliert die Sprache
ihre referentielle Transparenz (die Auswertungsreihenfolge wird entscheidend).
Betrachten wir dazu das folgende Beispiel in einer funktionalen Pseudosprache:
1
2
3
4
5
6
7
8
9
10
11
function writeString ( File f i l e , String str )
returns retFile
return r e t F i l e = doImperativeWrite ( f i l e , s t r )
end
f u n c t i o n funkyFunc ( F i l e f i l e )
returns ( File , File )
r e t u r n ( w r i t e S t r i n g ( f i l e , ”AB” ) , w r i t e S t r i n g ( f i l e , ”CD” ) )
end
F i l e f i l e = fileFromSomewhere ( ” f o o . t x t ” )
( f i l e A , f i l e B ) = funkyFunc ( f i l e )
Die Funktion writeString schreibt den String, den sie als Parameter erhält in
die ebenfalls übergebene Datei und gibt die neue Datei zurück. Die Funktion
funkyFunc wiederum übernimmt eine Datei, und gibt ein Tupel aus zwei Dateien
zurück, die jeweils durch den Aufruf der writeString-Funktion erzeugt werden.
Bei diesem Tupel ist nun unklar, was genau in der Datei steht, denn das hängt
von der Auswertungsreihenfolge der beiden Funktionsaufrufe ab. Wird zuerst
der linke Ausdruck ausgewertet, dann steht in der Datei ”ABCD”, ansonsten
”CDAB”. Damit ist die referentielle Transparenz des Programms nicht mehr
gewährleistet. Ein Ausweg wäre, dass beide Dateien unabhängige Kopien der
übergebenen Datei sind ( fileA enthält dann ”AB” und fileB ”CD”, das entspricht
jedoch nicht der Vorstellung eines Dateisystems und wie es manipuliert wird
(was geschieht mit der ursprünglichen Datei?).
Genau hier an der Schnittstelle zwischen funktionalen und imperativen Programmiersprachen können lineare Variablen eine elegante Lösung darstellen.
Sehen wir uns dazu eine abgewandelte Version des writeString-Beispiels an.
1 function writeString ( linear File f i l e , String str )
2
returns linear File
3
return r e t F i l e = doImperativeWrite ( f i l e , s t r )
4 end
5
3
6 F i l e f i l e = fileFromSomewhere ( ” f o o . t x t ” )
7 F i l e f i l e B = w r i t e S t r i n g ( f i l e , ” Hallo ”)
8 F i l e f i l e C = w r i t e S t r i n g ( f i l e B , ” Welt ! ” )
In dieser neuen Version nimmt die writeString-Funktion eine lineare File-Variable
entgegen. Dadurch ergeben sich folgende Änderungen:
• mit dem Aufruf der writeString-Funktion verliert die Variable file beim
Aufrufer ihre Gültigkeit
• die Funktion funkyFunc wäre nicht mehr erlaubt
• die Funktion writeString wird jetzt mit unterschiedlichen File-Variablen
aufgerufen
Die lineare writeString-Funktion kann somit jetzt nicht mehr parallel ausgeführt
werden, sondern nur noch in wohldefinierter Weise hintereinander. Ein positiver
Nebeneffekt dieser Vorgehensweise ist, dass die Datei nun die Ressourcenmetapher einer physikalischen Datei deutlich besser abbildet. Der Aufrufer übergibt
der Funktion die Datei und verliert solange den Zugriff darauf, bis er sie explizit wieder zurück erhält. In der Hauptsache wird durch diese Version auch
das Problem behoben, dass durch Seiteneffekte ein identischer Funktionsaufruf
zu völlig unterschiedlichen Ergebnissen führt, die referentielle Transparenz kann
nun gewährleistet werden. Der nächste Abschnitt beschäftigt sich mit dem zweiten Problem funktionaler Programmierung, nämlich wie durch Direktersetzung
stark iterative Berechnungen mit linearen Typen speichereffizient durchgeführt
werden können.
4
Speicherverwaltung
In einer funkionalen Programmiersprache zeichnen sich lineare Variablen dadurch aus, dass man sie genau einmal benutzen kann und muss: man darf sie
weder duplizieren noch einfach nicht beachten. Das bedeutet konkret, dass eine Funktion, die eine lineare Variable übergeben bekommt diese entweder (zum
Beispiel durch Auslesen) explizit verbrauchen muss, oder sie am Ende wieder als
Rückgabewert zurückliefern muss. Beide Tatsachen bringen interessante Eigenschaften mit sich. Dadurch, dass man eine lineare Variable nicht duplizieren darf
ergibt sich die Möglichkeit den Umgang mit Arrays signifikant performanter zu
gestalten. Ein übergebenes Array muss dadurch nämlich nicht jedes mal kopiert
werden, wenn es bearbeitet wird, sondern schon der Compiler kann dafür sorgen,
dass die Werte direkt im Arrayspeicher aktualisiert werden und als neue Variable
zurückgegeben werden - schließlich ist sichergestellt, dass niemand sonst an anderer Stelle auf diese Werte zugreift. Die Tatsache, dass man lineare Variablen
nicht einfach nicht beachten darf, ist ebenso bedeutend. Normalerweise muss
ein erheblicher Aufwand betrieben werden, um zu bestimmen, wann Speicherplatz freigegeben werden kann. So muss zum Beispiel über Referenzzähler oder
Garbage Collectoren bestimmt werden, ob das Objekt noch von irgendwo her
4
referenziert ist oder der Speicherplatz wieder in die Liste des freien Speichers
eingereiht werden kann. Mit linearen Typen entfallen diese Notwendigkeiten was nicht bedeutet, dass es überhaupt nicht möglich ist, linear referenzierten
Speicher wieder freizugeben oder Werte zu kopieren: beides muss nur explizit
geschehen [7]. Im Gegensatz zu C übrigens, welches ja auch explizites Speichermanagement mit sich bringt, ist dieses Verfahren mit linearen Variablen
sicher: dadurch, dass keine Mehrfachreferenzen bestehen, kann es nicht zu “wilden Zeigern” kommen, die noch auf bereits freigegebenen Speicher verweisen.
Man erhält dadurch theoretisch die Möglichkeit einer Programmiersprache, die
so effizient wie C ist - und trotzdem sicher. Betrachten wir nun ein konkretes Beispiel, den in [1] vorgestellten Quicksort, allerdings in alternativer Caml-artiger
Syntax:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(∗ hohe Elemente von x nach h , n i e d r i g e nach l ∗)
l e t rec h i l o x i h l =
i f x = [ ] then h , l
else
l e t f i r s t x : : r e s t x = x in
i f f i r s t x < i then
hilo restx i h ( f i r s t x : : l )
else
hilo restx i ( f i r s t x : : h) l
;;
(∗ L i s t e x i n d i e L i s t e l e i n s o r t i e r e n ∗)
l e t rec qs x l =
i f x = [ ] then l
else
l e t i : : r e s t x = x in
l e t ( high , low ) = h i l o r e s t x i [ ] [ ] in
qs low ( i : : ( qs h i g h l ) )
;;
In Zeile 12 wird die Funktion qs definiert, die zwei Parameter x und l übernimmt,
wobei Schritt für Schritt die Ausgangsliste x in die Liste l einsortiert wird. Die
Abbruchbedingung ist, dass die Liste x leer wird, in diesem Fall wird in Zeile
13 die sortierte Liste l zurückgegeben. Andernfalls wird im nächsten Schritt
den Variablen i das erste Listenelement zugewiesen und der Variablen restx
die restliche Liste1 . Der Listenvariable high schließlich werden in Zeile 16 alle
Elemente größer als i zugewiesen, der Variable low alle die kleiner als i sind und
in Zeile 17 wird rekursiv wieder qs aufgerufen2 . Die Funktion hilo dient dem
Aufteilen der Listenelemente in die kleinen und großen Elemente.
1 Eine Liste ist entweder “[]”, also leer (im Terminus funktionaler Sprachen wie LISP häufig
auch “nil” genannt), oder eine verkettete Liste first::rest, wobei first das erste Listenelement
ist, und rest eine Liste die alle anderen Elemente enthält). Das let-Statement, z.B. aus Zeile
15 weist also i das erste Element aus x zu und restx den Rest der Liste x
2 Eine kleine Besonderheit aus Effiziengründen ist hier, dass nicht einfach parallel die Listen
low und high sortiert und anschließend verkettet werden, wie es bei quicksort intuitiv wäre,
sondern die teure Verkettungsoperation dadurch vermieden wird, dass die kleinen Listenelemente sukkzessive von links auf l draufsortiert werden.
5
Dadurch, dass hilo endrekursiv ist und im rekursiven Aufruf von qs eine Konkatenation der kompletten Teillisten vermieden wird, ist diese Variante bereits
ein sehr gut optimierter Quicksort. Das nächste Listing zeigt nun die lineare
Version des Quicksortalgorithmus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(∗ hohe Elemente von x nach h , n i e d r i g e nach l ∗)
l e t rec h i l o x i h l =
i f − n i l x then ( k i l l x ; h , l , i )
else
dlet f i r s t x : : r e s t x = x in
l e t ( t r u t h , f i r s t x 2 , i 2 ) = ( f i r s t x l< i ) in
i f t r u t h then
h i l o r e s t x i 2 h ( f i r s t x 2 :−: l )
else
h i l o r e s t x i 2 ( f i r s t x 2 :−: h ) l
;;
(∗ L i s t e x i n d i e L i s t e l e i n s o r t i e r e n ∗)
l e t rec qs x l =
i f − n i l x then ( k i l l x ; l )
else
dlet i : : r e s t x = x in
l e t ( h i g h , l o w , i 2 ) = h i l o r e s t x i [ ] [ ] in
qs low ( i 2 :−: ( qs h i g h l ) )
;;
Hier fallen auf den ersten Blick folgende Neuerungen auf:
• Das neue Konstrukt if −nil: es ist nötig, da bei jeder Operation auf einer linearen Variable, diese konsumiert wird. In [1] werden sogenannte
“flache Operationen” für Abfragen nach “nil” vorgeschlagen, da diese nur
einen sehr beschränkten Teil der Variable abfragen und diese somit nicht
verbrauchen müssen. Ohne dieses Zusatzkonstrukt wären an dieser Stelle
Kopien des Wertes nötig.
• Die explizite kill -Anweisung für x, falls es nil ist: da eine lineare Variable
nicht einfach verworfen werden darf, muss sie explizit zerstört werden3 .
• Die neue Anweisung dlet : sie spaltet eine lineare Liste in die zwei Teile
Kopf und Rest und weist sie zwei linearen Variablen zu
• Der neue, lineare Vergleichsoperator l<: auch hier gilt, dass der
Vergleich beide Operanden verbraucht. Deshalb gibt diese lineare
<Vergleichsfunktion zusätzlich zum Wahrheitswert auch die beiden Operanden als neue lineare Variablen zurück.
• Die Funktion hilo gibt jetzt als zusätzlichen Parameter i zurück: da wie
immer lineare Variablen nicht verworfen werden dürfen, müsste sie entweder explizit zerstört werden, oder eben zurückgegeben werden. In diesem
3 Der ganze Ausdruck in den Klammern um kill herum benutzt den Caml-Sequenzoperator
; - Dieser sorgt dafür, dass zuerst kill ausgeführt wird, das keinen Rückgabewert hat, und
anschließend das Tupel (h,l,i) zurückgegeben wird.
6
Fall wird sie aber sowieso von der aufrufenden Funktion gebraucht und
deshalb zurückgegeben.
Diese modifizierte Version von Quicksort ist vollkommen linear, das heißt, dass
sie keinerlei Speichermüll produziert, der von einem Garbage Collector gefunden
und beseitigt werden müsste. Das liegt daran, dass die lineare Listenkonstruktion mit :−: Listenspeicherzellen, die mit dlet von der Liste abgespalten wurden,
direkt recyceln kann. Laut [1] ist diese lineare Quicksort-Version4 die schnellste,
die programmiert werden konnte und sogar um den Faktor 1,81 schneller als
die in Common-Lisp eingebaute sort-Routine. Da es bekanntlich nahezu keine
Vorteile ohne gewisse Nachteile gibt, werden im nächsten Abschnitt kurz die
Nachteile beleuchtet, die die Verwendung linearer Typen mit sich bringt.
5
Nachteile linearer Programmierung
Nachdem im bisherigen Teil der Arbeit hauptsächlich die Vorteile betrachtet
wurden, die die Verwendung linearer Typen mit sich bringt, soll nun auch auf
die Nachteile eingegangen werden. Der erste Nachteil ist, dass – wie bereits am
Beispiel mit Quicksort deutlich sichtbar – linearer Code die Tendenz hat aufgeblähter und umständlicher zu werden. Dadurch, dass man Variablen immer
explizit zerstören und bei Bedarf kopieren muss, müssen zahlreiche Hilfsfunktionen geschrieben werden, die diese Funktion erfüllen, wie zum Beispiel die l<
Funktion zum Vergleich zweier Werte. In diesem Zusammenhang entstehen auch
nahezu immer zusätzliche Rückgabewerte, beispielsweise das i im QuicksortBeispiel des vorigen Kapitels. Diese für die lineare Programmierung typischen
Rückgabetupel können bei ungenügender Compiler- und Laufzeitoptimierung
sogar zu deutlichen Performanceeinbußen führen, genauso wie die Notwendigkeit expliziter, tiefer Kopien, die in realen Programmen häufig nur für Vergleiche
erstellt werden müssen, und danach sofort wieder zerstört werden [9]. Um zu
zeigen, dass lineare Typen, beziehungsweise zumindest etwas sehr ähnliches,
aber trotzdem durchaus performant in der Praxis Verwendung findet wird sich
der nächste Abschnitt mit der Programmiersprache Clean beschäftigen, die mit
Uniqueness-Typen ein verwandtes Konzept verwendet.
6
Uniqueness Typen und Clean
Clean ist eine rein funktionale Programmiersprache, das heißt referentiell transparent. Sie hat den Ruf sehr schnell zu kompilieren und ist trotz ihrer funktionalen Natur von Grund auf auch dafür Gedacht große Projekte damit zu verwirklichen. So wurde das komplette vom Projektteam zur Verfügung gestellte Paket
an Entwicklungstools inklusive IDE mit spezialisiertem Texteditor, Linker und
Analysetools vollständig in Clean geschrieben. Sogar der Clean-Compiler für
Version 2.0 der Sprache ist komplett in Clean geschrieben und ist nur um den
4 Das
heißt die ursprünglich in [1] vorgestellte, semantisch gleiche Linear-LISP-Version.
7
Faktor 1.7 langsamer als die alte, in reinem C-Code geschriebene Version, wobei
er sich immer noch komplett selbst in unter einer Minute kompilieren kann [5].
Vor allem um Features wie graphische Oberflächen und I/O-Features allgemein
bereitzustellen, sind Funktionen nötig, die Seiteneffekte erzeugen. Um trotzdem die semantische Integrität der Sprache zu gewährleisten, greift Clean dazu
auf zu linearen Typen sehr ähnliche Uniqueness-Typen zurück, die es außerdem erlauben in Arrays wie mit imperativen Programmiersprachen performant
Direktersetzungen vorzunehmen. In diesem Abschnitt soll diese Sprache kurz
vorgestellt werden.
6.1
Uniqueness als Typ-Attribut in Clean
Um festzulegen, dass auf eine Ressource nur eine Referenz existieren darf, gibt es
in Clean ein “Uniqueness”-Attribut. Betrachten wir dazu die Funktion WriteFile,
die eine Liste von Zeichen und eine Datei übernimmt, die Zeichen in die Datei
schreibt und diese dann zurückgibt:
1 W r i t e F i l e : : [ Char ] ∗ F i l e −> ∗ F i l e
2 WriteFile [ ]
file = file
3 WriteFile [ c : cs ] f i l e = WriteFile cs ( fwritec c f i l e )
Die Tatsache, dass der Datei-Parameter “File” in der Typbeschreibung der
Funktion mit einem * annotiert, bedeutet, dass dieser Parameter unique ist.
Das bedeutet, dass beim Aufruf dieser Funktion die Anzahl der Referenzen auf
diese Datei genau 1 betragen muss. Das sorgt zum Beispiel dafür, dass die
folgende Funktion nicht kompilierbar wäre:
1 WriteToTuple : : [ Char ] F i l e −> ( File , F i l e )
2 WriteToTuple x f i l e =( ( W r i t e F i l e x f i l e ) , ( W r i t e F i l e x f i l e ) )
Dadurch, dass WriteFile eine unique-Datei als Parameter übernimmt, erkennt der
Compiler, dass dieser doppelte Aufruf der Funktion nicht erlaubt ist, und das ist
auch gut so, denn wie im Beispiel aus Abschnitt 3 gezeigt, würde ein derartiger
Aufruf zu Problemen führen. Dank Uniqueness-Typen sieht der Compiler den
Fehler und verhindert diesen Aufruf. Wie aus den Beispielen mit linearen Typen
bekannt, wird für eine Art Ressourcenübergabe der unique-Variablen von einem
Funktionsaufruf zum nächsten gesorgt. Die beiden folgenden Implementierungen
einer Funktion ChainedWrite sind beide semantisch gleich und gültig:
1
2
3
4
5
6
7
8
ChainedWrite : : [ Char ] [ Char ] ∗ F i l e −> ∗ F i l e
ChainedWrite x y f i l e = W r i t e F i l e y ( W r i t e F i l e x f i l e )
// A l t e r n a t i v e 2
ChainedWrite x y f i l e = r e t f i l e
where
fileOnceWritten = WriteFile x f i l e
r e t f i l e = WriteFile y fileOnceWritten
8
Der Nachteil der ersten Alternative ist, dass spätestens bei längeren Ketten
störend ist, dass die Leserichtung der Reihenfolge von x und y umgekehrt zur
geschriebenen Reihenfolge ist. Das tolle an Clean ist, dass es sogar noch eine dritte Alternative dazu gibt, die das ewige Umbenennen der Datei erspart,
nämlich das sogenannte let-before, dass durch eine Raute abgekürzt wird:
1 ChainedWrite x y f i l e
2
# f i l e = WriteFile x f i l e
3
# f i l e = WriteFile y f i l e
4
= file
Das funktioniert deshalb, weil mit jeder let-before-# ein neuer Gültigkeitsbereich
anfängt, das heißt prinzipiell handelt es sich bei den files um lauter verschiedene
Variablen, die wie in Abbildung 1 gezeigt in verschiedenen Bereichen gültig sind.
Abbildung 1: Bereichsschachtelung bei let-before
Durch diese Art die Verkettung zu schreiben, entsteht beinahe das Flair und
der Komfort imperativer Programmierung, wobei dieses Vorgehen auch nur bei
unique-Ressourcen zu empfehlen ist, da einen der Compiler darauf aufmerksam
macht, wenn man die Gültigkeitsbereiche falsch einschätzt, was ansonsten zu
sehr vertrackten Fehlern führen kann. Sehen wir uns nun den Typ einer Funktion
an, die x Zeichen aus einer Datei liest:
1 ReadXChars : : Int F i l e −> [ Char ]
Hier fällt auf, dass der Datei-Parameter der ReadXChars-Funktion nicht unique
ist, was deshalb nicht nötig ist, da sie nicht verändert wird und somit durchaus
mehrere parallele Lesezugriffe auf ein und dieselbe Datei erlaubt werden können.
Deshalb ist die folgende Funktion ebenfalls gültig:
1 ReadToTuple : : Int F i l e −> ( [ Char ] , [ Char ] )
2 ReadToTuple x f i l e = ( ( ReadXChars x f i l e ) , ( ReadXChars x f i l e ) )
Eine Besonderheit bei unique-Typen ist auch, dass ein solcher durchaus in Zukunft zu einem normalen (geteilten) Typen werden kann, was bei linearen Typen
nicht möglich wäre. In Prosa lautet der Unterschied zwischen linearen und uniquen Typen deshalb, dass lineare Typen in Zukunft nicht mehr geteilt werden
9
dürfen, und man bei uniquen Typen sicher ist, dass sie vorher nicht geteilt wurden5 . Daher ist es zum Beispiel durchaus möglich, dass die unique-Datei aus
der WriteFile-Funktion an eine Funktion wie ReadXChars übergeben wird, die
keinen unique-Parameter erwartet. Dadurch verliert die Datei allerdings ihren
ungeteilten Status, und kann in Zukunft konsequenterweise nicht mehr an eine
Funktion übergeben werden, die einen ungeteilten Parameter erwartet.
6.2
Die Welt in Clean
Die verbleibende Frage ist nun, wie man überhaupt an eine Datei kommt, die
man den Funktionen übergeben könnte. Das Prinzip, dass hier verwendet wird,
ist, dass jedes Clean-Programm zu Beginn eine Variable *World übergeben
bekommt, die ebenfalls unique ist (es kann somit immer nur eine Welt geben).
Eine Funktion, die eine geöffnete Datei zurückliefert bekommt nun diese Welt
übergeben und spaltet davon die Datei ab, anschließend kann sie die geänderte
Welt und die Datei zurückgeben, wie am Beispiel von fopen zu sehen, das eine
Datei öffnet:
1 f o p e n : : String Int ∗w −> ∗ ( Bool , ∗ File , ∗w) | F i l e S y s t e m w
Der Teil | FileSystem w bedeutet in etwa, dass w ein Typ der Klasse Filesystem
sein muss - also sozusagen dem Dateisystem-Teil der Welt.
Abschließend sei noch erwähnt, dass Clean auch das destruktive Update auf
Arrays mit Hilfe seines Uniqueness-Systems auf ähnliche Weise angeht, um zeitkritsche iterative Programme so performant wie in einer imperativen Sprache
lösen zu können.
Wer mehr über Clean und sein Uniqueness-System erfahren will und vielleicht
selber damit experimentieren will, dem sei die Einsteigerdokumentation [6] ans
Herz gelegt. Das Projekt-Wiki, das alles Wissenswerte enthält, findet sich unter:
http://wiki.clean.cs.ru.nl/Clean.
7
Zusammenfassung
Im Rahmen dieser Arbeit wurde das Konzept linearer Typen vorgestellt und
gezeigt, dass es einige interessante Anwendungen für ein solches Typsystem vor
allem in funktionalen Programmiersprachen gibt. Durch lineare Typen ist es
möglich, Ressourcen sicher zu verwalten und eine saubere Integration von Seiteneffekten in funktionalen Programmiersprachen zu ermöglichen. Außerdem
können häufig vermisste Features aus imperativen Programmiersprachen wie
Direktupdates von Speicherzellen (zum Beispiel in Arrays) auch in funktionalen Sprachen umgesetzt werden, was potenziell große Performancegewinne verspricht. Es hat sich aber gezeigt, dass zumindest die ursprüngliche Vorstellung
5 Durch diese nicht strikt-lineare Abwandlung linearer Typen, kann zum Beispiel auch bei
if-Anweisungen sowohl im Bedingungsteil als auch in beiden Auswertungszweigen die gleiche
unique-Variable vorkommen, da bei der letzendlichen Auswertung in (genau) einem Zweig,
nur eine Referenz Bestand hat.
10
von linearen Typen in der Praxis unnötig strikt ist, was bei der Programmierung
zu sehr schwer lesbarem Code führt und der Overhead sogar Performanzeinbußen mit sich bringen kann. Die Programmiersprache Clean schließlich zeigt,
dass lineare Typen beziehungsweise die angepasste Theorie von Uniquenesstypen auch praktisch in einer universell einsetzbaren und performanten Programmiersprache sinnvoll angewendet werden können.
Literatur
[1] Henry G. Baker.
A “Linear Logic”
http://www.pipeline.com/ hbaker1/LQsort.html.
Quicksort,
1993.
[2] Steven Cooper. On Linear Types and Imperative Update. PhD thesis, August
1997.
[3] Jean-Yves Girard. Linear Logic. Theoretical Computer Science, 50:1–102,
1987.
[4] Chris Hawblitzel. Linear Types for Aliased Resources (Extended Version).
Technical report, Microsoft Research, Microsoft Corporation, October 2005.
[5] Rinus Plasmeijer and Marko van Eekelen.
Clean Language Report
2.1. Department of Software Technology, University of Nijmegen, 2002.
http://clean.cs.ru.nl/download/Clean20/doc/CleanLangRep.2.1.pdf.
[6] Rinus Plasmeijer, Marko van Eekelen, Pieter Koopman, and Sjaak Smetsers.
Functional Programming in CLEAN, September 2002. http://wiki.clean.
cs.ru.nl/Functional_Programming_in_Clean.
[7] Philip
Wadler.
Linear
types
an
change
world!
Programming
Concepts
and
Methods,
http://homepages.inf.ed.ac.uk/wadler/papers/linear/linear.ps.
the
1990.
[8] Philip Wadler.
Is there a use for linear logic?
In Partial Evaluation and Semantics-Based Program Manipulation (PEPM), 1991.
http://homepages.inf.ed.ac.uk/wadler/papers/linearuse/linearuse.ps.
[9] David Wakeling and Colin Runciman. Linearity and laziness. In John
Hughes, editor, Functional Programming Languages and Computer Architecture, volume 523 of Lecture Notes in Computer Science, pages 215–240.
Springer Verlag Berlin Heidelberg, 1991.
11
Herunterladen