3. Funktionales Programmieren

Werbung
3. Funktionales
Programmieren
• Grundkonzepte funktionaler Programmierung
• Algorithmen auf Listen und Bäumen
• Abstraktion mittels Polymorphie und
Funktionen höherer Ordnung
• Semantik, Testen und Verifikation
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
1
Inhalt von Kapitel 3:
1. Einführung
2. Grundkonzepte von Softwaresystemen
3. Funktionales Modellieren und Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3.1.1 Zentrale Begriffe und Einführung
3.1.2 Rekursive Funktionen
3.1.3 Listen und Tupel
3.1.4 Benutzerdefinierte Datentypen
3.1.5 Signaturen und Strukturen
3.1.6 Ein- und Ausgabe
3.2 Algorithmen auf Listen und Bäumen
3.2.1 Sortieren
3.2.2 Suchen
3.3 Abstraktion mittels Polymorphie und
Funktionen höherer Ordnung
3.3.1 Typisierung
3.3.2 Funktionen höherer Ordnung
3.4 Semantik, Testen und Verifikation
3.4.1 Zur Semantik funktionaler Programme
3.4.2 Testen und Verifikation
4. Prozedurales Programmieren
5. Objektorientiertes Programmieren
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
2
3.1 Grundkonzepte funktionaler
Programmierung
Vorgehen:
• Zentrale Begriffe und Einführung:
Funktion, Wert, Typ, Datenstruktur, Auswertung, ...
• Rekursive Funktionen
• Listen und Tupel
• Benutzerdefinierte Datentypen
3.1.1 Zentrale Begriffe und Einführung
Funktionale Programmierung im Überblick:
• Funktionales Programm:
- partielle Funktionen von Eingabe- auf Ausgabedaten
- besteht aus Deklarationen von (Daten-)Typen,
Funktionen und (Daten-)Strukturen
- Rekursion ist eines der zentralen Sprachkonzepte
- in Reinform: kein Zustandskonzept, keine veränderlichen Variablen, keine Schleifen, keine Zeiger
• Ausführung eines funktionalen Programms:
Anwendung der Funktion auf Eingabedaten
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
3
Definition:
(partielle Funktion)
Ein Funktion heißt partiell, wenn sie nur auf einer
Untermenge ihres Argumentbereichs definiert ist.
Andernfalls heißt sie total.
Beispiel: (partielle Funktion)
1. Bezeichne nat die Menge der positiven ganzen Zahlen
und sei fact : nat  nat
wie folgt definiert:
1
, für n = 0
fact (n-1) * n
, für n > 0
fact (n) =
Dann ist fact wohldefiniert und total.
2. Bezeichne real die Menge der auf dem Rechner
darstellbaren reellen Zahlen. Dann ist die Funktion
dividiere : real * real  real
dividiere (dvd, dvs) = dvd / dvs
partiell (Division durch Null ist nicht definiert).
3. Bezeichne string die Menge der Zeichenreihen.
Dann ist die Funktion abschneide2, die die ersten
beiden Zeichen einer Zeichenreihe abschneidet
partiell (warum?)
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
4
Definition: (Funktionsanwendung, -auswertung,
Terminierung, Nichtterminierung)
Bezeichne f eine Funktion und a ein zulässiges
Argument von f.
Die Anwendung von f auf a nennen wir eine
Funktionsanwendung (engl. function application);
üblicherweise schreibt man dafür f(a) oder f a .
Den Prozess der Berechnung des Funktionswerts
nennen wir Auswertung (engl. evaluation). Die
Auswertung kann:
- nach endlich vielen Schritten terminieren und ein
Ergebnis liefern (normale Terminierung, engl.
normal termination),
- nach endlich vielen Schritten terminieren und einen
Fehler melden (abrupte Terminierung, engl. abrupt
termination),
- nicht terminieren, d.h. der Prozess der Auswertung
kommt (von alleine) nicht zu Ende.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
5
Bemerkungen:
• Entsprechendes gilt in anderen Programmierparadigmen.
• Da Terminierung nicht entscheidbar ist, benutzt
man in der Informatik häufig partielle Funktionen.
Beispiel:
(Zur Entscheidbarkeit der Terminierung)
McCarthy‘s Funktion:
Sei m : nat  nat
n -10
m( n ) =
wie folgt definiert:
, für n >100
m( m( n+11) ) , für n ≤ 100
Ist m für alle Argumente wohldefiniert?
• In der Theorie kann man durch Einführen eines
Elements „undefiniert“ jede partielle Funktion total
machen. Üblicherweise bezeichnet man das Element
für „undefiniert“ mit ⊥ (engl. „bottom“).
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
6
Begriffsklärung:
(Wert, Value)
Werte (engl. values) in der (reinen) funktionalen
Programmierung sind
- Elementare Daten (Zahlen, Wahrheitswerte,
Zeichen, ...),
- zusammengesetzte Daten (Listen von Werten,
Wertepaare,...),
- (partielle) Funktionen mit Werten als Argumenten
und Ergebnissen.
Bemerkungen:
• In anderen Sprachparadigmen gibt es auch Werte,
allerdings werden Funktionen nicht immer als
Werte betrachtet. (z.B. in der objektorientierten
Programmierung: „immutable objects“)
• Im Mittelpunkt der funktionalen Programmierung
steht die Definition von Wertemengen (Datentypen)
und Funktionen. Funktionale Programmiersprachen
stellen dafür Sprachmittel zur Verfügung.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
7
• Wie für abstrakte Objekte oder Begriffe typisch,
besitzen Werte
- keinen Ort,
- keine Lebensdauer,
- keinen veränderbaren Zustand,
- kein Verhalten.
Begriffsklärung:
(Typ, type)
Typisierte Sprachen besitzen ein Typsystem.
Ein Typ (engl. type) fasst Werte zusammen, auf
denen die gleichen Funktionsanwendungen zulässig
sind. In typisierten Sprachen besitzt jeder Wert
einen Typ.
In funktionalen Programmiersprachen gibt es drei Arten
von Werten bzw. Typen, mit denen man rechnen kann:
• Basisdatentypen ( int, bool, string, ... )
• benutzerdef., insbesondere rekursive Datentypen
• Funktionstypen ( z.B. int  bool oder
( int  int )  ( int  int )
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
)
8
Datenstrukturen
Eine Struktur fasst Typen und Werte zusammen,
insbesondere also auch Funktionen.
Datenstrukturen sind Strukturen, die mindestens
einen „neuen“ Datentyp und alle seine wesentlichen
Funktionen bereitstellen.
Eine Datenstruktur besteht aus einer oder mehrerer
disjunkter Wertemengen zusammen mit den darauf
definierten Funktionen.
In der Mathematik nennt man so ein Gebilde eine
Algebra oder einfach nur Struktur.
In der Informatik spricht man auch von einer
Rechenstruktur.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
9
Definition: (Datenstruktur mit Signatur)
Eine Signatur einer Datenstruktur (T,F) besteht aus
- einer endlichen Menge T von Typbezeichnern und
- einer endlichen Menge F von Funktionsbezeichnern,
wobei für jedes f ∈ F ein Funktionstyp
f : T1 * ... * Tn  T0 , Ti ∈ T, 0 ≤ i ≤ n, 0 ≤ n,
definiert ist. n gibt die Stelligkeit von f an.
Eine (partielle) Datenstruktur mit Signatur (T,F)
ordnet
- jedem Typbezeichner T ∈ T eine Wertemenge,
- jedem Funktionsbezeichner f ∈ F eine partielle
Funktion zu,
so dass Argument- und Wertebereich von f den
Wertemengen entsprechen, die zu f‘s Funktionstyp
gehören.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
10
Bemerkungen:
• Wir betrachten zunächst die Basisdatenstrukturen,
wie man sie in jeder Programmier-, Spezifikationsund Modellierungssprache findet.
• Die Basisdatenstrukturen (engl. basic / primitive
data structures) bilden die Grundlage zur Definition
weiterer Typen, Funktionen und Datenstrukturen.
• Als Beispiel dienen uns die Basisdatenstrukturen
der funktionalen Sprache ML. Später lernen wir
auch die Basisdatenstrukturen von Java kennen.
• Wir benutzen Funktionsbezeichner auch mit
Infix-Schreibweise.
• Funktionen ohne Argumente nennen wir Konstanten.
• Allgemeinere Formen von Strukturen betrachten
wir erst am Ende des Kapitels.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
11
Die Datenstruktur Boolean:
Typ:
bool
Funktionen:
= : bool * bool  bool
<> : bool * bool  bool
not: bool 
bool
Konstanten:
true:
bool
false: bool
Dem Typbezeichner bool ist die Wertemenge
{ true, false } zugeordnet.
=
bezeichnet die Gleichheit auf Wahrheitswerten
<> bezeichnet die Ungleichheit auf Wahrheitsw.
not bezeichnet die logische Negation.
true bezeichnet den Wert true.
false bezeichnet den Wert false.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
12
Bemerkung:
Im Folgenden unterscheiden wir nur noch dann
zwischen Funktionsbezeichner und bezeichneter
Funktion, wenn dies aus Gründen der Klarheit
nötig ist.
Die Datenstruktur Int:
Die Datenstruktur Int erweitert die Datenstruktur
Boolean, d.h. sie umfasst den Typ bool und die
darauf definierten Funktionen. Zusätzlich enthält sie:
Typ:
int
Funktionen:
= : int * int  bool
(* Gleichheit *)
<>: int * int  bool
(* Ungleichheit *)
~ : int  int
(* Negation *)
+ : int * int  int
(* Addition *)
- : int * int  int
(* Subtraktion *)
* : int * int  int
(* Multiplikation *)
abs: int  int
14.10.08
(* Absolutbetrag *)
© A. Poetzsch-Heffter, TU Kaiserslautern
13
div: int * int  int
(* ganzzahlige
Division *)
mod: int * int  int
(* Divisionsrest *)
< : int * int  bool
(* kleiner *)
> : int * int  bool
(* größer *)
<=: int * int  bool
(* kleiner gleich *)
>=: int * int  bool (* größer gleich *)
Konstanten:
- in Dezimaldarstellung: 0, 128, ~245
- in Hexadezimaldarstellung: 0x0, 0x80, ~0xF5
Dem Typbezeichner int ist in ML eine rechnerabhängige Wertemenge zugeordnet, typischerweise
die Menge der ganzen Zahlen von -2^30 bis 2^30-1.
Innerhalb der Wertemenge sind die Funktionen der
Datenstruktur Int verlaufsgleich mit den üblichen
Funktionen auf den ganzen Zahlen.
Außerhalb der Wertemenge sind sie nicht definiert.
Insbesondere bezeichnen +, *, abs, ~
partielle Funktionen.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
14
Begriffsklärung:
Wenn unterschiedliche Funktionen oder andere
Programmelemente den gleichen Bezeichner
haben, spricht man vom Überladen des Bezeichners
(engl. Overloading).
Beispiel: (Überladung von Bezeichnern)
Funktionsbezeichner können überladen werden,
d.h. in Abhängigkeit vom Typ ihrer Argumente
bezeichnen sie unterschiedliche Funktionen.
Die Datenstruktur Real:
Die Datenstruktur Real erweitert die Datenstruktur Int,
d.h. sie umfasst deren Typen und Funktionen sowie:
Typ:
real
Funktionen:
~ : real  real
(* Negation *)
+ : real * real  real
(* Addition *)
- : real * real  real
(* Subtraktion *)
* : real * real  real
(* Multiplikation *)
/ : real * real  real
(* Division *)
abs: real  real
14.10.08
(* Absolutbetrag *)
© A. Poetzsch-Heffter, TU Kaiserslautern
15
< : real * real  bool
(* kleiner *)
> : real * real  bool
(* größer *)
<=: real * real  bool
(* kleiner gleich *)
>=: real * real  bool
(* größer gleich *)
real : int  real
(* nächste reelle Zahl *)
round: real  int
(* nächste ganze Zahl *)
floor: real  int
(* größte ganze Zahl <= *)
ceil : real  int
(* kleinste ganze Zahl >= *)
trunc: real  int (* ganzzahliger Anteil *)
Konstanten:
- mit Dezimalpunkt: 0.0, 1000.0, 128.9, ~2.897
- mit Exponenten:
0e0, 1E3, 1289E~1, ~2897e~3
Dem Typbezeichner real ist in ML eine rechnerabhängige Wertemenge zugeordnet. Entsprechendes
gilt für die präzise Semantik der Funktionsbezeichner.
Bemerkung:
• Die ganzen Zahlen sind in der Programmierung
keine Teilmenge der reellen Zahlen!
• Keine Gleichheitsoperationen auf reellen Zahlen.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
16
Die Datenstruktur Char:
Die Datenstruktur Char erweitert die Datenstruktur
Int, also auch Bool. Zusätzlich enthält sie:
Typ:
char
Funktionen:
= : char * char  bool
<>: char * char  bool
< : char * char  bool
(* kleiner *)
> : char * char  bool
(* größer *)
<=: char * char  bool
>=: char * char  bool
chr: int
 char
