Weitere oft gewünschte Operationen sind: sortierte Ausgabe, Suchen aller Datensätze mit bestimmten Eigenschaften, bearbeiten von Daten ohne eindeutige Schlüssel, etc. Entsprechend unseren Datensätzen betrachten wir die folgende Signatur / Schnittstelle: signature DICTIONARY = sig type dict; val Empty: dict; val get : dict * int -> bool * string ; val put : dict * int * string -> dict ; val remove: dict * int -> dict; end; Bemerkung: In der Literatur zur funktionalen Programmierung wir „get“ oft „lookup“ oder „search“, „put“ oft „insert“ und „remove“ oft „delete“ genannt. Um den Zusammenhang zu OO-Schnittstellen augenfälliger zu machen, benutzen wir die dort üblichen Namen. Ziel ist es, Datenstrukturen zu finden, bei denen der Aufwand für obige Operationen gering ist. Wir betrachten hier die folgenden Dictionary-Realisierungen: • lineare Datenstrukturen (Übung) • (natürliche) binäre Suchbäume (Vorlesung) 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 191 Binäre Suchbäume Begriffsklärung: (binärer Suchbaum) Ein markierter Binärbaum B ist ein natürlicher binärer Suchbaum (kurz: binärer Suchbaum), wenn die Suchbaum-Eigenschaft gilt, d.h. wenn für jeden Knoten K in B gilt: - Alle Schlüssel im linken Unterbaum von K sind echt kleiner als der Schlüssel von K. - Alle Schlüssel im rechten Unterbaum von K sind echt größer als der Schlüssel von K. Bemerkung: • „Natürlich“ bezieht sich auf das Entstehen der Bäume in Abhängigkeit von der Reihenfolge der Einfüge-Operationen (Abgrenzung zu balancierten Bäumen). • In einem binären Suchbaum gibt es zu einem Schlüssel maximal einen Knoten mit entsprechender Markierung. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 192 Datenstruktur: Wir stellen Dictionaries als Binärbäume mit Markierungen vom Typ dataset dar: datatype btree = Node of dataset * btree * btree | Empty ; Die Konstante Empty repräsentiert das leere Dictionary. Binärbäume, die als Dictionary verwendet werden, müssen die Suchbaum-Eigenschaft erfüllen. Alle Funktionen, die Dictionaries als Parameter bekommen, gehen davon aus, dass die Suchbaum-Eigenschaft für die Parameter gilt. Die Funktionen müssen garantieren, dass die Eigenschaft auch für Ergebnisse gilt. Man sagt: Die Suchbaum-Eigenschaft ist eine Datenstrukturinvariante von Dictionaries. Wir guarantieren die Datenstrukturinvariante u.a. dadurch, dass wir Nutzern der Datenstruktur keinen Zugriff auf den Konstruktor Node geben. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 193 structure Dictionary : DICTIONARY = struct datatype btree = Node of dataset * btree * btree | Empty ; type dict = btree ; fun get ... (* siehe unten *) fun put ... (* siehe unten *) fun remove ... (* siehe unten *) end; Die Struktur Dictionary stellt nur die in der Signatur DICTIONARY vereinbarten Typen, Werte und Funktionen den Nutzern zur Verfügung. Insbesondere sind Node und btree nur in Dictionary verfügbar und nicht für Nutzer von Dictionary. Suchen eines Eintrags: Wenn kein Eintrag zum Schlüssel existiert, liefere (false,““) ; sonst liefere (true,s), wobei s der String zum Schlüssel ist: fun get (Empty, k) = (false,"") | get (Node ((km,s),l,r),k) = if k < km then get (l,k) else if k > km then get (r,k) else (* k = km *) (true,s) 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 194 Einfügen: Algorithmisches Vorgehen: - Neue Knoten werden immer als Blätter eingefügt. - Die Position des Blattes wird durch den Schlüssel des neuen Eintrags festgelegt. - Beim Aufbau eines Baumes ergibt der erste Eintrag die Wurzel. - Ein Knoten wird in den linken Unterbaum der Wurzel eingefügt, wenn sein Schlüssel kleiner ist als der Schlüssel der Wurzel; in den rechten, wenn er größer ist. Dieses Verfahren wird rekursiv fortgesetzt, bis die Einfügeposition bestimmt ist. Beispiel: Einfügen von 33: 45 22 17 42 33 22.11.2007 57 52 65 49 © A. Poetzsch-Heffter, TU Kaiserslautern 195 Die algorithmische Idee lässt sich direkt umsetzen. Beachte aber, dass das Dictionary nicht verändert wird, sondern ein neues erzeugt und abgeliefert wird: fun put (Empty,k,s) = Node((k,s),Empty,Empty) | put (Node ((km,sm),l,r),k,s) = if k < km then Node ((km,sm),put(l,k,s),r) else if k > km then Node ((km,sm),l,put(r,k,s) else (* k = km *) Node ((k,s),l,r) Bemerkungen: • Die Reihenfolge des Einfügens bestimmt das Aussehen des binären Suchbaums: Reihenfolgen: 2;3;1 1;3;2 1 2 1 1;2;3 3 1 3 2 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 2 3 196 • Es gibt sehr viele Möglichkeiten, aus einer vorgegebenen Schlüsselmenge einen binären Suchbaum zu erzeugen. • Bei sortierter Einfügereihenfolge entartet der binäre Suchbaum zur linearen Liste. • Der Algorithmus zum Einfügen ist schnell, insbesondere weil keine Ausgleichs- oder Reorganisationsoperationen vorgenommen werden müssen. Löschen: Löschen ist die schwierigste Operation, da - ggf. innere Knoten entfernt werden und dabei - die Suchbaum-Eigenschaft erhalten werden muss. Algorithmisches Vorgehen: - Die Position eines zu löschenden Knotens K mit Schlüssel X wird nach dem gleichen Verfahren wie beim Suchen eines Knotens bestimmt. - Dann sind drei Fälle zu unterscheiden: 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 197 1. Fall: K ist ein Blatt. Lösche K: Y X Y Z Z Entsprechend, wenn X in rechtem Unterbaum. 2. Fall: K hat genau einen Unterbaum. K wird im Eltern-Knoten durch sein Kind ersetzt und gelöscht: Y Y X Z Z Die anderen links-rechts-Varianten entsprechend. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 198 3. Fall: K hat genau zwei Unterbäume. Problem: Wo werden die beiden Unterbäume nach dem Löschen eingehängt? Hier gibt es 2 symmetrische Lösungsvarianten: - Ermittle den Knoten KR mit dem kleinsten Schlüssel im rechten Unterbaum, Schlüssel von KR sei XR. - Speichere XR und die Daten von KR in K. - Lösche KR gemäß Vorgehen zu Fall 1 bzw. 2. (andere Variante: größten Schlüssel im rechten UB) X XR Y Z Y Z XR 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 199 Umsetzung in ML: • Die Fälle 1 und 2 lassen sich direkt behandeln. • Für Fall 3 realisiere Hilfsfunktion removemin, die - nichtleeren binären Suchbaum b als Parameter nimmt; - ein Paar (min,br) als Ergebnis liefert, wobei -- min der kleinste Datensatz in b ist und -- br der Baum ist, der sich durch Löschen von min aus b ergibt. fun removemin (Node (d,Empty,r)) = (d,r) | removemin (Node (d,l,r)) = let val (min,ll) = removemin l; in (min, Node(d,ll,r)) end; fun remove (Empty,k) = Empty | remove (Node((km,s),l,r),k)= if k < km then Node ((km,s), remove(l,k), r) else if k > km then Node ((km,s), l, remove(r,k)) else (* k = km *) case (l,r) of (Empty,rt) => r | (lt,Empty) => l | (lt,rt) => let val (min,rr) = removemin r in Node (min,l,rr) end 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 200 Diskussion: Der Aufwand für die Grundoperationen Einfügen, Suchen und Löschen eines Knotens ist proportional zur Tiefe des Knotens, bei dem die Operation ausgeführt wird. Ist h die Höhe des Suchbaumes, ist der Aufwand der Grundoperationen im ungünstigsten Fall also O(h), wobei log (N+1)-1 ≤ h ≤ N-1 für Knotenanzahl N. Folgerung: Bei degenerierten natürlichen Suchbäumen kann linearer Aufwand für alle Grundoperationen entstehen. Im ungünstigsten Fall ist das Laufzeitverhalten also schlechter als bei der binären Suche auf Feldern. Im Mittel verhalten sich Suchbäume aber wesentlich besser (O(log N)). Zusätzlich versucht man durch gezielte Reorganisation eine gute Balancierung zu erreichen (siehe Kapitel 5). Bemerkung: Mit modifizierenden Operationen kann man das Auf- und Abbauen der Suchbäume vermeiden und damit die Effizienz steigern. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 201 3.3 Abstraktion mittels Polymorphie und Funktionen höherer Ordnung Überblick: • Grundbegriffe der Typisierung • Polymorphie als Abstraktionsmittel • Typsysteme und Typinferenz • Einführung in Funktionen höherer Ordnung • Wichtige Funktionen höherer Ordnung • Abstraktionen über Datenstrukturen 3.3.1 Typisierung Inhalte: • Was ist ein Typ? • Ziele der Typisierung • Polymorphie und parametrische Typen • Typsystem von ML und Typinferenz Fast alle modernen Spezifikations- und Programmiersprachen besitzen ein Typsystem. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 202 Was ist ein Typ? Ein Typ beschreibt Eigenschaften von Elementen der Modellierung oder Programmierung: • Elementare Typen stehen häufig für die Eigenschaft, zu einer bestimmten Wertemenge zu gehören ( bool, int, char, string, ... ). • Zusammengesetzte Typen beschreiben die genaue Struktur ihrer Elemente ( Tupel-, Listenund Funktionstypen). • Parametrische Typen beschreiben bestimmte Eigenschaften und lassen andere offen; z.B.: - Elemente vom Typ ‘a list sind homogene Listen; man kann also null, hd, tl anwenden. Offen bleibt z.B. der Ergebnistyp von hd. - Elemente vom Typ ‘a * ‘a list sind Paare, so dass die Funktionen für Paare angewendet werden können. Außerdem besitzen sie die Eigenschaft, dass die zweite Komponente immer eine Liste ist, deren Elemente vom selben Typ sind wie die erste Komponente: fun f ( p:('a * 'a list ) ): 'a list = let val (fst,scd) = p in fst::scd end; 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 203 Ziele der Typisierung Die Typisierung verfolgt drei Ziele: • Automatische Erkennung von Programmierfehlern (durch Übersetzer, Interpreter); • Verbessern der Lesbarkeit von Programmen; • Ermöglichen effizienterer Implementierungen. Wir konzentrieren uns hier auf das erste Ziel. Zentrale Idee: - Für jeden Ausdruck und jede Funktion wird ein Typ festgelegt. - Prüfe, ob die Typen der aktuellen Parameterausdrücke mit der Signatur der angewendeten Funktion oder Operation übereinstimmen. Beispiele: (Typprüfung von Ausdrücken) f: int Æ int, dann sind: f 7 , f (hd [ 1,2,3 ]) , f ( f 78 ) + 9 typkorrekt; f true , [ f, 5.6 ] , f hd nicht typkorrekt. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 204 Bemerkung: Typisierung war lange Zeit nicht unumstritten. Hauptgegenargumente sind: • zusätzlicher Schreibaufwand • Einschränkung der Freiheit: - inhomogene Listen - Nutzen der Repräsentation von Daten im Rechner Beispiel: Es gibt viele Programme, die nicht typkorrekt sind, sich aber trotzdem zur Laufzeit gutartig verhalten; z.B: Aufgabe 1: Schreibe eine Funktion mp: - Eingabe: Liste von Paaren entweder vom Typ bool * int oder bool * real - Zulässige Listen: Wenn 1. Komponente true, dann 2. Komponente vom Typ int, sonst vom Typ real - Summiere die Listenelemente und liefere ein Paar mit beschriebener Eigenschaft. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 205 Realisierung in ML-Notation (kein ML-Programm, da nicht typkorrekt!): fun mp [ ] = | mp ((true,n)::xs ) = (true, 0) case mp xs of (true,k) => (true, k+n ) | (false,q) => (false, q + (real n)) | mp ((false,r)::xs ) case mp xs (true,k) = of => | (false,q) => (false, (real k) + r) (false, q + r) Bemerkung: Wegen fehlender Typisierung ist es schwierig, die Überladung des Pluszeichens aufzulösen. Aufgabe 2: Schreibe eine Funktion, - die ein n-Tupel (n>0) nimmt und - die erste Komponente des Tupels liefert. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 206 Realisierung in ML-Notation (kein ML-Programm, da nicht typkorrekt!) : - fun f n = #1 n; stdIn:14.1-14.15 Error: unresolved flex record (can't tell what fields there are besides #1) Polymorphie und parametrische Typen Im Allg. bedeutet Polymorphie Vielgestaltigkeit. In der Programmierung bezieht sich Polymorphie auf die Typisierung bzw. das Typsystem. Begriffsklärung: (polymorphes Typsystem) Das Typsystem einer Sprache S beschreibt, - welche Typen es in S gibt bzw. wie neue Typen deklariert werden; - wie den Ausdrücken von S ein Typ zugeordnet wird; - welche Regeln typisierte Ausdrücke erfüllen müssen. Ein Typsystem heißt polymorph, wenn es Werte bzw. Objekte gibt, die zu mehreren Typen gehören. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 207 Bemerkung: • Man unterscheidet: - Parametrische Polymorphie - Subtyp-Polymorphie (vgl. Typisierung in Java) • Oft spricht man im Zusammenhang mit der Überladung von Funktions- oder Operatorsymbolen von Ad-hoc-Polymorphie. Beispiel: Dem +-Operator könnte man in ML den Typ „int * int Æ int oder real * real Æ real “ geben. • In polymorphen Typsystemen gibt es meist eine Relation „ist_spezieller_als“ zwischen Typen T1, T2 : T1 heißt spezieller als T2, wenn die Eigenschaften, die T1 garantiert, die Eigenschaften von T2 implizieren (umgekehrt sagt man: T2 ist allgemeiner als T1). Beispiel: Der Typ int list ist spezieller als der parametrische Typ ‘a list . Insbesondere gilt: Jeder Wert vom Typ int list kann überall dort benutzt werden, wo ein Wert vom Typ ‘a list erwartet wird. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 208 Typsystem von ML und Typinferenz Typen werden in ML durch Typausdrücke beschrieben: - Typkonstanten sind die elementaren Datentypen: bool, int, char, string, real, ... - Typvariablen: ‘a, ‘meineTypvar, ‘‘a, ‘‘gTyp - Ausdrücke gebildet mit Typkonstruktoren: Sind TA, TA1, TA2, TA3, ... Typausdrücke, dann sind die folgenden Ausdrücke auch Typausdrücke: TA1 * TA2 Typ der Paare TA1 * TA2 * TA3 Typ der Triple ... TA list Listentyp TA1 -> TA2 Funktionstyp ... 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 209 Beispiele: (Typausdrücke) - int, bool - ‘a, ‘b - int * bool , int * ‘a , int * ‘b * ‘a - int list, ‘a list , (int * ‘b * ‘a ) list - int -> int , ‘a list -> (int * ‘b * ‘a ) list - (int -> int) -> (real -> real) Präzedenzregeln für Typkonstruktoren: list bindet am stärksten * bindet am zweitstärksten -> bindet am schwächsten und ist rechtsassoziativ Beispiele: (Präzedenzen) int * bool list steht für int * ( bool list ) int * real -> int list steht für (int * real) -> (int list ) ‘a -> char -> bool ‘a -> ( char -> bool ) 22.11.2007 steht für © A. Poetzsch-Heffter, TU Kaiserslautern 210 Begriffserklärung: (Vergleich von Typen) Seien TA und TB Typausdrücke und var(TA) die Menge der Typvariablen, die in TA vorkommen. Eine Variablensubstitution ist eine Abbildung β von var(TA) auf die Menge der Typausdrücke. Bezeichne TA β den Typausdruck, den man aus TA erhält, wenn man alle Variablen v in TA konsistent durch β (v) ersetzt. TB ist spezieller als TA, wenn es eine Variablensubstitution β gibt, so dass TA β = TB. TA und TB bezeichnen den gleichen Typ, wenn TA spezieller als TB ist und TB spezieller als TA. Beispiele: int list ist spezieller als ‘a list ‘a list ist spezieller als ‘b list ‘a -> ‘a ist spezieller als und umgekehrt ‘a -> ‘b int list * ‘b und ‘c list * bool sind nicht vergleichbar, d.h. der erste Ausdruck ist nicht spezieller als der zweite und der zweite nicht spezieller als der erste. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 211 Bemerkung: • Bezeichne TE die Menge der Typausdrücke in ML. - Die „ist_spezieller_als“ Relation auf TE x TE ist reflexiv und transitiv, aber nicht antisymmetrisch. - Identifiziert man alle Typausdrücke, die den gleichen Typ bezeichnen, erhält man die Menge T der Typen. ( T, ist _spezieller_als ) ist eine partielle Ordnung. • Das Typsystem von ML ist feiner als hier dargestellt. Insbesondere wird zwischen allen Typen und der Teilmenge von Typen unterschieden, auf denen die Gleichheit definiert ist, den sogenannten Gleichheitstypen. Beispiel: - fun listcompare [] [] = true | listcompare [] (x::xs) = false | listcompare (x::xs) [] = false | listcompare (x::xs) (y::ys) = if x=y then listcompare xs ys else false; val listcompare = fn : ''a list -> ''a list -> bool 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 212 • Typvariablen für Gleichheitstypen beginnen mit zwei Hochkommas. Damit verstehen wir nun auch den Typ der Gleichheitsfunktion in ML: - val eq = fn (x,y) => x = y ; val eq = fn : ''a * ''a -> bool Damit ist gesagt, was Typen in ML sind. Typregeln in ML: Wir nennen eine Ausdruck A typannotiert, wenn A und allen Teilausdrücken von A ein Typ zugeordnet ist. Beispiel: Die Typannotationen von ist abs ~2 ( (abs:int->int)(~2:int) ):int Die Typregeln legen fest, wann ein typannotierter Ausdruck typkorrekt ist. In ML muss gelten: - die Typen der formalen Parameter müssen gleich den Typen der aktuellen Parameter sein. - if-then-else, andalso, orelse müssen korrekt typisierte Parameter haben. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 213 Typinferenz: Typinferenz bedeutet das Ableiten der Typannotation für Ausdrücke aus den gegebenen Deklarationsinformationen. Sie ist in gängigen Programmiersprachen oft recht einfach, in modernen Programmiersprachen teilweise recht komplex. Beispiele: 1. Leere Liste: - val a = [ ]; val a = [ ] : 'a list 2. Einsortieren in geordnete Liste von Zahlen: - fun einsortieren (p1,p2) = case p2 of x::xs => if p1 <= x then p1::p2 else x::(einsortieren (p1,xs)) | [ ] => [ p1 ] ; val einsortieren = fn : int * int list -> int list 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 214 3. Einsortieren in geordnete Liste mit Vergleichsfunktion: - fun einsortieren2 (v,p1,p2) = case p2 of x::xs => if v (p1,x) then p1::p2 else x::(einsortieren2 (v,p1,xs)) | [ ] => [ p1 ] ; val einsortieren2 = fn: ('a * 'a-> bool) * ‘a * 'a list -> 'a list Bemerkung: Bei der Typinferenz versucht man immer den allgemeinsten Typ herauszufinden. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 215 3.3.2 Funktionen höherer Ordnung Überblick: • Einführung in Funktionen höherer Ordnung • Wichtige Funktionen höherer Ordnung • Abstraktionen über Datenstrukturen Einführung Funktionen höherer Ordnung sind Funktionen, die - Funktionen als Argumente nehmen und/oder - Funktionen als Ergebnis haben. Selbstverständlich sind auch Listen oder Tupel von Funktionen als Argumente oder Ergebnisse möglich. Eine Funktion F, die Funktionen als Argumente nimmt und als Ergebnis liefert, nennt man häufig auch ein Funktional. Funktionale, die aus der Schule bekannt sind, sind - Differenzial - unbestimmtes Integral 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 216 Sprachliche Aspekte: Alle wesentlichen Sprachmittel zum Arbeiten mit Funktionen höherer Ordnung sind bereits bekannt: - Funktionsabstraktion - Funktionsdeklaration - Funktionsanwendung - Funktionstypen Konzeptionelle Aspekte: Zwei konzeptionelle Aspekte liegen der Anwendung von Funktionen höherer Ordnung in der SoftwareEntwicklung zugrunde: 1. Abstraktion und Wiederverwendung 2. Metaprogrammierung, d.h. das Entwickeln von Programmen, die Programme als Argumente und Ergebnisse haben. Wir betrachten im Folgenden den ersten Aspekt. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 217 Aufgabe: Sortiere eine Liste xl von Zahlen Rekursionsidee: - Sortiere zunächst den Rest der Liste. - Das ergibt eine sortierte Liste xs. - Sortiere das erste Element von xl in xs ein. Umsetzung in ML: - fun einsortieren e [ ] | = [ e ] einsortieren e (x::xr) = if e <= x then else e::x::xr x::(einsortieren e xr) ; val einsortieren = fn : int -> int list -> int list - fun sort [] = [] | sort (x::xr) = einsortieren x (sort xr); val it = fn : int list -> int list Frage: - Warum kann man mit sort nicht auch Werte der Typen char, string, real, etc. sortieren? Antwort: Auflösung der Überladung von <= 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 218 Wiederverwendung zur Sortierung von real-Listen: - fun einsortieren (e:real) [] = [e] | einsortieren e (x::xr) if e <= x then else = e::x::xr x::(einsortieren e xr) ; val einsortieren = fn : real -> real list -> real list Unbefriedigend: Verändern einer Funktion, die für den Anwender von sort ggf. nicht bekannt. Abstraktion durch Parametrisierung: Wiederverwendung von sort wird möglich, wenn wir die Vergleichsoperation als zusätzlichen Parameter einführen: - fun einsortieren vop e [ ] = | einsortieren vop e (x::xr) = if vop (e,x) then else - fun sort | sort e::x::xr x::(einsortieren vop e xr) ; vop [ ] vop (x::xr) einsortieren 22.11.2007 [ e ] = = [ ] vop x (sort vop xr) ; © A. Poetzsch-Heffter, TU Kaiserslautern 219 Anwendung: - sort (op <=) [ 2,3, 968,~98,34,0 ] ; val it = [~98,0,2,3,34,968] : int list - sort (op >=) [ 2,3, 968,~98,34,0 ] ; val it = [968,34,3,2,0,~98] : int list - sort ((op >=):real*real->bool) [1.0, 1e~4 ]; val it = [1.0,0.0001] : real list - val strcmp = ((op <=):string*string->bool); - sort strcmp ["Abbay", "Abba", "Ara", "ab"]; val it = ["Abba","Abbay","Ara","ab"] : string list Bemerkung: Polymorphe Funktionen können häufig auch Funktionen als Parameter nehmen. Beispiel: 1. Funktion cons: abs::fac::nil 2. Identitätsfunktion: (fn x => x) (fn x => x) (Ausdruck ist von der Typisierung her problematisch) 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 220 Wichtige Funktionen höherer Ordnung Dieser Abschnitt betrachtet einige Beispiele für Funktionen höherer Ordnung und diskutiert das Arbeiten mit solchen Funktionen. Funktionskomposition: - fun fcomp f g = (fn x => f(g x)); val fcomp = fn : ('a->'b)->('c->'a)->'c->'b In ML gibt es die vordefinierte Infix-Operation o für die Funktionskomposition. Map: Anwendung einer Funktion auf die Elemente einer Liste map f [ x1, ..., xn ] = [ (f x1), ... , (f xn) ] - fun map f [] | = [] map f (x::xs) = (f x):: map f xs; Anwendungsbeispiel 1: - map size [“Schwerter“,“zu“,“Pflugscharen“]; val it = [9,2,12] : int list 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 221 Anwendungsbeispiel 2: Aufgabe: - Eingabe: Liste von Listen von ganzen Zahlen - Ausgabe: gleiche Listenstruktur, Zahlen verdoppelt - fun double n = 2*n; - map (map double) [ [1,2], [34829] ]; val it = [ [2,4], [69658] ] : int list list Currying und Schönfinkeln: Funktionen mit einem Argumenttupel kann man die Argumente auch sukzessive geben. Dabei entstehen Funktionen höherer Ordnung. Beispiele: - fun times m n = m * n; val times = fn : int -> int -> int - val double = times 2; val double = fn : int -> int - double 5; val it = 10 : int 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 222 Zwei Varianten von map im Vergleich: 1. Die beiden Argumente als Paar: - fun map (f , [ ]) | = [ ] map (f,(x::xs)) = (f x) :: map (f,xs); val map = fn: ('a->'b) * 'a list -> 'b list 2. Die Argumente nacheinander („gecurryt“): - fun map f [] | = [] map f (x::xs) = (f x) :: map f xs; val map = fn: ('a->'b) -> 'a list -> 'b list Bemerkung: Die gecurryte Fassung ist flexibler: Sie kann nicht nur auf ein vollständiges Argumententupel angewendet werden, sondern auch zur Definition neuer Funktionen mittels partieller Anwendung benutzt werden. Beispiele: val double = times 2 ; val ilsort = sort (op <=) ; val doublelist = 22.11.2007 map double ; © A. Poetzsch-Heffter, TU Kaiserslautern 223 Curryen von Funktionen: Die Funktion curry liefert zu einer Funktion auf Paaren die zugehörige gecurryte Funktion: - fun curry f x y = f (x,y); val curry = fn: ('a*'b->'c) -> 'a -> 'b -> 'c Prüfen von Prädikaten für Listenelemente: Die folgenden Funktionen exists und all prüfen, ob - es Elemente in einer Liste gibt, die ein gegebenes Prädikat erfüllen bzw. - alle Elemente einer Liste ein gegebenes Prädikat erfüllen. - fun exists pred [ ] = false | exists pred (x::xs) = pred x orelse exists pred xs; val exists = fn: ('a->bool) -> 'a list -> bool - fun all pred [ ] | = true all pred (x::xs) = pred x andalso all pred xs; val all = fn : ('a -> bool) -> 'a list -> bool 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 224 Anwendungsbeispiel: Prüfen, ob ein Element in einer Liste enthalten ist: - fun ismember x xs = exists (fn y=> x=y) xs; val ismember = fn : ''a -> ''a list -> bool Punktweise Veränderung von Funktionen: Die „Veränderung“ einer Funktion an einem Punkt des Argumentbereichs: - fun update f x v y = if x = y then v else f y ; val update = fn:(''a->'b)->''a->'b-> ''a -> 'b Falten von Listen: Eine sehr verbreitete Funktion ist das Falten einer Liste mittels einer binären Funktion und einem neutralen Element: foldr ⊗ n [e1, e2,..., en ] = e1 ⊗ (e2 ⊗ (...(en ⊗ n) ...)) Deklaration von foldr: - fun foldr f n [ ] | = n foldr f n (x::xs) = f (x,foldr f n xs); val foldr = fn: ('a*'b->'b)->'b->'a list->'b 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 225 Auf Basis von foldr lassen sich viele Listenfunktionen direkt, d.h. ohne Rekursion definieren: - val sum = foldr op+ 0 ; val sum = fn : int list -> int - fun l1 @ l2 = foldr op:: l2 l1; val @ = fn : 'a list * 'a list -> 'a list - fun implode1 cl = foldr op^ "" (map str cl); val implode = fn : char list -> string - val implode2 = foldr (fn(x,y)=>(str x)^y) ““; val implode2 = fn : char list -> string Bemerkung: Die Programmentwicklung mittels Funktionen höherer Ordnung (funktionale Programmierung) ist ein erstes Beispiel für: • das Zusammensetzen komplexerer Bausteine, • programmiertechnische Variationsmöglichkeiten, • die Problematik der Wiederverwendung: - die Bausteine müssen bekannt sein, - die Bausteine müssen ausreichend generisch sein. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 226 Abstraktion über Datenstrukturen Datenstrukturen lassen sich bzgl. einiger in ihnen verwendeten Typen und Funktionen parametrisieren. Der Parameter ist dann kein Wert sondern eine Struktur. Der „Typ“ des Parameters, d.h. dessen Eigenschaften, werden durch eine Signatur angegeben. Eine derart abstrahierte/parametrisierte Datenstruktur nennt man in ML einen Funktor. Ein Funktor nimmt eine Datenstruktur als Parameter und liefert eine Datenstruktur als Ergebnis. Exemplarisch betrachten wir hier Dictionaries, die bzgl. - des Schlüsseltyps key - des Typs der Daten data - eines Dummy-Elements dummydata und - der Vergleichsoperation auf Schlüsseln parametrisiert sind. signature ENTRY = sig eqtype key; type data; val dummydata : data; val leq: key * key -> bool end; 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 227 Der Funktor nimmt also eine Datenstruktur mit Signatur ENTRY als Parameter und liefert eine Datenstruktur für Dictionaries: functor MakeBST ( Et: ENTRY ): sig type dict; val Empty: dict; val get: dict * Et.key -> bool * Et.data ; val put: dict * Et.key * Et.data -> dict ; val remove: dict * Et.key -> dict end = struct open Et; type dataset = key * data ; datatype btree = Node of (dataset*btree*btree) | Empty ; type dict = btree ; fun get (Empty, k) = (false,dummydata) | get (Node ((km,s),l,r),k) = if k = km then (true,s) else if leq(k,km) then get (l,k) else get (r,k) ... (* Fortsetzung auf kommender Folie *) 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 228 (* vgl. Folie 188, 190 und 194 *) fun put (Empty,k,s) = Node((k,s),Empty,Empty) | put (Node ((km,sm),l,r),k,s) = if k = km then Node ((k,s),l,r) else if leq(k,km) then Node ((km,sm),put(l,k,s),r) else (* leq(km,k) *) Node ((km,sm),l,put(r,k,s)) fun removemin (Node(d,Empty,r)) = (d,r) | removemin (Node(d,l,r)) = let val (min,ll) = removemin l; in (min, Node(d,ll,r)) end; fun remove (Empty,k) = Empty | remove (Node((km,s),l,r),k) = if leq(k,km) then Node ((km,s), remove(l,k), r) else if leq(km,k) then Node ((km,s), l, remove(r,k)) else (* k = km *) case (l,r) of (Empty,rt) => r | (lt,Empty) => l | (lt,rt) => let val (min,rr) = removemin r in Node (min,l,rr) end end; 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 229 Zur Anwendung übergeben wir dem Funktor eine Datenstruktur mit Signatur ENTRY: structure IntStringDataset = struct type key = int; type data = string; val dummydata = ""; val leq = op < end; structure Dictionary = MakeBST(IntStringDataset); Bemerkung: • Bei der Abstraktion über Datenstrukturen übernimmt die Signatur die Rolle des Parameter- und Ergebnistyps. • Wie bei der Funktionsabstraktion geht es auch bei der Abstraktion über Datenstrukturen darum, den einmal geschriebenen Rumpf für mehrere unterschiedliche Eingaben wiederzuverwenden. • Bzgl. der präzisen Abstraktion von Programmen hinsichtlich unterschiedlicher Aspekte sind funktionale Programmiersprachen am weitesten. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 230 Bemerkungen: (zur ML-Einführung) • Ziel des Kapitels war es nicht, eine umfassende ML-Einführung zu geben. ML dient hier vor allem als Hilfsmittel, wichtige Konzepte zu erläutern. • Die meisten zentralen Konstrukte wurden behandelt. • Es fehlt Genaueres zu: - Fehlerbehandlung - Modularisierungskonstrukte (Signaturen, Strukturen, abstrakte Typen, Funktoren) - Konstrukte zur imperativen Programmierung - Ein-/Ausgabe-Operationen; z.B. print : string -> unit • Viele Aspekte der Programmierumgebung wurden vernachlässigt. 22.11.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 231