Funktionen höherer Ordnung

Werbung
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
Herunterladen