ord: char  int
(* char zu ASCII Code
*)
(* ASCII Code zu char
*)
Konstanten:
Konstantenbezeichner haben die Form #“α“, wobei α
-
ein druckbares Zeichen ist,
- die Form \t, \n, \\, \“, ... hat,
- die Form \zzz hat, wobei z eine Ziffer ist und die
dargestellte Dezimalzahl ein legaler Zeichencode ist.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
17
Die Datenstruktur String:
Die Datenstruktur String erweitert die Datenstruktur
Char, d.h. sie umfasst deren Typen und Funktionen.
Zusätzlich enthält sie:
Typ:
string
Funktionen:
= : string * string  bool
<>: string * string  bool
< : string * string  bool
(* kleiner *)
> : string * string  bool
(* größer *)
<=: string * string  bool
>=: string * string  bool
^ : string * string  string
(* Zeichenreihenkonkatenation *)
size: string  int
(* Länge *)
substring: string * int * int  string
(* Teilzeichenreihe *)
str: char  string
(* entsprechende
Zeichenreihe der Länge 1 *)
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
18
Konstanten:
Zeichenreihen eingeschlossen in doppelte
Hochkommas:
“Ich bin ein String!!“
“Ich \098\105\110 ein String!!“
“Mein Lehrer sagt: \“Nehme die Dinge genau!\““
“String vor Zeilenumbruch\n Nach Zeilenumbruch“
Dem Typbezeichner string ist die Menge der
Zeichenreihen über der Menge der Zeichen zugeordnet,
die von der vorliegenden ML-Implementierung
unterstützt werden.
Den Vergleichsoperationen liegt die lexikographische
Ordnung zugrunde, wobei die Ordnung auf den Zeichen
auf deren Code basiert (siehe Datenstruktur Char).
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
19
Bemerkung:
• Es wird unterschieden zwischen Zeichen und
Zeichenreihen der Länge 1.
• Aufbauend auf der Datenstruktur string und
der Listendatenstruktur stellt ML auch die
Funktionen
explode :
string
 char list
implode :
char list

