Clean: Eine saubere Sprache Inhaltsverzeichnis Einführung Eingliederung in der Sprachlandschaft 1 Grundbegriffe 2 Ersetzungsstrategie 5 Formulierung von Clean Programmen Definition von Funktionen 7 Sorten 9 Zusammengesetzte Daten 13 Beispiele 16 Zusammenfassung 18 Version 13.01.2001 Seite 1 Einführung Eingliederung in der Sprachlandschaft Arten von Programmiersprachen Zuweisungsorientiert Variablen und Anweisungen Sequenz von Anweisungen Reihenfolge, Zustand Regelorientiert (Logisch) Fakten und Regeln Ableitung neuer Aussagen (Versuch eine Aussage zu verifizieren) Objektorientiert Objekte mit innerem Zustand Ansammlung von Objekten, die untereinander Botschaften austauschen Funktionsorientiert Funktionen Auswertung der „Hauptfunktion“, in Clean „Start“ genannt, bewirkt gewöhnlich den Aufruf anderer Funktionen Version 13.01.2001 Seite 2 Grundbegriffe Problemstellung: Benötigt wird eine formale Sprache Terme erster Ordnung (Variablen sind Platzhalter für beliebige einzelne Terme) Formale Definition: S sei eine endliche menge, die der Sorten F i 0 Fi sei die Menge der Funktionssymbole mit fester Stelligkeit und Sortenzuordnung, dabei sei Fi die Menge der i-Stelligen Funktionssymbole. Die Menge F0 der nullstelligen Funktionssymbole heißt auch Menge der Konstanten. X sei eine von S und F disjunkte Menge mit Sortenzuordnung typ sei eine Funktion die f F und x X Sorte wie folgt zuordnet: typx S für x X typc S für c F0 typ f ist der Form s1 ... sn s0 für f Fn und s0 ...sn S . s0 heißt Ergebnissorte, s1 ...sn Argumentsorten F einschließlich der Sortenzuweisungsfunktion heiße Signatur Menge der Terme: 1. Jede Variable x X ist ein Term der Sorte typx 2. Jede Konstante c F0 F ist ein Term der Sorte typc 3. Sei f F F0 mit typ f s1 ... sn s0 und t 0 ...t n seien Terme der Sorten s0 ...sn dann ist f t 0 ...t n ein Term der Sorte s0 . 4. Nichts sonst ist ein Term Version 13.01.2001 Seite 3 Termersetzungssystem: Verarbeitungsmodell bestehend aus Terme und Regeln zur Überführung existierender Terme in neue, äquivalente Terme. Termgraphersetzungssystem (Graph Rewriting System, GRS): Termersetzungssystem, in welchem Terme durch gerichtete Graphen ersetzt werden, um durch gemeinsame Verwendung von Daten eine Vervielfachung der Arbeit zu vermeiden. Funktionelles Termgraphersetzungssystem (Functional Graph Rewriting System, FGRS): Termgraphersetzungssystem das eine funktionelle Ersetzungsstrategie verwendet. Ersetzungsstrategie: Funktion, die angibt welcher der möglichen Terme als nächster ersetzt (reduziert) wird. Clean Programm: Menge von (typgebundenen) Ersetzungsregeln Zu ersetzendes Muster (engl. redex, reducable expression): Untergraph für den es eine Reduktionsregel mit einer passenden linken Seite gibt Graph in Normalform: Enthält keine redex Version 13.01.2001 Seite 4 Ersetzungsstrategie von Clean Gegeben: Graph und eine Menge von Ersetzungsregeln (Clean: Funktionen) Mögliche Fälle: Mehrere redex (ersetzbare Ausdrücke) im Graphen Mehrere Möglichkeiten ein redex zu ersetzen Algorithmus der den nächsten redex auswählt: Ersetzungsstrategie Jedes Termgraphersetzungssystem besitzt eine Ersetzungsstrategie Clean: Funktionelle Strategie Besagt dass ein Term nur dann reduziert wird wenn sein Wert benötigt wird. (Lazy evaluation) Betrachte einen Knoten: Ist es ein Konstruktor (Grundsymbole die zum Aufbau von Datenstrukturen und Ersetzungsregeln dienen) so ist der Graph in Normalform, d.h. es gibt kein redex. Ist es ein Funktionssymbol so werden die Ersetzungsregeln in der Reihenfolge betrachtet, in der sie im Programm angegeben sind. Die erste passende Regel wird angewendet. Eine Regel ist passend wenn sie die Knotenstruktur aufbewahrt, d.h. entsprechende Knoten müssen dieselbe Signatur haben, Konstanten müssen übereinstimmen. Ist beim Vergleich im redex ein Funktionssymbol an einer Stelle vorhanden, wo in der Ersetzungsregel eine Konstante steht, so ruft sich der Algorithmus selber für den Untergraphen der von diesem Funktionssymbol dargestellt wird, auf. Die Auswertung von Untergraphen wird beim Versuch, eine passende Ersetzungsregel zu finden, erzwungen. Version 13.01.2001 Seite 5 Zusammenhang: Termersetzung – Funktionsorientierte Programmiersprache Regeln des Termersetzungssystems sind die Funktionen Ausführung jedes Clean Programms: Auswertung (Reduktion) der „Start“-Funktion. Diese Regel muss in jedem Clean - Programm angegeben sein, nur die dafür erforderlichen Terme werden reduziert. Beispiel: Ein erstes Clean Programm Start :: Int Start = Length [3,4] Length :: [x] -> Int Length [a:x] = 1 + Length x Length [] = 0 Ausführung (Reduktion) des obigen Programms (redex in fett dargestellt): Start Length [3,4] 1 + Length [4] 1 + 1 + Length [] 1 + 1 + 0 1 + 1 2 // // // // // // // a: Start ist der einzige redex b: Dieser Graph als Ganzes ist der neue redex c: + erzwingt die Reduktion seines Zweiten Parameters d: das gleiche noch ein mal e: zweiter Parameter von + durch Normalform ersetzt f: Der ganze Graph wird erneut zum redex g :Graph hat die Normalform erreicht Graphische Darstellung: Bemerkung: Ist der Graph ein Baum so gibt es keinen Unterschied zu einem Termersetzungssystem Version 13.01.2001 Seite 6 Formulierung von Clean Programmen Definition von Funktionen Regeln zur Ersetzung der Terme Kombination: Verwenden bereits definierter Funktionen over n k = fac n / (fac k * fac (n-k)) roots a b c = [ (~b+sqrt(b*b-4.0*a*c)) / (2.0*a) , (~b-sqrt(b*b-4.0*a*c)) / (2.0*a) ] Konstanten: Funktionen ohne Argument pi = 3.1415926535 e = exp 1.0 Fallunterscheidung: signum x | x>0 | x==0 = 1 = 0 = -1 Muster: Partielle Funktionen, Einschränkung des Definitionsbereichs Die Funktion h [1,x,y] = x+y ist nur für Listen bestehend aus 3 Elementen, von denen das erste 1 ist, definiert. Folgende Funktion zählt die Elemente einer Liste reverse :: [a] -> [a] reverse [] = [] reverse [x:xs] = reverse xs ++ [x] Rekursion: length [] = 0 length [_:rest] = 1 + length rest ’_’ kann als Platzhalter für nichtverwendete Teile des Musters stehen Version 13.01.2001 Seite 7 Lokale Definitionen können durch zwei Konstrukte erstellt werden: roots a b c = let s = sqrt (b*b-4.0*a*c) d = 2.0*a in [(~b+s)/d , (~b-s)/d ] und f x y = g(x+w) where g u = u+v where v = u*u w = 2+y Einrückung gibt Aufschluss über den Gültigkeitsbereich zu dem eine Definition gehört, solange man keine geschweiften Klammern verwendet und so die Gültigkeitsbereiche explizit angibt. f x y = g (x+w) where { g u =u + v where { v = u * u }; w = 2 + y }; Diese zwei Darstellungsarten dürfen jedoch auf Modulebene nicht gemeinsam auftreten. Version 13.01.2001 Seite 8 Sorten Jeder Ausdruck ist von einer bestimmten Sorte, z.B.: Start :: Int Start = 3+4 :: heißt “ist von der Sorte” Es gibt 4 Grundsorten: Int, Real, Bool, Char Wenn S eine Sorte ist dann ist auch [S] eine Sorte, die der Listen über Elemente der Sorte S x :: [Int] x = [1,2,3] //Liste von Int z :: [[Bool]] //Liste von Listen von Bools z = [[True,True],[False,True,False]] Sortenangaben sind für Funktionen mit einem oder mehreren Parameter möglich: sum :: [Int] -> Int //Funktion die eine Liste von Ints als Parameter erhält //und ein Int zurückliefert roots :: Real Real Real -> [Real] trigs :: [Real->Real] trigs = [sin,cos,tan] //Funktion die 3 Real Werte als Parameter //erhält und eine Liste davon zurückliefert //Liste von Funktionen die Real in Real überführen Sortenangaben sind nicht verpflichtend, Verwendung erhöht aber Lesbarkeit des Codes und verkleinert die Fehleranfälligkeit Automatische Sortenbestimmung: g g | | 0 y z = y x y z x == y = y otherwise = z Festlegungen: 1) typ(0) = Int //erster Parameter ist 0 2) typ(y) = rückgabetyp(g) //y wird zurückgeliefert 3) typ(x) = typ(y) //x wird mit y verglichen Schlussfolgerungen: 4) aus 1) und 3) => typ(y) = Int 5) aus 4) und 2) => rückgabetyp(g) = Int 6) da z zurückgeliefert wird und 5) => typ(z) = Int Version 13.01.2001 Seite 9 g :: Int Int Int -> Int Version 13.01.2001 Seite 10 Polymorphismus: Sortenvariablen length :: [a] -> Int //Anzahl der Elemente einer Liste von ‚irgendetwas’ hd :: [a] -> a //Das Erste Element einer Liste id :: a -> a id x = x //Identitätsfunktion Überladen von Funktionen Im Allgemeinen ist es nicht möglich denselben Namen für zwei Funktionen zu verwenden, dazu gibt es in Clean Funktionsklassen: Die Definition class (+) infixl 6 a :: a a -> a vereinbart die Klasse der Funktionen die mit + bezeichnet werden, in Infixnotation verwendet werden dürfen und dabei linksbindend wirken, und Priorität 6 haben. Konkrete Implementierung: Instanz instance + Bool where (+) :: Bool Bool -> Bool (+) True b = True (+) a b = b Einschränkung der Sorten: double :: a -> a | + a gibt an dass double für alle Sorten definiert ist für denen die + Funktion definiert ist, double wird dabei zur überladenen Funktion Version 13.01.2001 Seite 11 Spezialformen infix, infixl, infixr ermöglichen das Erstellen von Funktionen die in Infixnotation verwendet werden dürfen: (&&) infixr 1 :: Bool Bool -> Bool dabei gibt 1 die Priorität des Operators an. In den Standardbibliotheken sind die Prioritäten wie folgt definiert: 11: reserviert für Auswahloperatoren von 5: ++ +++ Arrays und Records 10: reserviert für Funktionsaufrufe 4: == 9: o 3: && ! % 8: ^ <> < > >= <= 2: || 7: * / mod rem 1: := 6: + - bitor bitand bitxor 0: ‘bind’ Angabe der Priorität: Klammerersparnis Was passiert wenn ein Operator im selben Ausdruck mehrmals hintereinander verwendet wird? Beispiele aus dem class (^) infixr 8 a :: !a !a -> a //wirkt rechtsbindend 2 ^ 2 ^ 3 2 ^ (2 ^ 3) class (+) infixl 6 a :: !a !a -> a //wirkt linksbindend 2 + 3 + 4 (2 + 3) + 4 class (/) infix 7 a :: !a !a -> a //darf nicht mehrmals hintereinander in einem //Ausdruck vorkommen 64 / 8 / 2 ist ungültig Version 13.01.2001 Seite 12 Funktionen höherer Ordnung Funktionen als Rückgabewert von Funktionen Partielle Parametrisierung – Currying In Clean ist die Verwendung einer Funktion mit weniger Parameter als Vorgeschrieben möglich: successor :: (Int -> Int) successor = plus 1 successor ist von der Sorte‚ Funktion die ein Int in einem Int überführt’ Die Funktionen plus :: Int Int -> Int plus a b = a + b //liefert direkt ein Int und plus :: Int -> (Int -> Int) plus a = (+) a //liefert eine einstellige Funktion die Int liefert sind bis auf ihrer Stelligkeit äquivalent. Funktionen als Parameter twice führt eine Funktion 2 mal hintereinander aus. twice :: (t->t) t -> t twice f x = f (f x) twice inc inc inc 1+1 2 inc 0 (inc 0) (0+1) 1 twice twice inc 0 twice (twice inc) 0 (twice inc) ((twice inc) 0) * (twice inc) 2 // wie daneben inc (inc 2) * inc 3 * 4 Andere Beispiele: map :: (a -> b) [a] -> [b] until :: (a->Bool) (a->a) a -> a Die Lambda Notation: Führt eine lokale, anonyme Funktion ein: \ Muster -> Ausdruck Beispiel aus der Standardbibliothek von Clean: Operator o zur Hintereinanderausführung von Funktionen (o) infixr 9 :: (b -> c) (a -> b) -> (a -> c) (o) g f = \x -> g (f x) Version 13.01.2001 Seite 13 Zusammengesetzte Daten Listen Sind in Clean Anreihungen von Elementen derselben Sorte Konstruktion durch Aufzahlung der Elemente [1,3,7,2,8] :: [Int] [3<4,a==5,p && q] :: [Bool] Konstruktion mit dem : Operator (vgl. Scheme: cons) Spiegelt interne Darstellung der Listen xs = [1,2,3] ist eigentlich eine Abgekürzte Schreibweise für xs = [1:[2:[3:[]]]] Konstruktion durch Aufzählung von Intervallen mit der .. Notation xs = [1..9] xs = [1,3..20] //Sonderfall: Intervallänge 1 ’..’ Notation wird vom Compiler durch die from_then_to Funktion ersetzt from_then_to : a a a -> [a] | Enum a from_then_to n1 n2 e | n1 <= n2 = _from_by_to n1 (n2-n1) e | otherwise = _from_by_down_to n1 (n2-n1) e where from_by_to n s e | n <= e = [n : _from_by_to (n+s) s e] | otherwise = [] from_by_down_to n s e | n >= e = [n : _from_by_down_to (n+s) s e] | otherwise = [] Konstruktion durch Angabe einer charakteristischen Eigenschaft Aus der Mengenlehre bekannt: V x 2 x N , x mod 2 0 Clean kennt eine ähnliche Konstruktionsweise für Listen: lst :: [Int] lst = [x*x \\ x <- [1..10] | x mod 2 == 0] Beispiel: Quicksort unter Verwendung dieses Konstrukts: qsort :: [a] -> [a] | Ord a //für Sorte a muss der Vergleichsoperator existieren qsort [] = [] Version 13.01.2001 Seite 14 qsort [a:xs] = qsort [x \\ x<-xs | x<a] ++ [a] ++ qsort [x \\ x<-xs | x>=a] Version 13.01.2001 Seite 15 Unendliche Listen – Lazy Evaluation Die Auswertungsreihenfolge (Reduktionsstrategie) mit der Clean arbeitet erlaubt es, potentiell unendlich lange Listen als Zwischenergebnisse zu verwenden. Natürlich hält ein Programm nicht, wenn die Reduktion einer unendlich langen Liste erzwingt wird. Beispiel: Erstellen eine Liste aller natürlichen Zahlen n >= 1 mit 3^n < 5 takeWhile ((>) 5) (map ((^)) 3) [1..]) takeWhile ((>) 5) (map ((^) 3) [1:[2..]]) takeWhile ((>) 5) [(^) 3 1:map ((^) 3) [2..]] takeWhile ((>) 5) [3:map ((^) 3) [2..]] [3:takeWhile ((>) 5) (map ((^) 3) [2..])] //3 < 5 [3:takeWhile ((>) 5) (map ((^) 3) [2:[3..]])] [3:takeWhile ((>) 5) [(^) 3 2: map ((^) 3) [3..]]] [3:takeWhile ((>) 5) [9: map ((^) 3) [3..]]] //9 > 5 [3:[]] Anderes Beispiel: Unendlich lange Liste der Primzahlen nach dem Algorithmus von Eratosthenes primes :: [Int] primes = sieve [2..] sieve :: [Int] -> [Int] sieve [prime : rest] = [prime : sieve[i \\ i <- rest | i mod prime <> 0]] Um sich den Anfang dieser Liste anzusehen, kann man z.B. die takeWhile Funktion verwenden: Start = takeWhile ((>) 100) primes Lazy evaluation ist nicht nur bei unendlich langen Listen vorteilhaft: prime :: Int -> Bool prime x = divisors x == [1,x] //divisors liefert Liste der Teiler Eine Applikative Auswertungsreihenfolge hätte als Folge die Erzeugung der Liste aller Teiler von x. Mit der Lazy evaluation von Clean werden z.B. für x = 30 nur die ersten 2 Elemente der Liste divisors x berechnet, dieses reicht dem == Operator um festzustellen dass die beiden Listen ungleich sind. (==) [x:xs] [y:ys] = x==y && xs==ys //rekursive Zeile des Vergleichsoperators (&&) False x = False (&&) True x = x Version 13.01.2001 //Logischer UND Operator: wenn das erste Argument //False ist, wird x nicht ausgewertet Seite 16 Tupel Anreihungen mit fester Länge von Elementen verschiedener Sorten (1,'a') :: (Int,Char) ("foo",True,2) :: (String,Bool,Int) ([1,2],sqrt) :: ([Int],Real->Real) (1,(2,3)) :: (Int,(Int,Int)) Können zur Sortenbildung und in Muster verwendet werden Sind bei Funktionen die mehr als ein Rückgabewert haben sollen, nützlich Beispiel aus der Standardbibliothek: Aufteilung von Listen splitAt :: Int [a] -> ([a],[a]) //Liefert ein 2-Tupel von Listen splitAt 0 xs = ([] ,xs) splitAt n [] = ([] ,[]) splitAt n [x:xs] = ([x:ys],zs) where (ys,zs) = splitAt (n-1) xs Records Unterschied zu Tupeln: Jedes Element wird durch einen Namen eindeutig bestimmt. //Vereinbaren :: Person = { , , } der neuen Sorte name :: String birthdate :: (Int,Int,Int) cleanuser :: Bool //Erstellen eines Records von der Sorte Person SomePerson :: Person SomePerson = { name = "Rinus" , birthdate = (10,26,1952) , cleanuser = True } Arrays Anreihungen mit fester Länge von Elementen derselben Sorte Sehr effizient, da die Zugriffszeit auf ein Element konstant ist //Vereinbarung MyArray :: {Int} MyArray = {1,3,5,7,9} //Auswählen des 2-ten Elements MyArray.[2] Arten von Arrays: ’lazy array’ : Speicherblock mit Zeigern auf Elemente, diese Werden nur bei Bedarf ausgewertet (reduziert). Notation: {Sorte} ’strict array’ : wie oben, jedoch werden alle Elemente ausgewertet Notation: {!Sorte} ’unboxed array’ : Speicherblock enthält keine Zeiger, sondern direkt Elemente Notation: {#Grundsorte}, wobei Grundsorte eines von Bool Int Real Char ist. Version 13.01.2001 Seite 17 Beispiele Gemeinsame Verwendung von Zwischenergebnisse (Untergraphen) Start = 3 * 7 + 3 * 7 Start = x + x where x = 3 * 7 Start 3 * 7 + 3 * 7 3 * 7 + 21 21 + 21 42 Start x + x where x = 3 * 7 x + x where x = 21 42 power :: Int Int -> Int power x 0 = 1 power x n = x * power x (n-1) Start :: Int Start = power (3+4) 2 Start power (3+4) 2 x * power x (2-1) where x = 3+4 x * power x 1 where x = 3+4 x * x * power x (1-1) where x = 3+4 x * x * power x 0 where x = 3+4 x * x * 1 where x = 3+4 x * x * 1 where x = 7 x * 7 where x = 7 49 Version 13.01.2001 Seite 18 Quicksort – optimierte Fassung Erste Fassung von Quicksort: viel zu großer Aufwand wegen ++ Operator (Listenkonkatenation) qsort :: [a] -> [a] | Ord a //für Sorte a muss der Vergleichsoperator existieren qsort [] = [] qsort [a:xs] = qsort [x \\ x<-xs | x<a] ++ [a] ++ qsort [x \\ x<-xs | x>=a] Zweite Fassung mit separater Ergebnisliste: qsort2 :: [a] -> [a] | Ord a qsort2 l = qs l [] qs :: [a] [a] ->[a] | Ord a qs [] c = c qs [a:xs] c = qs [x \\ x<-xs | x<a] [a:qs [x \\ x<-xs | x>=a] c] Start :: [Int] Start = qsort2 [1,2,1] Start qsort2 [1,2,1] qs [1,2,1] [] qs [] [1:qs [2,1] []] [1:qs [2,1] []] [1:qs [1] [2:qs [] []]] [1:qs [] [1:qs [] [2:qs [] []]]] [1:[1:qs [] [2:qs [] []]]] [1:[1:[2:qs [] []]]] [1:[1:[2:[]]]] = [1,1,2] Version 13.01.2001 Seite 19 Zusammenfassung Clean Funktionsorientierte Programmiersprache Basiert auf Termgraphersetzungssysteme - Ersetzungsstrategie: Lazy Evaluation Typgebunden (Sorten) Literatur: T.H.Brus: CLEAN – A language for functional graph rewriting in: Proc. Conference on Functional Programming Languages and Computer Architecture, Lecture Notes Computer Science 274 (1987) p. 364-384 Functional Programming in Clean, draft, july 1999 Reinhard Bündgen: Termersetzungssysteme, Vieweg Verlag, 1998 Version 13.01.2001 Seite 20