string
zur Verfügung.
• Jede Programmier-, Modellierungs- und
Spezifikationssprache besitzt Basisdatenstrukturen.
Die Details variieren aber teilweise deutlich.
• Wenn Basisdatenstrukturen implementierungsoder rechnerabhängig sind, entstehen
Portabilitätsprobleme.
• Der Trend bei den Basisdatenstrukturen geht
zur Standardisierung.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
20
Aufbau funktionaler Programme
Im Kern, d.h. wenn man die Modularisierungskonstrukte
nicht betrachtet, bestehen funktionale Programme aus:
• der Beschreibung von Werten:
- z.B. (7+23),
30
• Vereinbarung von Bezeichnern für Werte
(einschließlich Funktionen):
- val
x = 7;
• der Definitionen von Typen:
-
type t = ... ;
-
datatype dt = ... ;
Im Folgenden betrachten wir die Sprache ML.
ML bietet ein interaktives Laufzeitsystem, das
Eingaben obiger Form akzeptiert. Selbstverständlich
kann man Eingaben auch aus einer Datei einlesen.
Darüber hinaus gibt es Übersetzer für ML.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
21
Beschreibung von Werten:
- mittels Konstanten oder Bezeichnern für Werte:
23
“Ich bin eine Zeichenreihe“
true
x
- durch direkte Anwendung von Funktionen:
abs(~28382)
“Urin“ ^ “stinkt“
not(true)
- durch geschachtelte Anwendung von Funktionen:
floor (~3.4) = trunc (~3.4)
substring (“Urin“ ^ “stinkt“, 0,3)
- durch Verwendung der nicht-strikten Operationen:
if <boolAusdruck> then <Ausdruck>
else <Ausdruck>
<boolAusdruck> andalso <boolAusdruck>
<boolAusdruck> orelse <boolAusdruck>
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
22
Begriffsklärung: (Ausdruck, expression)
Ausdrücke sind das Sprachmittel zur Beschreibung von
Werten. Ein Ausdruck (engl. expression) in ML ist
- eine Konstante,
- ein Bezeichner (Variable, Name),
- die Anwendung einer Funktion auf einen Ausdruck,
- ein nicht-strikter Ausdruck gebildet mit den
Operationen if-then-else, andalso oder orelse,
- oder ist mit Sprachmitteln aufgebaut, die erst
später behandelt werden.
Jeder Ausdruck hat einen Typ:
- Der Typ einer Konstanten ergibt sich aus der Signatur.
- Der Typ eines Bezeichners ergibt sich aus dem Wert,
den er bezeichnet.
- Der Typ einer Funktionsanwendung ist der
Ergebnistyp der Funktion.
- Der Typ eines if-then-else-Ausdrucks ist gleich
dem Typ des Ausdruck im then- bzw. else-Zweig.
Der Typ der anderen nicht-strikten Ausdrücke ist
bool.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
23
Präzedenzregeln:
Wenn Ausdrücke nicht vollständig geklammert sind,
ist im Allg. nicht klar, wie ihr Syntaxbaum aussieht.
Beispiele:
3 = 5 = true
false = true orelse true
false andalso true orelse true
Präzedenzregeln legen fest, wie Ausdrücke zu
strukturieren sind:
- Am stärksten binden Funktionsanwendungen in
Präfixform.
- Regeln für Infix-Operationen:
infix 7 *, /, div, mod;
infix 6 +, -;
infix 4 = <> < > <= >=;
Je höher die Präzedenzzahl, desto stärker binden
die Operationen.
- andalso bindet stärker als orelse.
- Binäre Operationen sind linksassoziativ (d.h.
sie werden von links her geklammert).
- Vergleichsoperationen binden stärker als nichtstrikte Operationen.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
24
Deklaration und Bezeichnerbindung:
Bisher haben wir Ausdrücke formuliert, die sich auf
die vordefinierten Funktions- und Konstantenbezeichner von ML gestützt haben.
Syntaktisch gesehen, heißt Programmierung:
- neue Typen, Werte und Funktionen zu definieren,
- die neu definierten Elemente unter Bezeichnern
zugänglich zu machen.
Begriffsklärung: (Vereinbarung, Deklaration)
In Programmiersprachen dienen Vereinbarungen
oder Deklarationen (engl. declaration) dazu, den
in einem Programm verwendeten Elementen
Bezeichner/Namen zu geben.
Dadurch entsteht eine Bindung (n,e) zwischen dem
Bezeichner n und dem bezeichneten
Programmelement e.
An allen Programmstellen, an denen die Bindung
sichtbar ist, kann der Bezeichner benutzt werden,
um sich auf das Programmelement zu beziehen.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
25
Bemerkung:
• Die Art der Programmelemente, die in
Deklarationen vorkommen können, hängt von
der Programmiersprache ab. In ML sind es im
Wesentlichen Typen und Werte.
• Die Regeln, die die Sichtbarkeit von Bindungen
bzw. Bezeichern festlegen, sind ebenfalls
sprachabhängig und können sehr komplex sein.
Wir führen die Sichtbarkeitsregeln schrittweise ein.
In Folgenden betrachten wir drei Arten von
ML-Vereinbarungen:
1. Wertvereinbarungen
2. Vereinbarungen (rekursiver) Funktionen
3. Vereinbarungen benutzerdeklarierter Typen
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
26
Wertvereinbarungen:
• haben (u.a.) die Form:
val <Bezeichner>
= <Ausdruck> ;
val <Bezeichner> : <Typ> = <Ausdruck> ;
In der ersten Form wird der Typ des Ausdrucks
zum Typ des Bezeichners, in der zweiten Form
muss der Typ des Ausdrucks gleich dem
vereinbarten Typ sein.
• Der rechtsseitige Ausdruck darf nur Bezeichner
enthalten, die „vorher“ oder im Ausdruck selbst
gebunden wurden.
Beispiele: (Wertvereinbarungen)
val sieben : real = 7.0 ;
val dkv = 3.4 ;
val flag = floor ( ~dkv ) = trunc (~dkv) ;
val dkv = “Deutscher Komiker Verein e.v.“ ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
27
Funktionsvereinbarungen:
Zwei Probleme:
1. bisher keine Ausdrücke, die eine Funktion als
Ergebnis liefern (Ausnahme: Funktionsbezeichner)
2. Funktionen können rekursiv sein, d.h. der
Funktionsbezeichner kommt im definierenden
Ausdruck vor.
Lösungen:
Zu 1.: Erweitere die Menge der Ausdrücke, so dass
Ausdrücke Funktionen beschreiben können.
Dann kann die obige Wertvereinbarung genutzt
werden.
Zu 2.: Benutze spezielle Syntax für rekursive
Funktionsdeklarationen.
Genaueres dazu in Unterabschnitt 3.1.2.
Beispiele: (Funktionsvereinbarungen)
val string2charl:string->char list = explode;
val charl2string = implode ;
fun fac(n:int):int =
if n=0 then 1 else n*fac(n-1) ;
fun fac(n)= if n=0 then 1 else n*fac(n-1) ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
28
Typvereinbarungen:
Zwei Probleme:
1. bisher keine Ausdrücke, die Typen als
Ergebnis liefern
2. Typen können rekursiv sein, d.h. der
vereinbarte Typbezeichner kommt im
definierenden Typausdruck vor.
Lösungen:
Zu 1.: Führe Ausdrücke für Typen ein.
Zu 2.: Benutze spezielle Syntax für rekursive Typdeklarationen.
Genaueres dazu in Unterabschnitt 3.1.4 .
Beispiele: (Typvereinbarungen)
type intpaar = int * int ;
type charlist = char list ;
type telefonbuch =
(( string * string * string * int ) * string list ) list ;
type intNachInt = int -> int ;
val fakultaet : intNachInt = fac ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
29
Begriffsklärung: (Bezeichnerumgebung)
Eine Bezeichnerumgebung ist eine Abbildung von
Bezeichnern auf Werte (einschl. Funktionen) und
Typen, ggf. auch auf andersartige Programmelemente.
Oft spricht man auch von Namensumgebung
oder einfach von Umgebung (engl. Environment).
Beispiel: (Bezeichnerumgebung)
Die elementaren Datenstrukturen von ML bilden
die Standardumgebung für ML-Programme.
Bemerkung:
• Eine Bezeichnerumgebung wird häufig als Liste
von Bindungen modelliert.
• Jede Datenstruktur definiert eine Bezeichnerumgebung.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
30
Begriffsklärung: (funktionales Programm)
Ein funktionales Programm definiert eine
Bezeichnerumgebung für die Anwender des
Programms.
Interaktive Laufzeitumgebungen für funktionale
Programme erlauben das schrittweise Anreichern
und Nutzen von Bezeichnerumgebungen.
Beispiel: (funktionales Programm)
val
val
fun
val
val
val
a
b
f
a
b
d
= 7;
= 8;
(x: int):int = x+a;
= 20;
= true;
= f(a);
Frage: Welchen Wert bezeichnet d?
Bemerkung:
Am obigen Beispiel kann man den Unterschied
zwischen Wertvereinbarung und Zuweisung an
Programmvariable erkennen. Im Folgenden wird er
noch deutlicher werden.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
31
3.1.2 Rekursive Funktionen
Eine Funktion kann man durch einen Ausdruck
beschreiben, in dem die Argumente der Funktion
durch Bezeichner vertreten sind.
Um deutlich zu machen, welche Bezeichner für
Argumente stehen, werden diese deklariert.
Alle anderen Bezeichner des Ausdrucks müssen
anderweitig gebunden werden.
Diesen Schritt von einem Ausdruck zu der
Beschreibung einer Funktion nennt man
Funktionsabstraktion oder λ-Abstraktion.
Beispiele:
(Funktionsabstraktion)
1. Quadratfunktion:
Ausdruck:
x*x
Abstraktion:
λ x. x * x
ML-Notation: fn x => x * x
fn x : real => x * x
Vereinbarung eines Bezeichners für die Funktion:
val sq =
14.10.08
fn x :real => x * x ;
© A. Poetzsch-Heffter, TU Kaiserslautern
32
2. Volumenberechnung eines Kegelstumpfes:
Formel: Sei h die Höhe, r,R die Radien; dann
ergibt sich das Volumen V zu
V=
π*h
3
2
2
*(r +r*R+R )
ML-Ausdruck:
(Math.pi *h) / 3.0 * ( sq(r) + r * R + sq( R ) )
Abstraktion:
λ h, r, R . (Math.pi * h)/3 * ( sq(r) + r * R + sq( R ) )
ML-Notation:
fn ( h:real, r:real, R:real )
=>
(Math.pi * h)/3.0 * (sq(r) + r*R + sq(R))
Vereinbarung eines Bezeichners für die Funktion:
val volkegstu = fn ( h, r, R )
=>
(Math.pi * h)/3.0 * (sq(r) + r*R + sq(R)) ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
33
3. Abstraktion über Funktionsbezeichner:
Ausdruck:
f ( f (x) )
Abstraktion:
λ f, x . f ( f (x) )
ML-Notation:
fn (f: real -> real, x) => f(f(x))
Vereinbarung eines Bezeichners für die Funktion:
val twice = fn (f:real->real,x) => f(f(x))
val erg
= twice ( sq, 3.0 ) ;
4. Abstraktion mit Funktion als Ergebnis:
Ausdruck:
f ( f (x) )
1. Abstraktion:
λ x . f ( f (x) )
2. Abstraktion:
λ f. ( λ x . f ( f (x) ) )
ML-Notation:
fn f =>(fn x:real => f(f(x)))
Vereinbarung eines Bezeichners für die Funktion:
val appl2 = fn f =>(fn x:real => f(f(x))) ;
val pow4
= appl2 ( sq ) ;
val erg
= pow4 ( 3.0 ) ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
34
Funktionsdeklaration
In ML können Funktionen entweder mittels einer
normalen Wertvereinbarung (siehe oben)
oder mit einer speziellen Syntax deklariert werden.
Erster Schritt zur Einführung der speziellen Syntax:
fun <Bezeichner> <Signatur> = <Ausdruck>
and <Bezeichner> <Signatur> = <Ausdruck>
...
and <Bezeichner> <Signatur> = <Ausdruck> ;
wobei die Liste der mit dem Schlüsselwort “and“
begonnenen Zeilen leer sein kann.
Zunächst betrachten wir Signaturen der Form:
<Signatur>
 ( <Params> ) <TypOpt>
<Params>
 ε
| <ParamListe>
<ParamListe>  <ParamDekl>
|
<ParamDekl> , <ParamListe>
<ParamDekl>  <Bezeichner> <TypOpt>
<TypOpt>
 ε
|
14.10.08
: <TypAusdruck>
© A. Poetzsch-Heffter, TU Kaiserslautern
35
Beispiel: (rekursive Funktionsdekl.)
1. Einzelne rekursive Funktionsdeklaration:
fun
rpow
if
( r: real, n: int ): real =
n = 0 then 1.0
else r * rpow (r,n-1) ;
2. Verschränkt rekursive Funktionsdeklaraion:
fun gerade ( n: int ) : bool =
n = 0
orelse ungerade (n-1)
and ungerade ( n: int ) : bool =
if n=0 then false else gerade (n-1)
Deklaration rekursiver Funktionen
Begriffsklärung: (rekursive Definition)
Eine Definition oder Deklaration nennt man
rekursiv, wenn der definierte Begriff bzw. das
deklarierte Programmelement im definierenden
Teil verwendet wird.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
36
Bemerkung:
• Rekursive Definitionen finden sich in vielen
Bereichen der Informatik und Mathematik,
aber auch in anderen Wissenschaften und
der nichtwissenschaftlichen Sprachwelt.
• Wir werden hauptsächlich rekursive Funktionsund Datentypdeklarationen betrachten.
Definition: (rekursive Funktionsdekl.)
Eine Funktionsdeklaration heißt direkt rekursiv,
wenn der definierende Ausdruck eine Anwendung
der definierten Funktion enthält.
Eine Menge von Funktionsdeklarationen heißt
verschränkt rekursiv oder indirekt rekursiv
(engl. mutually recursive), wenn die Deklarationen
gegenseitig voneinander abhängen.
Eine Funktionsdeklaration heißt rekursiv, wenn
sie direkt rekursiv ist oder Element einer Menge
verschränkt rekursiver Funktionsdeklarationen ist.
Begriffsklärung: (rekursive Funktion)
Eine Funktion heißt rekursiv, wenn es rekursive
Funktionsdeklarationen gibt, mit denen sie definiert
werden kann.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
37
Bemerkung:
• Die Menge der rekursiven Funktionen ist
berechnungsvollständig.
• Rekursive Funktionsdeklarationen können als eine
Gleichung mit einer Variablen verstanden werden,
wobei die Variable von einem Funktionstyp ist.
Beispiel: (Definition der Fakultätsfunktion)
Gesucht ist die Funktion f, die folgende Gleichung
für alle n ∈ nat erfüllt:
f(n) = if n=0 then 1 else n * f(n-1)
Zur Auswertung von Funktionsanwendungen:
Sei fun f(x) = A[x]
In ML werden Funktionsanwendungen f(e) nach der
Strategie call-by-value ausgewertet:
• Werte Ausdruck e aus; Ergebnis nennt man den
aktuellen Parameter z .
• Ersetze x in A[x] durch z .
• Werte den resultierenden Ausdruck A[z] aus.
Beispiele: (Rekursion)
siehe Vorlesung
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
38
Formen rekursiver Funktionsdeklarationen:
Vereinfachend betrachten wir nur Funktionsdeklarationen, bei denen die Fallunterscheidung „außen“ und
die rekursiven Aufrufe in den Zweigen der
Fallunterscheidung stehen.
• Eine rekursive Funktionsdeklaration heißt linear
rekursiv, wenn in jedem Zweig der Fallunterscheidung höchstens eine rekursive Anwendung
erfolgt (Beispiel: Definition von fac).
• Eine rekursive Funktionsdeklaration heißt
repetitiv, wenn sie linear rekursiv ist und die
rekursiven Anwendungen in den Zweigen der
Fallunterscheidung an äußerster Stelle stehen.
Beispiele:
1. Die übliche Definition von fac ist nicht repetitiv,
da im Zweig der rekursiven Anwendung die
Multiplikation an äußerster Stelle steht.
2. Die folgende Funktion facrep ist repetitiv:
fun facrep ( n: int, res: int ): int =
if n=0 then res
else facrep( n-1, res*n )
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
39
• Eine rekursive Funktionsdeklaration für f heißt
geschachtelt rekursiv, wenn sie Teilausdrücke
der Form f ( ... f ( ... ) ... ) enthält.
• Eine rekursive Funktionsdeklaration für f heißt
kaskadenartig rekursiv, wenn sie Teilausdrücke
der Form h ( ... f ( ... ) ... f ( ... ) ... ) enthält.
Beispiel: (kaskadenartige Rekursion)
Berechne: Wie viele Kaninchen-Pärchen gibt es
nach n Jahren, wenn man
• am Anfang mit einem Pärchen beginnt,
• jedes Pärchen nach zwei Jahren und dann jedes
folgende Jahr ein weiteres Pärchen Nachwuchs
erzeugt und
• die Kaninchen nie sterben.
Die Anzahl der Pärchen stellen wir als Funktion fib
von n dar:
• vor dem
1. Jahr:
fib(0) = 1
• nach dem 1. Jahr:
fib(1) = 1
• nach dem 2. Jahr:
fib(2) = 2
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
40
nach dem n. Jahr:
die im Jahr vorher existierenden Pärchen
plus die Anzahl der neu geborenen und die ist
gleich der Anzahl von vor zwei Jahren, also gilt:
fib( n ) = fib( n-1 ) + fib( n-2 )
für n > 1.
Insgesamt ergibt sich folgende kaskadenartige
Funktionsdeklaration:
fun fib ( n: int ):int =
if n <= 1 then 1 else fib (n-1) + fib (n-2)
Bemerkung:
• Aus Beschreibungssicht spielt die Form der
Rekursion keine Rolle; wichtig ist eine möglichst
am Problem orientierte Beschreibung.
• Aus Programmierungssicht spielt Auswertungseffizienz eine wichtige Rolle, und diese hängt
von der Form der Rekursion ab.
Beispiel:
Kaskadenartige Rekursion führt im Allg. zu einer
exponentiellen Anzahl von Funktionsanwendungen.
( z.B. bei fib(30) bereits 1.664.079 Anwendungen )
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
41
3.1.3 Listen und Tupel
Die Datenstruktur der Listen
Eine Liste über einem Typ T ist eine total geordnete
Multimenge mit endlich vielen Elementen aus T
(bzw. eine endliche Folge, d.h. eine Abb. {1,...,n }  T ).
ML stellt standardmäßig eine Datenstruktur für Listen
bereit, die bzgl. des Elementtyps parametrisiert ist:
Typen: Ist t ein ML-Typ, dann bezeichnet t list
den Typ der Listen mit Elementen aus t.
Funktionen:
hd
: t list  t
tl
: t list  t list
_::_
: t * t list  t list
null
: t list  bool
(* cons *)
(* Test, ob Liste leer *)
length: t list  int
_@_
: t list * t list  t list
rev
: t list  t list
(* Umkehren einer Liste / engl. revert *)
Konstanten:
nil
14.10.08
:  t list
© A. Poetzsch-Heffter, TU Kaiserslautern
42
Dem Typ t list ist als Wertemenge die Menge
aller Listen über Elementen vom Typ t zugeordnet.
Bemerkungen:
• In ML gibt es eine spezielle Notation für Listen:
statt
kann man
a :: a
k
[ a ,a
k
k-1
k-1
:: ... :: a :: nil
1
, ... ,a ]
1
schreiben; insbesondere [ ] für nil.
• Selbstverständlich kann man mit Listen von Listen
arbeiten.
• Wie wir in 3.3 sehen werden, unterstützt ML
Typparameter. Z. B. ist nil vom Typ ‘a list ,
wobei ‘a eine Typvariable ist.
Beispiele: (Funktionen auf Listen)
•
Addiere alle Zahlen einer Liste von Typ int list:
-
fun mapplus ( xl: int list ) =
if null(xl) then 0
else hd(xl) + mapplus(tl(xl)) ;
-
mapplus ( [ 1,2,3,4,5,6 ] ) ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
43
2. Prüfen einer Liste von Zahlen auf Sortiertheit:
- fun ist_sortiert ( xl ) =
if null (xl) orelse null ( tl(xl) ) then true
else if hd (xl) <= hd(tl(xl))
then ist_sortiert( tl (xl))
else false ;
3. Zusammenhängen zweier Listen (engl. append):
- fun append ( l1, l2 ) =
if l1 = [ ] then l2
else hd (l1) :: append ( tl(l1), l2 ) ;
4. Umkehren einer Liste:
- fun rev ( xl ) =
if xl = nil then [ ]
else
append ( rev (tl (xl)), [ hd(xl) ] ) ;
5. Zusammenhängen der Elemente einer Liste von Listen:
- fun concat ( xl ) =
if null(xl) then [ ]
else append ( hd(xl) , concat( tl( xl ) ) ) ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
44
6. Wende eine Liste von Funktionen vom Typ
int  int nacheinander auf eine ganze
Zahl an:
- fun seqappl ( xl : (intint) list, i: int ) :
if null( xl ) then i
else seqappl ( tl(xl), (hd(xl) i)) ;
Bemerkungen:
• Rekursive Funktionsdeklaration sind bei Listen
angemessen, weil Listen rekursive Datenstrukturen
sind.
• Mit Pattern Matching lassen sich die obigen
Deklaration noch eleganter fassen (s. unten).
Die Datenstrukturen der Tupel
Wir betrachten zunächst Paare und verallgemeinern
dann auf n-Tupel:
Paare oder 2-Tupel sind die Elemente des
kartesischen Produktes zweier ggf. verschiedener
Mengen oder Typen. Paare gehören zu Produkttypen.
Als Typkonstruktor wird * in Infix-Schreibweise benutzt:
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
45
Typen: Sind t1, t2 ML-Typen, dann bezeichnet
t1 * t2 den Typ der geordneten Paare
aus Elementen von t1 und t2.
Funktionen:
(* Paarbildung *)
(_,_): t1 * t2  t1 * t2
#1
: t1 * t2  t1
#2
: t1 * t2  t2
(* 1. Komponente *)
(* 2. Komponente *)
Konstanten: keine
Dem Typ t1*t2 ist die Menge der Paare bestehend
aus Elementen von t1 und t2 zugeordnet.
Beispiel: (Funktionen auf Paaren)
Transformiere eine Liste von Paaren in ein Paar
von Listen:
fun unzip ( xl:(int*char) list ):
(int list)*(char list) =
if
null( xl )
then ([],[])
else ( (#1(hd(xl))) :: (#1(unzip(tl(xl)))),
(#2(hd(xl))) :: (#2(unzip(tl(xl)))) );
( auch das geht erheblich schöner mit Pattern
Matching )
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
46
ML unterstützt n-Tupel für alle n ≥ 0, n≠ 1:
Für n=0 ist es die triviale Datenstruktur Unit:
Typ: unit
Funktionen: keine
Konstanten: ():  unit
Die Wertemenge zum Typ unit enthält genau
ein Element, genannt Einheit (engl. unity).
Bemerkung:
unit wird oft als Ergebnistyp verwendet, wenn
es keine relevanten Ergebniswerte gibt.
Für n=1 gilt:
1-Tupel vom Typ t werden wie Elemente vom Typ t
betrachtet.
Für n>1 gilt:
n-Tupel sind eine Verallgemeinerung von Paaren.
Klammern werden zur Tupel-Konstruktion verwendet.
#i mit 0<i≤ n selektiert die i-te Komponente.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
47
Beispiel: (Geschachtelte Tupel)
Mit der Tupelbildung lassen sich „baumstrukturierte“
Werte, sogenannte Tupelterme, aufbauen.
So entspricht der Tupelterm:
((8,true), (("Tupel","sind","toll"),"aha"));
dem Baum:
8
true
"Tupel" "sind"
"aha"
"toll"
Beispiel: (Funktionen auf n-Tupeln)
1. Flache ein Paar von Tripeln in ein 6-Tupel aus:
- fun ausflachen ( p: (‘a * ‘b * ‘c) * (‘d * ‘e * ‘f) ) =
( #1(#1(p)), #2(#1(p)), #3(#1(p)),
#1(#2(p)), #2(#2(p)), #3(#2(p)) ) ;
2. Funktion zur Paarbildung:
14.10.08
fun paarung (lk,rk) = (lk,rk) ;
© A. Poetzsch-Heffter, TU Kaiserslautern
48
Stelligkeit von Funktionen:
Formal gesehen, sind alle Funktionen in ML einstellig,
haben also nur ein Argument. Die Klammern um das
Argument kann man weglassen, also f a statt f(a) .
Statt mehrerer Parameter übergibt man ein Tupel
von Parametern. Die Tupelklammern kann man
natürlich nicht weglassen.
Beispiel:
Die Additionsoperation in ML op + nimmt nicht zwei
Argumente sondern ein Tupel von Argumenten.
Statt mehrere Parameter als Tupel zu übergeben,
kann man sie auch „nacheinander“ übergeben.
Z. B. hat die Funktion
fun curriedplus m n
=
m + n
;
den Typ
fn : int  (int  int)
Die Auswertung des Ausdruck curriedplus 7 liefert
also eine Funktion vom Typ (int  int) .
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
49
Zusammenfassung:
• In ML haben alle Funktionen nur eine Parameter!
• Funktionen mit n-Parametern werden in ML
durch einstellige Funktionen ausgedrückt, die ein
n-Tupel als Parameter nehmen!
• Die bisher verwendeten Klammern bei einstelligen
Funktionen sind also überflüssig.
Syntax der Funktionsanwendung in ML:
<Ausdruck> <Ausdruck>
Funktionstyp
zum Funktionstyp passender
Parametertyp
Pattern Matching
Pattern Matching meint in diesem Zusammenhang
die Verwendung von Mustern zur Vereinfachung von
Vereinbarungen/Bezeichnerbindungen.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
50
Beispiel: (Funktionssyntax)
- fun ist_sortiert x =
if null x orelse null (tl x) then true
else if hd x <= hd (tl x)
then ist_sortiert( tl x )
else false ;
- fun append (l1,l2) =
if l1 = [ ] then l2
else hd l1 :: append ( tl l1, l2 ) ;
- fun concat x =
if null x then [ ]
else append ( hd x, concat ( tl x ) ) ;
- fun unzip ( x : (‘a * ‘b) list ): (‘a list) * (‘b list) =
if null x
then ([],[])
else ( #1 (hd x) :: (#1 (unzip (tl x))),
#2 (hd x) :: (#2 (unzip (tl x))) ) ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
51
Begriffsklärung: (Konstruktoren)
Konstruktoren in ML sind bestimmte Funktionen:
- die Funktionen zur Tupelbildung
- die Listenfunktion _::_ (daher der Name cons)
- benutzerdefinierte Konstruktoren zu rekursiven
Datentypen (siehe 3.1.4).
Begriffsklärung: (ML-Muster, ML-Pattern)
Muster (engl. Pattern) in ML sind Ausdrücke gebildet
über Bezeichnern, Konstanten und Konstruktoren.
Alle Bezeichner in einem Muster müssen verschieden
sein.
Ein Muster M mit Bezeichnern b , ..., b passt auf
1
k
einen Wert w (engl. a pattern matches a value w),
wenn es eine Substitution der Bezeichner bi in M
durch Werte v i gibt, in Zeichen M[v1 /b 1, ... , vk /bk ],
so dass
M[v /b , ... , v /b ] = w
1
14.10.08
1
k
k
© A. Poetzsch-Heffter, TU Kaiserslautern
52
Beispiel: (ML-Muster, Passen)
1. (a,b) passt auf (4,5) mit Substitution a=4, b=5.
3. (a,b) passt auf (~47, (true, “dada“)) mit
a = ~47, b = (true, “dada“) .
6. x::xs passt auf 7::8::9::nil mit x = 7 und
xs = 8::9::nil , d.h. xs = [ 8, 9 ] .
9. x1::x2::xs passt auf 7::8::9::nil mit x1 = 7,
x2 = 8, x3 = [ 9 ] .
12. first :: rest passt auf [ “computo“, “ergo“, “sum“ ]
mit first = “computo“, rest = [ “ergo“, “sum“ ]
15. ( (8,x) , (y,"aha") ) pass auf
((8,true), ( ("Tupel","sind","toll"), "aha") )
mit
x = true und y = ("Tupel","sind","toll")
8
14.10.08
x
y
"aha"
© A. Poetzsch-Heffter, TU Kaiserslautern
53
Muster können in ML-Wertvereinbarungen
verwendet werden:
val <Muster> = <Ausdruck> ;
Wenn das Muster auf den Wert des Ausdrucks
passt und σ die zugehörige Substitution ist,
werden die Bezeichner im Muster gemäß σ an
die zugehörige Werte gebunden.
Beispiel: (Wertvereinbarung mit Mustern)
-
val
( a, b )
=
( 4, 5 );
val a = 4 : int
val b = 5 : int
- a+b ;
val it = 9: int
Muster können in ML-Funktionsdeklarationen
verwendet werden:
fun <Bezeichner> <Muster> = <Ausdruck>
...
| <Bezeichner> <Muster> = <Ausdruck> ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
54
Beispiel: (Funktionsdeklaration mit Mustern)
1. Deklaration von mapplus ohne und mit Mustern:
- fun mapplus (xl: int list) =
if null xl
then 0
else (hd xl)+ mapplus (tl xl) ;
- fun mapplus []
= 0
| mapplus (x::xl) = x + mapplus xl ;
2. Einige der obigen Funktionsdeklaration mit
Verwendung von Mustern:
- fun ist_sortiert [ ]
= true
| ist_sortiert (x::nil)
= true
| ist_sortiert (x1::x2::xs) =
if x1 > x2 then false
else ist_sortiert (x2::xs ) ;
- fun append ([],l2)
|
=
l2
append (x::xs,l2) =
x::append (xs,l2) ;
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
55
3. Verwendung geschachtelter Muster:
- fun unzip (xl:(int*char)list):
(int list)*(char list) =
if null xl then ([],[])
else ((#1 (hd xl))::(#1 (unzip (tl xl))),
(#2 (hd xl))::(#2 (unzip (tl xl))));
- fun unzip []
= ([],[])
| unzip ((x,y)::ps) =
(x::(#1 (unzip ps)),
y::(#2 (unzip ps)) );
let- und case-Ausdrücke:
Der Mustermechanismus kann auch innerhalb
von Ausdrücken eingesetzt werden.
1. Syntax des let-Ausdrucks:
let
<Liste von Deklarationen>
in
<Ausdruck>
end
Die aus den Deklarationen resultierenden
Bindungen sind nur im let-Ausdruck gültig.
D.h. sie sind sichtbar im let-Ausdruck, an den
Stellen, an denen sie nicht verdeckt sind.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
56
2. Syntax des case-Ausdrucks:
case <Ausdruck0> of
<Muster1> => <Ausdruck1>
...
| <MusterN> => <AusdruckN>
Bedeutung: Werte Ausdruck0 aus und prüfe der
Reihe nach, ob der resultierende Wert auf eines
der Muster passt. Passt er auf ein Muster, nehme
die entsprechenden Bindungen vor und werte
den zugehörigen Ausdruck aus (die Bindungen
sind nur in dem zugehörigen Ausdruck gültig).
Beispiele: (let-Ausdruck)
- val a = let val b = 7*8 in b*b end;
- val a = let val a = 7*8
in
let val (a,b) = (a,a+1)
in
a*b
end
end;
- fun unzip [] = ([],[])
| unzip ((x,y)::ps) =
let (xs,ys) = unzip ps
in (x::xs,y::ys)
end
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
;
57
Beispiel: (case-Ausdruck)
- fun ist_sortiert xl
case xl of
[]
| (x::nil)
=
=>
true
=>
true
| (x1::x2::xs) =>
if x1 > x2 then false
else ist_sortiert (x2::xs) ;
Bemerkungen:
• Das Verbot von gleichen Bezeichnern in Mustern
hat im Wesentlichen den Grund, dass nicht für alle
Werte/Typen die Gleichheitsoperation definiert ist.
- val mal2
=
fn x => 2 * x ;
- val twotimes =
fn x => x+x ;
- val (a,a)
( mal2 , twotimes )
=
• Offene Frage: Was passiert, wenn keines der
angegebenen Muster passt?
 Abrupte Terminierung.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
58
3.1.4 Benutzerdefinierte Datentypen
Übersicht:
• Vereinbarung von Typbezeichnern
• Deklaration neuer Typen
• Summentypen
• Rekursive Datentypen
Fast alle modernen Spezifikations- und
Programmiersprachen gestatten es dem Benutzer,
„neue“ Typen zu definieren.
Vereinbarung von Typbezeichnern
ML erlaubt es, Bezeichner für Typen zu deklarieren
(vgl. Folie 105). Dabei wird kein neuer Typ definiert,
sondern nur ein Typ an einen Bezeichner gebunden.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
59
Beispiele: (Typvereinbarungen)
type intpaar
= int * int
type charlist = char list
type adresse = string * string* string * int
type telefonbuch =
( adresse * string list ) list
type intNachInt
val
=
int  int
fakultaet : intNachInt
=
;
fac ;
Typvereinbarungen sind nur Abkürzungen.
Zwei unterschiedliche Bezeichner können den
gleichen Typ bezeichnen; z.B.:
- type inttripel = int * int * int;
type inttripel = int * int * int
- type date = int * int * int;
type date = int * int * int
- fun kalenderwoche (d:date): int = ... ;
val kalenderwoche = fn : date -> int
- kalenderwoche (192,45,111111);
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
60
Deklaration neuer Typen
Neue Typen werden in ML mit dem datatype-Konstrukt
definiert, das im Folgenden schrittweise erläutert wird.
Definition eines neuen Typs und Konstruktors:
datatype <Typbezeichner> =
<Konstruktorbezeichner> of <TypAusdruck>
Die obige Datatypdeklaration definiert:
- einen neuen Typ und bindet ihn an <Typbezeichner>
- eine Konstruktorfunktion, die Werte des durch den
Typausdruck beschriebenen Typs in den neuen Typ
einbettet. Die Konstruktorfunktion ist injektiv.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
61
Beispiel: (Definition von Typ und Konstruktor)
datatype person =
Student of string * string * int * int
definiert den neuen Typ person und den Konstruktor
Student: string*string*int*int -> person
Wir definieren die Selektorfunktionen:
- fun vorname (Student(v,n,g,m))
val vorname = fn : person -> string
= v ;
- fun name (Student(v,n,g,m))
val name = fn : person -> string
= n ;
- fun geburtsdatum (Student(v,n,g,m)) = g ;
val geburtsdatum = fn : person -> int
- fun matriknr (Student(v,n,g,m))
val matriknr = fn : person -> int
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
= m ;
62
Jede Datentypdeklaration definiert einen neuen Typ,
d.h. insbesondere:
- Werte vom Argumentbereich des Konstruktors sind
inkompatibel mit dem neuen Typ;
- Werte strukturgleicher benutzerdefinierter Typen
sind inkompatibel.
Beispiele: (Typkompatibilität)
1. Der Typ person ist inkompatibel mit dem Typ
string*string*int*int , insbesondere ist
vorname (“Niels“,“Bohr“,18851007, 221)
nicht typkorrekt.
2. Der Typ person ist inkompatibel mit dem
strukturgleichen Typ adresse
datatype adresse =
WohnAdresse of string * string * int * int
Insbesondere ist
name (WohnAdresse(“Pfaffenbergstraße“,
“Kaiserslautern“, 27, 67663 ) )
nicht typkorrekt.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
63
Bemerkung:
Den Konstruktor kann man sich als eine Markierung
der Werte seines Argumentbereichs vorstellen.
Dabei werden Werte mit unterschiedlicher Markierung
als verschieden betrachtet.
Summentypen
Ein Summentyp stellt die disjunkte Vereinigung der
Elemente anderen Typen zu einem neuen Typ dar.
Die meisten modernen Programmiersprachen
unterstützen die Deklaration von Summentypen.
Summentypen in ML:
Bei einer Datentypdeklaration kann man verschiedene
Alternativen angeben:
datatype <Typbezeichner> =
<Konstruktorbezeichner1> of <TypAusdruck1>
...
| <KonstruktorbezeichnerN> of <TypAusdruckN>
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
64
Beispiele: (Summentypen)
1. Ein anderer Datentyp zur Behandlung von Personen:
datatype person2 =
Student
of string*string*int*int
| Mitarbeiter of string*string*int*int
| Professor
of string*string*int*int*string
2. Eine benutzerdefinierte Datenstruktur für Zahlen:
- datatype number =
Intn
of int
| Realn of real
- fun isInt (Intn m) = true
| isInt (Realn r) = false ;
val isInt = fn : number -> bool
- fun isReal (Intn m) = false
| isReal (Realn r) = true ;
val isReal = fn : number -> bool
- fun neg
|
neg
(Intn m)
=
Intn (~m)
(Realn r) = Realn (~r);
val neg = fn : number -> number
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
65
- fun plus (Intn m,Intn n)
= Intn (m+n)
|
plus (Intn m,Realn r) = Realn((real m)+r)
|
plus (Realn r,Intn m) = Realn(r+(real m))
|
plus (Realn r,Realn q)= Realn(r+q);
val plus = fn : number * number -> number
Begriffsklärung:
Konstruktorfunktionen oder Konstruktoren liefern
Werte des neu definierten Datentyps. Sie können
in Mustern verwendet werden (z.B.: Student, Intn).
Diskriminatorfunktionen oder Diskriminatoren
prüfen, ob der Wert eines benutzerdefinierten
Datentyps zu einer bestimmten Alternative gehört
(Beispiel: isInt).
Selektorfunktionen oder Selektoren liefern
Komponenten von Werten des definierten Datentyps
(z.B.: vorname, name, ...).
Bemerkung:
In funktionalen Sprachen kann man meist auf
Selektorfunktionen verzichten. Man wendet stattdessen Pattern.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
66
Weitere Formen des datatype-Konstrukts:
Das datatype-Konstrukt kann auch verwendet werden,
um Aufzählungstypen zu definieren. Dabei wird in
den Alternativen der Teil beginnend mit dem
Schlüsselwort of weggelassen.
Die Wertemenge eines Aufzählungstyps ist eine
endliche Menge (von Namen).
Beispiele: (Aufzählungstypen)
- datatype
Montag
wochentag
=
| Dienstag | Mittwoch | Donnerstag
| Freitag | Samstag
| Sonntag
- fun istMittwoch Mittwoch =
|
istMittwoch
w
=
;
true
false ;
Oder knapper:
-
fun istMittwoch w
14.10.08
=
(w=Mittwoch) ;
© A. Poetzsch-Heffter, TU Kaiserslautern
67
Beide Formen der Datentypdeklaration können
kombiniert werden; z.B.:
- datatype intBottom
Bottom
| Some of int
=
Bemerkung (vgl. 3.3):
Datentypdeklarationen lassen sich parametrisieren.
Die Typparameter werden dabei vor den vereinbarten
Typbezeichner gestellt:
- datatype ‘a bottom =
Bottom
|
Some of ‘a
ML sieht dafür standardmäßig den folgenden Typ vor:
- datatype ‘a option = NONE | SOME of ‘a
Zur Erweiterung von Summentypen
Im Gegensatz zur objektorientierten Programmierung
(vgl. Kapitel 5) lassen sich die Summentypen in
der funktionalen Programmierung meist nicht
erweitern, d.h. einem definierten Datentyp können
im Nachhinein keine Alternativen hinzugefügt werden.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
68
Beispiel: (Erweiterung von Summentypen)
Den Datentyp person2:
datatype person2 =
Student
of string*string*int*int
| Mitarbeiter of string*string*int*int
| Professor
of string*string*int*int*string
hätte man aus person durch Hinzufügen der
Alternativen für Mitarbeiter und Professoren gewinnen
können.
Bemerkung:
Die Möglichkeit, einen Datentypen T nachträglich
erweitern zu können, bringt zwei Vorteile für die
Wiederverwendung mit sich:
1. Der Programmieraufwand wird reduziert.
2. Programme, die für T entwickelt wurden, können
teilweise weiter benutzt werden.
In ML müssen Erweiterungen im Allg. durch neue
Summentypen realisiert werden.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
69
Beispiel:
Ein Datentyp dt :
datatype
dt
=
Constr1
of
t1
|
Constr2
of
t2
|
Constr3
of
t3
|
Constr4
of
t4
soll eine fünfte Alternative bekommen. Statt alle
existierenden Programme auf fünf Alternativen
umzustellen, kann man mit dtneu arbeiten:
datatype
dtneu =
|
Constr0
of
dt
Constr5
of
t5
Nachteil: Die Programme müssen auf dtneu
angepasst werden.
Bemerkung:
Der Datentyp exn ist in ML erweiterbar. Jede
Ausnahme-Vereinbarung fügt eine Alternative hinzu.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
70
Rekursive Datentypen
Von großer Bedeutung in der Programmierung sind
rekursive Datentypen.
Definition: (rekursive Datentypdeklaration)
Eine Datentypdeklaration heißt direkt rekursiv,
wenn der definierte Typ in einer der Alternativen
der Datentypdeklaration vorkommt.
Wie bei Funktionen gibt es auch verschränkt
rekursive Datentypdeklarationen.
Eine Datentypdeklaration heißt rekursiv, wenn
sie direkt rekursiv ist oder Element einer Menge
verschränkt rekursiver Datentypdeklarationen ist.
Begriffsklärung: (rekursiver Datentyp)
Ein Datentyp heißt rekursiv, wenn er mit einer
rekursiven Datentypdeklaration definiert wurde.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
71
Beispiele: (Listendatentypen)
1. Ein Datentyp für Integer-Listen:
- datatype
intlist
=
nil
|
Cons
of
int
*
intlist
;
2. Ein Datentyp für homogene Listen mit Elementen
von beliebigem Typ:
- datatype
‘a list
=
nil
|
Cons
of
‘a
*
‘a list
Vorsicht:
Diese Deklaration verdeckt den vordefinierten
Datentyp ‘a list.
3. Der vordefinierte Datentyp für homogene Listen
mit Elementen von beliebigem Typ:
-
datatype
‘a list
=
of
*
nil
|
14.10.08
::
‘a
‘a list
© A. Poetzsch-Heffter, TU Kaiserslautern
72
Baumartige Datenstrukturen:
Ottmann, Widmayer:
„Bäume gehören zu den wichtigsten in der
Informatik auftretenden Datenstrukturen“.
A
B
C
D
E
F
G
H
Begriffsklärung: (zu Bäumen)
• In einem endlich verzweigten Baum hat jeder
Knoten endlich viele Kinder.
• Üblicherweise sagt man, die Kinder sind von
links nach rechts geordnet.
•. Einen Knoten ohne Kinder nennt man Blatt, einen
Knoten mit Kindern einen inneren Knoten oder
Zweig.
• Den Knoten ohne Elter nennt man Wurzel.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
73
• Ein Baum heißt markiert, wenn jeder Knoten k
eine Markierung m(k) besitzt.
• In einem Binärbaum ist jeder Knoten entweder
ein Blatt, oder er hat genau zwei Kinder.
• Zu jedem Knoten gehört ein Unterbaum.
Datentyp für markierte Binärbäume:
- datatype
intbbaum
Blatt
|
of
Zweig of
=
int
- val einbaum =
int * intbbaum * intbbaum
;
Zweig (7,
Zweig(3,(Blatt 2),(Blatt 4)), (Blatt 5));
- fun
|
m (Blatt n)
=
n
m (Zweig (n,lk,rk)) =
n
;
Ein mit ganzen Zahlen markierter Binärbaum
heißt sortiert, wenn für alle Knoten k gilt:
- Die Markierungen der linken Nachkommen von k
sind kleiner als m(k).
- Die Markierungen der rechten Nachkommen von k
sind größer als m(k).
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
74
Aufgabe:
Prüfe, ob ein Baum vom Typ intbbaum sortiert ist.
Idee:
Berechne zu jedem Unterbaum die minimale und
maximale Markierung und prüfe rekursiv die
Sortiertheitseigenschaft für alle Knoten/Unterbäume.
fun maxmark (Blatt n) = n
| maxmark (Zweig (n,l,r)) =
Int.max(n,Int.max(maxmark l, maxmark r))
fun minmark (Blatt n) = n
| minmark (Zweig (n,l,r)) =
Int.min(n,Int.min(minmark l, minmark r))
fun istsortiert (Blatt n) = true
| istsortiert (Zweig (n,l,r)) =
istsortiert l andalso istsortiert r
andalso
(maxmark l)<n andalso n<(minmark r)
istsortiert einbaum
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
75
Wenig effiziente Lösung!! Besser ist es, die
Berechnung von Minima und Maxima mit der
Sortiertheitsprüfung zu verschränken.
Idee:
Entwickle eine Einbettung, die für sortierte
Bäume auch Minimum und Maximum liefert.
fun istsorteinbett (Blatt n) = (true,n,n)
|
istsorteinbett (Zweig (n,l,r)) =
let val (lerg,lmn,lmx) = istsorteinbett l
val (rerg,rmn,rmx) = istsorteinbett r
in
( lerg andalso rerg andalso
lmx<n
andalso
n<rmn,
lmn,
rmx )
end ;
fun istsortiert b = #1 (istsorteinbett b)
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
76
Weitere Eigenschaften von Bäumen:
Begriffsklärung: (zu Bäumen)
Der leere Baum ist ein Baum ohne Knoten.
Die Höhe eines Baumes ist der maximale Abstand
eines Blattes von der Wurzel.
Ein Baum, der nur aus einem Blatt besteht, hat die
Höhe 1.
Der leere Baum hat die Höhe 0.
Die Tiefe eines Knotens ist sein Abstand zur Wurzel.
Die Knoten gleicher Tiefe t nennt man das Niveau t.
Die Größe eines Baums ist die Anzahl seiner Knoten.
Datentyp möglicherweise leere Binärbäume:
- datatype
imlbbaum
=
Leer
|
14.10.08
Knoten of
int * imlbbaum * imlbbaum
© A. Poetzsch-Heffter, TU Kaiserslautern
;
77
Bäume mit variabler Kinderzahl:
Bäume mit variabler Kinderzahl lassen sich realisieren:
- durch mehrere Alternativen für Zweige (begrenzte
Anzahl von Kindern)
- durch Listen von Unterbäumen:
datatype vibaum = Kn of int * (vibaum list)
Beachte:
Der Rekursionsanfang ergibt sich durch Knoten mit
leerer Unterbaumliste.
fun zaehleKnViBaum (Kn (_,xs)) =
1 + (zaehleKnViBaumLst xs)
and zaehleKnViBaumList [] = 0
| zaehleKnViBaumLst (x::xs) =
(zaehleKnViBaum x)
+ (zaehleKnViBaumLst xs)
Bäume mit variabler Kinderzahl lassen sich z.B. zur
Repräsentation von Syntaxbäumen verwenden, indem
das Terminal- bzw. Nichtterminalsymbol als Markierung
verwendet wird.
Besser ist es allerdings, die Information über die
Symbole mittels Konstruktoren auszudrücken.
--> Gewinn von Typsicherheit (vgl. nächstes Beispiel).
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
78
Verschränkte Datentypdeklarationen:
Genau wie Funktionsdeklarationen werden
verschränkt rekursive Datentypdeklarationen mit
dem Schlüsselwort „and“ verbunden:
datatype <Typbezeichner1> =
<Konstruktorbezeichner1_1> of <TypAusdruck1_1>
...
| <Konstruktorbezeichner1_N> of <TypAusdruck1_N>
and
...
and <TypbezeichnerM> =
<KonstruktorbezeichnerM_1> of <TypAusdruckM_1>
...
| <KonstruktorbezeichnerM_K> of <TypAusdruckM_K>
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
79
Beispiel: (abstrakte Syntaxbäume)
Der abstrakte Syntaxbaum eines Programms
repräsentiert die Programmstruktur unter
Verzicht auf Schlüsselworte und Trennzeichen.
Rekursive Datentypen eignen sich sehr gut zur
Beschreibung von abstrakter Syntax. Als Beispiel
betrachten wir die abstrakte Syntax von Femto:
datatype
programm = Prog
and
wertdekl = Dekl
and
ausdruck =
Bzn
|
Zahl
|
Add
|
Mul
of wertdekl list * ausdruck
of string * ausdruck
of
of
of
of
string
int
ausdruck * ausdruck
ausdruck * ausdruck
Das Programm
val a = 73;
( a + 12 ) ;
Wird durch folgenden Baum repräsentiert:
Prog ( [Dekl (“a“,Zahl 73)],
Add (Bzn “a“,Zahl 12) )
Die Baumrepräsentation eignet sich besser als die
Zeichenreihenrepräsentation zur weiteren Verarbeitung von Programmen.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
80
Beispiel: (verschränkte Datentypen)
Bei abstrakten Syntaxbäumen wird häufig
verschränkte Rekursion der Datentypen benötigt.
Als Beispiel betrachten wir die abstrakte Syntax
einer Erweiterung von Femto um let-Ausdrücke:
datatype
programm = Prog
and
wertdekl = Dekl
and
ausdruck =
Bzn
|
Zahl
|
Add
|
Mul
|
Let
of wertdekl list * ausdruck
of string * ausdruck
of
of
of
of
of
string
int
ausdruck * ausdruck
ausdruck * ausdruck
wertdekl * ausdruck
Es ergibt sich einen Verschränkung zwischen der
Definition von
wertdekl ,
die den Typ ausdruck benutzt, und der Definition von
ausdruck ,
die den Typ wertdekl benutzt.
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
81
Unendliche Datenobjekte:
Zu einer nicht-leeren Liste kann man sich das erste
Element und eine Liste als Rest geben lassen kann.
Hat die endliche Liste xl die Länge n>0, dann hat
(tl xl) die Länge n-1.
Eine unendliche Liste besitzt keine natürlichzahlige
Länge und wird durch Anwendung von tl nicht kürzer.
Begriffklärung: (Strom)
Potenziell unendliche Listen werde meist als Ströme
bezeichnet. Typische Operationen:
- lesen des ersten Elements (head)
- entfernen des ersten Elements (tail)
- prüfen, ob noch Elemente im Strom vorhanden sind.
Beispiel: (Stromtyp)
In ML lassen sich beispielsweise unendliche Listen von
Strings mit folgender Datenstruktur realisieren:
datatype stringstream =
Nil
| Cons of string * (unit -> stringstream)
fun hd (Cons ( x, xf ))
| hd Nil
fun
x
raise Empty
tl (Cons ( x, xf ))
|
14.10.08
=
=
tl
Nil
=
=
xf ()
raise Empty
© A. Poetzsch-Heffter, TU Kaiserslautern
82
Beispiel:
(unendliche Liste)
Liste aller (positiven) Dualzahlen dargestellt als
Zeichenreichen:
- fun incr
| incr
| incr
val incr =
(#"O"::xs) =
(#"L"::xs) =
[ ]
=
fn : char list
#"L"::xs
#"O":: incr xs
[ #"L" ]
;
-> char list
- fun incrstr s =
implode (rev (incr (rev (explode s))));
val incrstr = fn : string -> string
- fun dualz_ab s =
Cons (s, fn x=> dualz_ab (incrstr s));
val dualz_ab = fn: string -> stringstream
- val dl = dualz_ab "O" ;
val dl = Cons ("O",fn) : stringstream
- hd dl;
val it = "O" : string
- val dl = tl dl;
val dl = Cons ("L",fn) : stringstream
- val dl = tl dl;
val dl = Cons ("LO",fn) : stringstream
14.10.08
© A. Poetzsch-Heffter, TU Kaiserslautern
83
Herunterladen