Funktionales Programmieren

Werbung
3. Funktionales Programmieren
3.0
Kapitel 3
Funktionales
Programmieren
©Arnd Poetzsch-Heffter
TU Kaiserslautern
157
3. Funktionales Programmieren
3.0
Übersicht
3. Funktionales Programmieren
Grundkonzepte funktionaler Programmierung
Zentrale Begriffe und Einführung
Rekursive Funktionen
Listen und Tupel
Benutzerdefinierte Datentypen
Ein- und Ausgabe
Module
Zusammenfassung von 3.1
Algorithmen auf Listen und Bäumen
Sortieren
Suchen
Polymorphie und Funktionen höherer Ordnung
Typisierung
Funktionen höherer Ordnung
Semantik, Testen und Verifikation
©Arnd Poetzsch-Heffter
TU Kaiserslautern
158
3. Funktionales Programmieren
3.0
Übersicht (2)
Zur Semantik funktionaler Programme
Testen und Verifikation
©Arnd Poetzsch-Heffter
TU Kaiserslautern
159
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Abschnitt 3.1
Grundkonzepte funktionaler Programmierung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
160
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.1
Zentrale Begriffe und Einführung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
161
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Zentrale Begriffe und Einführung
Funktionale Programmierung im Überblick:
• Funktionales Programm:
I partielle Funktionen von Eingabe- auf Ausgabedaten
I besteht aus Deklarationen von (Daten-)Typen, Funktionen und
(Daten-)Strukturen
I Rekursion ist eines der zentralen Sprachkonzepte
I in Reinform: kein Zustandskonzept, keine veränderlichen Variablen,
keine Schleifen, keine Zeiger
• Ausführung eines funktionalen Programms: Anwendung einer
Funktion auf Eingabedaten
• Zusätzliche Programmierkonstrukte, um die Kommunikation mit
der Umgebung zu beschreiben
©Arnd Poetzsch-Heffter
TU Kaiserslautern
162
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Definition: (partielle Funktion)
Ein Funktion heißt partiell, wenn sie nur auf einer Untermenge ihres
Argumentbereichs definiert ist.
Andernfalls heißt sie total.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
163
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (partielle Funktion)
1. Bezeichne Nat die Menge der natürlichen Zahlen (0 und größer)
und sei fact :: Nat → Nat wie folgt definiert:
(
fact(n) =
1
, für n = 0
fact(n − 1) ∗ n , für n > 0
Dann ist fact wohldefiniert und total.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
164
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (partielle Funktion) (2)
2. Bezeichne Float die Menge der auf dem Rechner darstellbaren
Gleitkommazahlen. Dann ist die Funktion
sqrt :: Float → Float ,
die die Quadratwurzel (engl. square root) berechnet, partiell.
3. Bezeichne String die Menge der Zeichenreihen. Dann ist die
Funktion abschneide2, die die ersten beiden Zeichen einer
Zeichenreihe abschneidet partiell (warum?)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
165
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Definition: (Funktionsanwendung, -auswertung,
Terminierung, Nichtterminierung)
Bezeichne f eine Funktion, a ein zulässiges Argument von f .
Die Anwendung von f auf a nennen wir eine Funktionsanwendung
(engl. function application); meist 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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
166
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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 wie folgt definiert:
(
n − 10
, für n > 100
m(n) =
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
„jede“partielle Funktion total machen. Üblicherweise bezeichnet
man das Element für „undefiniert “ mit ⊥ (engl. „bottom “).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
167
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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.
Also sind auch Listen von Funktionen Werte.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
168
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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.
• Wie für abstrakte Objekte oder Begriffe typisch, besitzen Werte
I
I
I
I
keinen Ort,
keine Lebensdauer,
keinen veränderbaren Zustand,
kein Verhalten.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
169
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Typ, engl. type)
Ein Typ (engl. type) fasst Werte zusammen, auf denen die gleichen
Funktionsanwendungen zulässig sind.
Typisierte Sprachen besitzen ein Typsystem, das für jeden Wert
festlegt, welchen Typ er hat.
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 )
©Arnd Poetzsch-Heffter
TU Kaiserslautern
170
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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 solche Gebilde Algebren oder einfach
nur Strukturen.
In der Informatik spricht man auch von einer Rechenstruktur.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
171
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Definition: (Signatur einer Datenstruktur)
Die Signatur (T, F) einer Datenstruktur 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,
definiert ist. n gibt die Stelligkeit von f an.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
172
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Definition: (Datenstruktur mit Signatur)
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 0 s Funktionstyp gehören.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
173
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Wir betrachten zunächst die Basisdatenstrukturen, wie man sie in
jeder Programmier-, Spezifikations- und 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 Haskell. Später lernen wir auch die Basisdatenstrukturen
von Java kennen.
• Wir benutzen auch Operatorsymbole wie + und * um Funktionen
zu bezeichnen.
• Nullstellige Funktionen nennen wir Konstanten.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
174
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der booleschen Werte:
Typ:
Bool
Funktionen:
(==)
(/=)
(&&)
(||)
not
::
::
::
::
::
Bool → Bool → Bool
Bool → Bool → Bool
Bool → Bool → Bool
Bool → Bool → Bool
Bool → Bool
Konstanten:
True :: Bool
False :: Bool
©Arnd Poetzsch-Heffter
TU Kaiserslautern
175
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der booleschen Werte: (2)
Dem Typbezeichner Bool ist die Wertemenge {True, False} zugeordnet.
(==)
(/=)
(&&)
(||)
not
bezeichnet die Gleichheit auf Wahrheitswerten
bezeichnet die Ungleichheit auf Wahrheitsw.
bezeichnet das logische Und
bezeichnet das logische Oder
bezeichnet die logische Negation
True
False
bezeichnet den Wert True
bezeichnet den Wert False
©Arnd Poetzsch-Heffter
TU Kaiserslautern
176
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Operatorsymbole werden meist mit Infix-Notation verwendet:
34 + 777 , True || False , True && False == True
• Ist ein Operatorsymbol, kann man () in Haskell wie einen
Funktionsbezeichner verwenden:
(+) 34 777 , (||) True False ,
(==) ((&&) True False) True
• Ist f ein (mindestens) zweistelliger Funktionsbezeichner, kann
man `f ` in Haskell mit Infix-Notation verwenden:
34 `div` 777
• Im Folgenden unterscheiden wir nur noch dann zwischen
Funktionsbezeichner und bezeichneter Funktion, wenn dies aus
Gründen der Klarheit nötig ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
177
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der ganzen Zahlen:
Die Datenstruktur der ganzen Zahlen erweitert die Datenstruktur der
booleschen Werte, d.h. sie umfasst den Typ Bool und die darauf
definierten Funktionen. Zusätzlich enthält sie u. a.:
Typ:
Integer
Funktionen:
(==), (/=)
(<), (<=), (>), (>=)
(+), (*), (-)
div, mod
negate, signum, abs
©Arnd Poetzsch-Heffter
::
::
::
::
::
Integer → Integer → Bool
Integer → Integer → Bool
Integer → Integer → Integer
Integer → Integer → Integer
Integer → Integer
TU Kaiserslautern
178
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der ganzen Zahlen: (2)
Konstanten:
– in Dezimaldarstellung: 0, 127, -23
– in Hexadezimaldarstellung: 0x0, 0x7F, −0x17
– in Oktaldarstellung: 0o0, 0o177, −0o27
Dem Typbezeichner Integer ist die Menge der ganzen Zahlen als
Wertemenge zugeordnet.
Die Funktionen der Datenstruktur bezeichnen die üblichen Funktionen
auf den ganzen Zahlen; div bezeichent die ganzzahlige Division, mod
liefert den Rest der ganzzahligen Division.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
179
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der beschränkten ganzen Zahlen:
Die Datenstruktur der beschränkten ganzen Zahlen erweitert die
Datenstruktur der booleschen Werte. Zusätzlich enthält sie u. a.:
Typ:
Int
Funktionen:
(==), (/=)
(<), (<=), (>), (>=)
(+), (*), (-)
div, mod
negate, signum, abs
::
::
::
::
::
Int → Int → Bool
Int → Int → Bool
Int → Int → Int
Int → Int → Int
Int → Int
Konstanten:
minBound, maxBound :: Int
– in Dezimaldarstellung: 0, 127, -23
– in Hexadezimaldarstellung: 0x0, 0x7F, -0x17
– in Oktaldarstellung: 0o0, 0o177, -0o27
©Arnd Poetzsch-Heffter
TU Kaiserslautern
180
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der beschränkten ganzen Zahlen:
(2)
Dem Typbezeichner Int ist eine rechnerabhängige Wertemenge
zugeordnet, die mindestens die ganzen Zahlen von −229 bis 229 − 1
enthalten muss.
Innerhalb der Wertemenge sind die Funktionen der Datenstruktur der
beschränkten ganzen Zahlen verlaufsgleich mit den Funktionen auf
den ganzen Zahlen.
Außerhalb der Wertemenge ist ihr Verhalten nicht definiert.
Insbesondere können (+), (∗), abs, negate partiell sein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
181
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung:
Wenn unterschiedliche Funktionen oder andere Programmelemente
den gleichen Bezeichner haben, spricht man vom Überladen des
Bezeichners (engl. Overloading).
Beispiel: (Überladung von Bezeichnern)
Wie in den obigen Datenstrukturen gezeigt, können
Funktionsbezeichner und Operatorbezeichner in Haskell überladen
werden, d.h. in Abhängigkeit vom Typ ihrer Argumente bezeichnen sie
unterschiedliche Funktionen.
Beispiele: negate, (==), (+)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
182
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Gleitkommazahlen:
Die Datenstruktur der Gleitkommazahlen erweitert die Datenstruktur
der ganzen Zahlen und bietet u. a.:
Typ:
Float
Funktionen:
(==), (/=)
(<), (<=), (>), (>=)
(+), (*), (-), (/)
negate, signum, abs
fromInteger
truncate, round
ceiling, floor
exp, log, sqrt
(**), logBase
sin, cos, tan
©Arnd Poetzsch-Heffter
::
::
::
::
::
::
::
::
::
::
Float → Float → Bool
Float → Float → Bool
Float → Float → Float
Float → Float
Integer → Float
Float → Integer
Float → Integer
Float → Float
Float → Float → Float
Float → Float
TU Kaiserslautern
183
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Gleitkommazahlen: (2)
Konstanten:
pi :: Float
– mit Dezimalpunkt: 0.0, 1000.0, 128.9, -2.897
– mit Exponenten: 0e0, 1e3, 1289e-1, -2897e-3
Dem Typbezeichner Float ist in Haskell eine rechnerabhängige
Wertemenge zugeordnet.
Entsprechendes gilt für die präzise Bedeutung der Funktionen und
Konstanten.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
184
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkung:
• Die ganzen Zahlen sind in der Programmierung keine Teilmenge
der reellen Zahlen!
• In Haskell gibt es weitere Zahlentypen (number types):
I Double (vordefiniert): doppelt präzise Gleitkommazahlen
I Rational (definiert in Standardbibliothek): rationale Zahlen
©Arnd Poetzsch-Heffter
TU Kaiserslautern
185
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Zeichen:
Die Datenstruktur der Zeichen (engl. character) erweitert die
Datenstruktur der beschränkten ganzen Zahlen. Zusätzlich enthält sie
u. a.:
Typ:
Char
Funktionen:
(==), (/=)
:: Char → Char → Bool
(<), (<=), (>), (>=) :: Char → Char → Bool
succ, pred
:: Char → Char
toEnum
:: Int → Char
fromEnum
:: Char → Int
©Arnd Poetzsch-Heffter
TU Kaiserslautern
186
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Zeichen: (2)
Konstanten:
– in Zeichendarstellung: 'A', 'a', '0', 'ß', '"'
– spezielle Zeichen: '\'', '\n', '\t', '\b', '\\'
– in numerischer Darstellung: '\65', '\x41', '\o101'
minBound, maxBound :: Char
Dem Typbezeichner Char ist die Menge der Unicode-Zeichen
zugeordnet. Jedes Unicode-Zeichen besitzt eine Nummer im Bereich
von 0 bis 1.114.111 .
Die Vergleichsoperationen stützen sich auf die Nummerierung.
Die Funktionen succ bzw. pred liefern das Nachfolger- bzw.
Vorgängerzeichen entsprechend der Nummerierung.
Die Funktionen fromEnum bzw. toEnum liefern die Nummer eines
Zeichens bzw. das Zeichen zu einer Nummer.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
187
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Zeichenreihen:
Zeichenreihen sind in Haskell als Listen von Zeichen realisiert und
erweitern die Datenstruktur der Zeichen. Alle auf Listen verfügbaren
Funktionen (siehe Datenstruktur der Listen) können für Zeichenreihen
verwendet werden, insbesondere:
Typ:
String oder [Char]
Funktionen:
(==), (/=)
:: String → String → Bool
(<), (<=), (>), (>=) :: String → String → Bool
head
:: String → Char
tail
:: String → String
length
:: String → Int
(++)
:: String → String → String
©Arnd Poetzsch-Heffter
TU Kaiserslautern
188
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Zeichenreihen: (2)
Konstanten:
– Zeichenreihendarstellung in doppelten Hochkommas:
"Ich bin ein String!!"
"Ich \098\105\110 ein String!!"
""
(die leere Zeichenreihe)
"Mein Lehrer sagt: \"Nehme die Dinge genau!\""
"String vor Zeilenumbruch \nNach Zeilenumbruch"
– Zeichenreihendarstellung als Liste von Zeichen:
[ 'H', 'a', 's', 'k', 'e', 'l', 'l']
Dem Typbezeichner String ist die Menge der Zeichenreihen/Listen
über der Menge der Zeichen zugeordnet.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
189
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Zeichenreihen: (3)
Den Vergleichsoperationen liegt die lexikographische Ordnung
zugrunde, wobei die Ordnung auf den Zeichen auf deren
Nummerierung basiert (siehe Datenstruktur Char).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
190
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkung:
• Es wird unterschieden zwischen Zeichen und Zeichenreihen der
Länge 1.
• Jede Programmier-, Modellierungs- und Spezifikationssprache
besitzt Basisdatenstrukturen. Die Details variieren aber teilweise
deutlich.
• Wenn Basisdatenstrukturen implementierungs- oder
rechnerabhängig sind, entstehen Portabilitätsprobleme.
• Der Trend bei den Basisdatenstrukturen geht zur
Standardisierung.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
191
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Aufbau funktionaler Programme
Im Kern, d.h. wenn man die Modularisierungskonstrukte nicht
betrachtet, bestehen funktionale Programme aus:
• der Beschreibung von Werten:
I
z.B. (7+23), 30
• Vereinbarung von Bezeichnern für Werte (einschließlich
Funktionen):
I
x = 7;
• der Definitionen von Typen:
I type String = [Char]
I data MyType = . . .
©Arnd Poetzsch-Heffter
TU Kaiserslautern
192
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
193
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beschreibung von Werten: (2)
• durch geschachtelte Anwendung von Funktionen:
45.67 + 6857 * ( -9)
floor ( -3.4) * truncate ( -3.4)
toEnum ((( fromEnum (last("Urin"++" stinkt ")))+2))::Char
• durch Verwendung des bedingten Ausdrucks (engl. conditional
expression):
if <boolAusdruck > then <Ausdruck >
else <Ausdruck >
©Arnd Poetzsch-Heffter
TU Kaiserslautern
194
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Ausdruck, expression)
Ausdrücke sind das Sprachmittel zur Beschreibung von Werten. Ein
Ausdruck (engl. expression) in Haskell ist
• eine Konstante,
• ein Bezeichner (Variable, Name),
• die Anwendung einer Funktion auf einen Ausdruck,
• ein bedingter Ausdruck gebildet
• oder ist mit Sprachmitteln aufgebaut, die erst später behandelt
werden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
195
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Ausdruck, expression) (2)
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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
196
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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 || True
False && True || True
©Arnd Poetzsch-Heffter
TU Kaiserslautern
197
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Präzedenzregeln: (2)
Präzedenzregeln legen fest, wie Ausdrücke zu strukturieren sind:
• Am stärksten binden Funktionsanwendungen in Präfixform.
• Regeln für Infix-Operatoren:
infixl 7 ∗, /, div, mod
infixl 6 +, −
infix 4 ==, / =, <, >, <=, >=
infixr 3 &&
infixr 2 ||
Je höher die Präzedenzzahl, desto stärker binden die
Operationen.
• Mit “infixl”/“infixr” gelistete Operatoren sind links-/rechtsassoziativ,
d.h. sie werden von links/rechts her geklammert.
• Mit “infix” gelistete Operatoren müssen geklammert werden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
198
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Deklaration und Bezeichnerbindung:
Bisher haben wir Ausdrücke formuliert, die sich auf die vordefinierten
Funktions- und Konstantenbezeichner von Haskell 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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
199
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Vereinbarung, Deklaration, Bindung)
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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
200
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkung:
• Die verschiedenen Arten an Programmelementen, die in
Deklarationen vorkommen können, hängen von der
Programmiersprache ab.
• In Haskell sind es im Wesentlichen:
1. Bezeichnervereinbarungen (nur zusammen mit
Wert-/Funktionsvereinbarung)
2. Wertvereinbarungen
3. Vereinbarungen (rekursiver) Funktionen
4. Vereinbarungen benutzerdeklarierter Typen
• 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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
201
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Wertvereinbarungen:
• Wertvereinbarungen haben (u.a.) die Form:
<Bezeichner >
=
<Ausdruck > ;
• Wertvereinbarungen kann man eine Bezeichnervereinbarung
voranstellen, um den Typ des Bezeichners zu deklarieren:
<Bezeichner > :: <Typ > ;
<Bezeichner > = <Ausdruck > ;
Der Typ des Ausdrucks muss gleich dem vereinbarten Typ sein.
• Der rechtsseitige Ausdruck darf nur sichtbare Bezeichner
enthalten.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
202
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Wertvereinbarungen)
b = 56 ;
a::Int
a = 7
sieben
sieben
flag
dkv
:: Float ;
= 7.0 ;
= floor sieben == truncate (- sieben )
= " Deutscher Komiker Verein e.v."
©Arnd Poetzsch-Heffter
TU Kaiserslautern
203
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Wertvereinbarungen) (2)
Einzeilige Vereinbarungen im Interpreter ghci:
let b = 56 ;
Mehrzeilige Vereinbarungen im Interpreter ghci:
:{
let {
a::Int
a = 7
sieben
sieben
flag
dkv
}
:}
©Arnd Poetzsch-Heffter
;
;
:: Float ;
= 7.0 ;
= floor sieben == truncate (- sieben ) ;
= " Deutscher Komiker Verein e.v."
TU Kaiserslautern
204
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionsvereinbarungen:
Zwei Probleme:
1. Bisher haben wir 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. Erlaube selbstbezügliche Deklarationen (Haskells Lösung) oder
benutze spezielle Syntax für rekursive Funktionsdeklarationen.
Genaueres dazu in Unterabschnitt 3.1.2 (Folien 214ff).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
205
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionsvereinbarungen)
myDivision :: Integer -> Integer -> Interger
myDivision = div
fac :: Integer -> Integer
-- Argument n muss >= 0 sein
fac n = if n==0 then 1 else n * fac (n -1)
plus2 :: Integer -> Integer
plus2 = (+) 2
©Arnd Poetzsch-Heffter
TU Kaiserslautern
206
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Typvereinbarungen:
Zwei Probleme:
1. Bisher haben wir 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 (z.B. Int -> Int).
Zu 2. Benutze spezielle Syntax für rekursive Typdeklarationen.
Genaueres dazu in Unterabschnitt 3.1.4. (Folien 269ff).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
207
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Typvereinbarungen)
type IntPaar
= (Int ,Int) ;
type CharList
= [Char] ;
type Telefonbuch =
[(( String ,String ,String ,Int) ,[ String ])] ;
type IntegerNachInteger
=
Integer -> Integer ;
fakultaet :: IntegerNachInteger ;
-- Argument muss >= 0 sein
fakultaet = fac ;
©Arnd Poetzsch-Heffter
TU Kaiserslautern
208
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
209
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Programmiersprachen stellen üblicherweise eine
Standard-Umgebung bereit mit den vordefinierten
Programmelementen (Werten, Funktionen, Typen, etc.). In Haskell
ist die Standard-Umgebung durch das Modul Prelude definiert.
• Eine Bezeichnerumgebung wird häufig als Liste von Bindungen
modelliert (vgl. Folie 200).
• Jede Datenstruktur und jedes Modul definiert eine
Bezeichnerumgebung.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
210
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Programm)
Ein Programm besteht in der Regel aus
• einer Menge von Deklarationen und
• einer (durch einen besonderen Namen) ausgezeichneten Funktion
bzw. Prozedur (oder ähnlichem Konstrukt), die angibt, wie die
Auswertung bzw. Ausführung des Programms zu starten ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
211
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (funktionales Programm)
import System .IO
fac :: Integer -> Integer
-- Argument n muss >= 0 sein
fac n = if n==0 then 1 else n * fac (n -1)
main = do {
hSetBuffering stdout NoBuffering ;
putStr " Eingabe x (x>=0): ";
a <- readLn ;
putStr " Ergebnis (fac x): ";
print (fac a);
}
©Arnd Poetzsch-Heffter
TU Kaiserslautern
212
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.2
Rekursive Funktionen
©Arnd Poetzsch-Heffter
TU Kaiserslautern
213
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Funktionsabstraktion
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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
214
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: Funktionsabstraktion
1. Quadratfunktion:
Ausdruck:
Abstraktion:
Haskell-Notation:
x ∗x
λx.(x ∗ x)
\ x -> (x * x)
Vereinbarung eines Bezeichners für die Funktion:
quadrat = \ x -> (x * x)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
215
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: Funktionsabstraktion (2)
2. Volumenberechnung eines Kegelstumpfes:
Formel:
Sei h die Höhe, rk , rg die Radien; dann ergibt sich das Volumen v zu
π∗h
∗ (rk 2 + rk ∗ rg + rg 2 )
3
Haskell-Ausdruck für die rechte Seite:
v=
(pi * h) / 3.0 * ( rk **2 + rk*rg + rg **2 )
Abstraktion in Haskell-Syntax und Vereinbarung von v:
v = \ h rk rg -> (pi*h)/3.0 * (rk **2+ rk*rg+rg **2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
216
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: Funktionsabstraktion (3)
3. Abstraktion über Funktionsbezeichner:
Ausdruck:
Abstraktion:
f (f x)
\ f x -> f (f x)
Mit Bezeichnervereinbarung:
twice = \ f x -> f (f x)
erg
= ( twice sqrt) 3.0
Äquivalente Vereinbarung:
twice2 = \ f -> \ x -> f (f x)
erg
= ( twice sqrt) 3.0
©Arnd Poetzsch-Heffter
TU Kaiserslautern
217
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionsdeklaration
In Haskell gibt es unterschiedliche syntaktische Formen für die
Funktionsdeklaration:
1. mittels direkter Wertvereinbarung:
<Funktionsbez >
=
<Ausdruck von Funktionstyp >
Beispiel:
fib = \ n -> if
n == 0 then 0
else if n == 1 then 1
else fib (n -1) + fib (n -2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
218
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionsdeklaration (2)
2. mittels einem oder mehreren formalen Parametern:
<Funktionsbez > <Parameterbez1 > ...
=
<Ausdruck >
Beispiel:
fib n = if
n == 0 then 0
else if n == 1 then 1
else fib (n -1) + fib (n -2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
219
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionsdeklaration (3)
3. mittels formalen Parametern und Fallunterscheidung über
Wächtern:
<Funktionsbez > <Parameterbez1 > ...
| <boolscher Ausdruck > = <Ausdruck >
...
| <boolscher Ausdruck > = <Ausdruck >
Die boolschen Ausdrücke in der Deklaration heißen Wächter,
engl. guards.
Beispiel:
fib
|
|
|
n
n == 0
= 0
n == 1
= 1
otherwise = fib (n -1) + fib (n -2)
Das Schlüsselwort otherwise steht hier für True.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
220
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionsdeklaration (4)
4. mittels Fallunterscheidung über Mustern:
<Funktionsbez > <Parametermuster > ... =
...
<Funktionsbez > <Parametermuster > ... =
<Ausdruck >
<Ausdruck >
Muster sind ein mächtiges Programmierkonstrukt, das weiter
unten genauer behandelt wird.
Beispiel:
fib 0 = 0
fib 1 = 1
fib n = fib (n -1) + fib (n -2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
221
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionsdeklaration (5)
5. mittels Kombinationen der Formen 3 und 4.
Beispiel:
fib
fib
|
|
0 = 0
n
n==1
= 1
otherwise = fib (n -1) + fib (n -2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
222
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Jeder Funktionsdeklaration sollte die Funktionssignatur
vorangestellt werden und ein Kommentar, der mindestens die
Voraussetzungen an die Parameter beschreibt.
Beispiel:
fib :: Integer -> Integer
-- fib k verlangt : k >= 0
fib 0 = 0
fib 1 = 1
fib n = fib (n -1) + fib (n -2)
• Die Form einer Funktionsdeklaration sollte so gewählt werden,
dass die Deklaration gut lesbar ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
223
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (rekursive Funktionsdeklaration)
1. Einzelne rekursive Funktionsdeklaration:
rpow :: Float -> Integer -> Float
-- rpow r m verlangt : m >= 0
rpow r n = if n == 0 then 1.0
else r * rpow r (n -1)
2. Verschränkt rekursive Funktionsdeklaration:
gerade
:: Integer -> Bool
ungerade :: Integer -> Bool
-- Bedingung an Parameter n bei beiden Funktionen :
-- n >= 0
gerade
n = (n == 0) || ungerade (n -1)
ungerade n = if n == 0 then False else gerade (n -1)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
224
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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.
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 Funktions- und
Datentypdeklarationen betrachten.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
225
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
226
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (rekursive Funktion)
Eine Funktion heißt rekursiv, wenn es rekursive
Funktionsdeklarationen gibt, mit denen sie definiert werden kann.
Bemerkungen:
• 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:
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)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
227
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Zur Auswertung von Funktionsanwendungen:
Sei f x = A[x] ;
Eine Funktionsanwendungen f e kann nach unterschiedlichen
Strategien durch Verwendung der Deklarationsgleichungen
ausgewertet werden, zum Beispiel call-by-value:
• 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.
Haskell benutzt die Auswertungsstrategie call-by-need (siehe 3.4).
Beispiele: (Rekursion)
siehe Vorlesung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
228
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (lineare/repetitive Rekursion)
Vereinfachend betrachten wir hier 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 (rekursiv),
wenn sie linear rekursiv ist und die rekursiven Anwendungen in
den Zweigen der Fallunterscheidung an äußerster Stelle stehen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
229
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele:
• Die übliche Definition von fac ist nicht repetitiv, da im Zweig der
rekursiven Anwendung die Multiplikation an äußerster Stelle steht.
• Die folgende Funktion facrep ist repetitiv:
facrep :: Integer -> Integer -> Integer
-- facrep n res verlangt : n >= 0 && res >= 1
facrep n res = if n == 0 then res
else facrep (n -1) (res*n)
fac n = facrep n 1
©Arnd Poetzsch-Heffter
TU Kaiserslautern
230
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Geschachtelte Rekursion)
• 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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
231
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (kaskadenartige Rekursion)
Berechne:
Wie viele Kaninchen-Pärchen leben nach n Jahren, wenn man
• am Anfang mit einem neu geborenden Pärchen beginnt,
• jedes neu geborene Pärchen nach zwei Jahren und dann jedes
folgende Jahr ein weiteres Pärchen Nachwuchs erzeugt und
• die Kaninchen nie sterben.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
232
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (kaskadenartige Rekursion) (2)
Die Anzahl der Pärchen stellen wir als Funktion ibo von n dar:
• vor dem 1. Jahr:
ibo(0) = 1
• nach dem 1. Jahr:
ibo(1) = 1
• nach dem 2. Jahr:
ibo(2) = 2
• nach dem n. Jahr:
die Anzahl aus dem Jahr vorher plus die Anzahl der im n. Jahr
Geborenen; und die ist gleich der Anzahl vor zwei Jahren, also:
ibo n = ibo(n − 1) + iob(n − 2) für n > 1.
Insgesamt ergibt sich folgende kaskadenartige Funktionsdeklaration:
ibo
n =
©Arnd Poetzsch-Heffter
if n<=1 then 1 else ibo (n -1) + ibo (n -2)
TU Kaiserslautern
233
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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 ibo 30 bereits
1.664.079 Anwendungen).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
234
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.3
Listen und Tupel
©Arnd Poetzsch-Heffter
TU Kaiserslautern
235
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Listen
Eine Liste über einem Typ T ist eine total geordnete Multimenge mit
Elementen aus T (bzw. eine Folge, d.h. eine Abb. Nat -> T ).
Eine Liste heißt endlich, wenn sie nur endlich viele Elemente enthält.
Haskell stellt standardmäßig eine Datenstruktur für Listen bereit, die
bzgl. des Elementtyps parametrisiert ist. Typparameter werden
üblicherweise geschrieben als a, b, ...
©Arnd Poetzsch-Heffter
TU Kaiserslautern
236
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Listen (2)
Typ:
[a] , a ist Typparameter
Funktionen:
(==), (/=)
(:)
(++)
head, last
tail, init
null
length
(!!)
take, drop
::
::
::
::
::
::
::
::
::
[a] → [a] → Bool
a → [a] → [a]
[a] → [a] → [a]
[a] → a
[a] → [a]
[a] → Bool
[a] → Int
[a] → Int → a
Int → [a] → [a]
wenn (==) auf a definiert
Konstanten:
[]
©Arnd Poetzsch-Heffter
:: [a]
TU Kaiserslautern
237
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Listen (3)
Dem Typ [a] ist als Wertemenge die Menge aller Listen über
Elementen vom Typ a zugeordnet.
Notation:
In Haskell gibt es eine vereinfachende Notation für Listen:
statt
x1 : x2 : ... : xn : []
kann man schreiben:
[ x1 , x2 , ..., xn ]
©Arnd Poetzsch-Heffter
TU Kaiserslautern
238
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionen auf Listen)
1. Addiere alle Zahlen einer Liste vom Typ [Int] mit neutralem
Element 0:
foldplus :: [Int] -> Int
foldplus xl = if null xl then 0
else (head xl) + foldplus (tail xl)
foldplus [1 ,2 ,3 ,4 ,5 ,6]
2. Prüfen einer Liste von Zahlen auf Sortiertheit:
ist_sortiert :: [Int] -> Bool
ist_sortiert xl = if null xl || null (tail xl)
then True
else if (head xl)<=(head (tail xl))
then ist_sortiert (tail xl)
else False
©Arnd Poetzsch-Heffter
TU Kaiserslautern
239
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionen auf Listen) (2)
3. Zusammenhängen zweier Listen (engl. append):
append :: [a] -> [a] -> [a]
append l1 l2 = if l1 == [] then l2
else (head l1):( append (tail l1) l2)
4. Umkehren einer Liste:
rev :: [a] -> [a]
rev xl = if null xl then []
else append (rev (tail xl)) [head xl]
©Arnd Poetzsch-Heffter
TU Kaiserslautern
240
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionen auf Listen) (3)
5. Zusammenhängen der Elemente einer Liste von Listen:
concat :: [[a]] -> [a]
concat xl = if null xl then []
else append (head xl) ( concat (tail xl))
6. Wende eine Liste von Funktionen vom Typ Int -> Int
nacheinander auf eine ganze Zahl an:
seqappl :: [( Int -> Int)] -> Int -> Int
seqappl xl i = if null xl then i
else seqappl (tail xl) (( head xl) i)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
241
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Rekursive Funktionsdeklaration sind bei Listen angemessen, weil
Listen rekursive Datenstrukturen sind.
• Mit Mustern lassen sich die obigen Deklaration noch eleganter
fassen (s. unten).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
242
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstrukturen der Paare
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. Der Typ der Paare ist
also ein Produkttyp.
Als Typkonstruktor wird (a,b) in Mixfix-Schreibweise benutzt.
Haskell stellt standardmäßig eine Datenstruktur für Paare bereit, die
bzgl. der Elementtypen parametrisiert ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
243
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstrukturen der Paare (2)
Typ:
(a,b) , a, b sind Typparameter
Funktionen:
(==), (/=) :: (a, b) → (a, b) → Bool
wenn (==) auf a und b definiert
(_,_)
:: a → b → (a, b)
fst
:: (a, b) → a
snd
:: (a, b) → b
Konstanten:
keine
Dem Typ (a,b) ist die Menge der geordneten Paare mit Elementen
vom Typ a und b zugeordnet.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
244
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Funktionen auf Paaren)
Transformiere eine Liste von Paaren in ein Paar von Listen:
unzip :: [(a, b)] -> ([a], [b])
unzip xl =
if null xl then ([] , [])
else ( (fst (head xl)):(fst ( unzip (tail xl))),
(snd (head xl)):(snd ( unzip (tail xl))) )
it = unzip [(1 , 2) , (3, 4) , (9, 10)]
(auch das geht erheblich schöner mit Mustern)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
245
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der n-Tupel
Haskell unterstützt n-Tupel für alle n ≥ 3:
Typ:
(a,b,...) , a, b, ... sind Typparameter
Funktionen:
(==), (/=) :: (a, b, ...) → (a, b, ...) → Bool
wenn (==) auf a, b, ... definiert
(_,_,...) :: a → b → ... → (a, b, ...)
Konstanten:
keine
Seien n ≥ 3 und a1 , . . . , an Typen mit Wertemenge w(a1 ), . . . , w(an );
dann ist dem Tupeltyp (a1 , . . . , an ) das kartesische Produkt
w(a1 ) × · · · × w(an ) als Wertemengen zugorndet; also eine Menge
geordneter n-Tupel, wobei das i-te Element vom Typ ai ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
246
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Es gibt keine Funkionen, um Elemente aus einem Tupel zu
selektieren. Dafür benötigt man Muster (siehe unten).
• Paare sind wie 2-Tupel, auf ihnen sind aber die Selektorfunktionen
fst und snd definiert.
• Es gibt keine 1-Tupel: Klammern um Ausdrücke dienen nur der
Strukturierung und haben darüber hinaus keine Bedeutung; d.h.
wenn e ein Ausdruck ist, ist ( e ) gleichbedeutend mit e.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
247
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Einheit
Haskell unterstützt eine Datenstruktur mit einem definierten Wert:
Typ:
()
Funktionen:
(==), (/=) :: () → () → Bool
Konstanten:
()
:: ()
Dem Typbezeichner () ist eine einelementige Wertemenge
zugeordnet. Der Wert wird als Einheit (engl. unity) bezeichnet.
Bemerkung:
Die Einheit wird oft als Ergebnis verwendet, wenn es keine relevanten
Ergebniswerte gibt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
248
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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
"aha"
"Tupel"
©Arnd Poetzsch-Heffter
"sind"
TU Kaiserslautern
"toll"
249
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Funktionen auf n-Tupeln)
1. Flache ein Paar von Paaren in ein 4-Tupel aus:
ausflachen :: ((a, b) ,(c, d)) -> (a, b, c, d)
-- nimmt ein Paar von Paaren und liefert 4- Tupel
ausflachen pp = ( fst (fst pp),
snd (fst pp),
fst (snd pp),
snd (snd pp) )
it = ausflachen ( (True ,7) , (’x’ ,5.6) )
Alternative Deklaration mit Mustern:
ausflachen ((a, b) ,(c, d)) = (a, b, c, d)
2. Funktion zur Paarbildung:
paarung :: a -> b -> (a,b)
paarung lk rk = (lk ,rk)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
250
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionstypen:
Eine zweistellige Funktion f mit Argumenten vom Typ a und Typ b und
Ergebnistyp c kann in Haskell auf zwei Arten typisiert werden:
1. Gecurryte Form:
f :: a -> b -> c
Nach den Präzedenzregeln für -> ist das a -> (b ->c),
also eine Funktion, die ein Wert vom Typ a nimmt und eine
Funktion vom Typ b -> c liefert.
Ist x::a und y::b, dann sind (f x) y oder gleichbedeutend
f x y korrekte Anwendungen.
2. Tupel-Form:
f :: (a,b) -> c
In diesem Fall ist für x::a und y::b, f (x,y) eine korrekte
Anwendungen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
251
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel:
Die Additionsoperation (+) auf Typ Int hat in Haskell den Typ
(+) :: Int -> Int -> Int
In der Mathematik typisiert man die Additionsoperation plus
üblichweise mit:
plus :: (Int ,Int) -> Int
Diese Variante kann man in Haskell wie folgt definieren:
plus ip = (fst ip) + (snd ip)
Oder eleganter mit Mustern:
plus (m,n) = m + n
©Arnd Poetzsch-Heffter
TU Kaiserslautern
252
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Muster in Deklarationen und Ausdrücken
Muster sind ein Sprachkonstrukt um strukturierte Werte einfacher
handhaben zu können (siehe Funktion ausflachen).
Ein Wert heißt hier strukturiert, wenn er mittels Konstruktoren aus
anderen Werten zusammengebaut wurde.
Konstruktoren sind spezielle Haskell-Funktionen.
Bisher behandelte Konstruktoren:
• der Listkonstruktor (:) (daher der Name “cons”)
• die Tupelbildung durch (_,...,_)
In 3.1.4 werden wir benutzerdefinierte Konstruktoren kennen lernen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
253
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Muster in Haskell)
Muster (engl. Pattern) in Haskell sind Ausdrücke gebildet über
Bezeichnern, Konstanten und Konstruktoren.
Alle Bezeichner in einem Muster müssen verschieden sein.
Ein Muster M mit Bezeichnern b1 , . . . , bk passt auf einen
strukturierten Wert w (engl.: a pattern matches a value w), wenn es
eine Substitution der Bezeichner bj in M durch Werte vj gibt, in
Zeichen M[v1 /b1 , . . . , vk /bk ], so dass
M[v1 /b1 , . . . , vk /bk ] = w
©Arnd Poetzsch-Heffter
TU Kaiserslautern
254
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (ML-Muster, Passen)
1. (x,y) passt auf (4,5) mit Substitution x=4, y=5.
2. (erstesElem,zweitesElem) passt auf (-47,(True,"dada"))
mit erstesElem =-47, zweitesElem =(True,"dada") .
3. x:xs passt auf 7:8:9:[] mit x = 7 und xs = 8:9:[] , d.h.
xs = [8,9].
4. x1:x2:xs passt auf 7:8:9:[] mit x1 = 7, x2 = 8, xs = [9] .
©Arnd Poetzsch-Heffter
TU Kaiserslautern
255
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (ML-Muster, Passen) (2)
5. first:rest passt auf ["computo","ergo","sum"]
mit first ="computo" und rest =["ergo", "sum"] .
6. ((8,x), (y,"aha")) passt
auf ((8,True), (("Tupel","sind","toll"), "aha"))
mit x = True und y = ("Tupel","sind","toll").
8
©Arnd Poetzsch-Heffter
True
y
TU Kaiserslautern
"aha"
256
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Wertvereinbarungen mit Mustern
Muster können in Haskell-Wertvereinbarungen verwendet werden:
<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.
Wenn das Muster auf den Wert des Ausdrucks nicht passt, wird eine
Ausnahme erzeugt, sobald auf einen der deklarierten Bezeichner
zugegriffen wird.
Beispiel: (Wertvereinbarung mit Muster)
(x, y)
=
©Arnd Poetzsch-Heffter
(4, 5);
TU Kaiserslautern
257
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionsvereinbarung mit Mustern
Muster können in Haskell-Funktionsdeklarationen verwendet werden
(vgl. Folie 221):
<Funktionsbez > <Parametermuster > ... =
...
<Funktionsbez > <Parametermuster > ... =
<Ausdruck >
<Ausdruck >
Bei der Funktionsanwendung wird der Reihe nach geprüft, auf welches
Parametermuster der aktuelle Parameter passt (vgl. Folie 228).
Die Gleichung zum ersten passenden Fall wird verwendet.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
258
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionsdeklaration mit Mustern)
1. Deklaration von foldplus ohne Muster:
foldplus :: [Int] -> Int
foldplus xl = if null xl then 0
else (head xl) + foldplus (tail xl)
Deklaration von foldplus mit Muster:
foldplus :: [Int] -> Int
foldplus []
= 0
foldplus (x:xl) = x + foldplus xl
©Arnd Poetzsch-Heffter
TU Kaiserslautern
259
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionsdeklaration mit Mustern) (2)
2. Deklaration von ist_sortiert::[Int] ->Bool mit drei Mustern:
ist_sortiert []
= True
ist_sortiert (x:[])
= True
ist_sortiert (x1:x2:xs) = if x1 <= x2
then ist_sortiert (x2:xs)
else False
Deklaration mit drei Mustern und Wächtern:
ist_sortiert []
ist_sortiert (x:[])
ist_sortiert (x1:x2:xs)
| x1 <= x2
| otherwise
©Arnd Poetzsch-Heffter
=
=
True
True
=
=
ist_sortiert (x2:xs)
False
TU Kaiserslautern
260
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionsdeklaration mit Mustern) (3)
Deklaration von ist_sortiert::[Int]->Bool mit zwei Mustern und
Wächtern:
ist_sortiert (x1:x2:xs)
| x1 <= x2
=
| otherwise
=
ist_sortiert x
=
ist_sortiert (x2:xs)
False
True
3. Deklaration von append ::[a]->[a]->[a]:
append [] xl2
= xl2
append (x:xl) xl2 = x : ( append xl xl2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
261
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionsdeklaration mit Mustern) (4)
4. Verwendung geschachtelter Muster:
unzip :: [(a, b)] -> ([a],[b])
unzip []
= ([] , [])
unzip ((x,y):ps) = ( (x : (fst ( unzip ps))),
(y : (snd ( unzip ps))) )
©Arnd Poetzsch-Heffter
TU Kaiserslautern
262
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
let-Ausdruck
Der Mustermechanismus kann auch innerhalb von Ausdrücken
eingesetzt werden.
Syntax des let-Ausdrucks:
let <Liste von Deklarationen >
in <Ausdruck >
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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
263
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (let-Ausdruck)
a = let a = 2*3
in a*a
b = let a = 2*3
in let (b,c) = (a,a+1)
in a*b*c
unzip :: [(a, b)] -> ([a], [b])
unzip []
= ([] , [])
unzip ((x,y):ps) = let (xs , ys) = unzip ps
in ((x:xs), (y:ys))
©Arnd Poetzsch-Heffter
TU Kaiserslautern
264
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
case-Ausdruck
Syntax des case-Ausdrucks:
case <Ausdruck0 >
of
<Muster1 > -> <Ausdruck1 >
...
<MusterN > -> <AusdruckN >
Prüfe der Reihe nach, ob der resultierende Wert von <Ausdruck0> 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).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
265
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (case-Ausdruck)
ist_sortiert xl =
case xl of
[]
-> True
(x:[])
-> True
(x1:x2:xs) -> if x1 <= x2
then ist_sortiert (x2:xs)
else False
©Arnd Poetzsch-Heffter
TU Kaiserslautern
266
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Das Verbot von gleichen Bezeichnern in Mustern hat im
Wesentlichen den Grund, dass nicht für alle Werte/Typen die
Gleichheitsoperation definiert ist.
mal2
=
twotimes =
(a,a)
=
\x -> 2*x
\x -> x+x
(mal2 , twotimes )
• Wenn keines der angegebenen Muster passt, wird eine
Ausnahme erzeugt (abrupte Terminierung).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
267
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.4
Benutzerdefinierte Datentypen
©Arnd Poetzsch-Heffter
TU Kaiserslautern
268
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Benutzerdefinierte Datentypen
Fast alle modernen Spezifikations- und Programmiersprachen
gestatten es dem Benutzer, „neue“ Typen zu definieren.
Übersicht:
• Vereinbarung von Typbezeichnern
• Deklaration neuer Typen
• Summentypen
• Rekursive Datentypen
©Arnd Poetzsch-Heffter
TU Kaiserslautern
269
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Vereinbarung von Typbezeichnern
Haskell erlaubt es, Bezeichner für Typen zu deklarieren (vgl. F. 208):
type IntPaar
= (Int ,Int) ;
type CharList
= [Char] ;
type Telefonbuch =
[(( String ,String ,String ,Int) ,[ String ])] ;
type IntegerNachInteger
=
Integer -> Integer ;
fakultaet :: IntegerNachInteger ;
-- Argument muss >= 0 sein
fakultaet = fac ;
Dabei wird kein neuer Typ definiert, sondern nur ein “neuer”
Bezeichner an einen bekannten Typ gebunden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
270
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen: (Typvereinbarungen)
• Typvereinbarungen können zur Abkürzung oder zur
Verdeutlichung benutzt werden (siehe Typ Telefonbuch).
• Zwei unterschiedliche Bezeichner können den gleichen Typ
bezeichnen; z.B.:
type IntTriple =
type Date
=
(Int ,Int ,Int)
(Int ,Int ,Int)
kalenderwoche :: Date -> Int
-- Parameter muss existierenden Kalendertag sein
kalenderwoche (tag ,monat ,jahr) = ...
kalenderwoche (11 ,12 ,2003)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
271
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Deklaration neuer Typen
Neue Typen werden in Haskell mit der datatype-Deklaration definiert,
die im Folgenden schrittweise erläutert wird.
Definition eines neuen Typs und Konstruktors:
data <NeuerTyp > =
<Konstruktor >
<Typ1 > ... <TypN >
Die obige Datatypdeklaration definiert:
• einen neuen Typ und bindet ihn an <NeuerTyp>
• eine Konstruktorfunktion mit Signatur
<Konstruktor >:: <Typ1 > -> ... -> <TypN > -> <NeuerTyp >
Die Konstruktorfunktion ist injektiv.
Typ- und Konstruktorbezeichner müssen mit einem Großbuchstaben
beginnen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
272
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Definition von Typ, Konstruktor, Selektoren)
data Person = Student String String Int Int
definiert den neuen Typ Person und den Konstruktor
Student :: String -> String -> Int -> Int -> Person
Wir definieren dazu folgende Selektorfunktionen:
vorname :: Person -> String
vorname ( Student v n g m)
= v
name :: Person -> String
name ( Student v n g m)
= n
geburtsdatum :: Person -> Int
geburtsdatum ( Student v n g m) = g
matriknr :: Person -> Int
matriknr ( Student v n g m)
©Arnd Poetzsch-Heffter
= m
TU Kaiserslautern
273
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
Jede Datentypdeklaration definiert einen neuen Typ, d.h.
insbesondere:
• die Werte des neuen Typs sind inkompatibel mit allen anderen
Typen;
• auch Werte strukturgleicher benutzerdefinierter Typen sind
inkompatibel.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
274
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Typkompatibilität)
1. Der Typ Person ist inkompatibel mit dem Tupeltyp
type Person2 =
(String ,String ,Int ,Int)
Insbesondere ist vorname ("Niels","Bohr",18851007,221) nicht
typkorrekt.
2. Person ist inkompatibel mit dem strukturgleichen Typ Adresse:
data Adresse = Wohnung String String Int Int
Insbesondere ist
name ( Wohnung " Casimirring " " Lautern " 27 67663 )
nicht typkorrekt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
275
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkung:
Den Konstruktor kann man sich als eine Markierung der Werte seines
Argumentbereichs vorstellen.
Dabei werden Werte mit unterschiedlicher Markierung als verschieden
betrachtet.
Konstruktoren erlauben es in gewisser Weise neue Produkttypen zu
definieren.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
276
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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.
In Haskell definiert man Summentypen durch Angabe von Alternativen
bei der datatype-Deklaration:
data <NeuerTyp > =
<Konstruktor1 >
|
<Konstruktor2 >
...
|
<KonstruktorM >
©Arnd Poetzsch-Heffter
<Typ1_1 > ... <Typ1_N1 >
<Typ2_1 > ... <Typ2_N2 >
<TypM_1 > ... <TypM_NM >
TU Kaiserslautern
277
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Summentypen)
1. Ein anderer Datentyp zur Behandlung von Personen:
data Person2 =
Student
String String Int Int
| Mitarbeiter String String Int Int
| Professor
String String Int Int String
©Arnd Poetzsch-Heffter
TU Kaiserslautern
278
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Summentypen) (2)
2. Eine benutzerdefinierte Datenstruktur für Zahlen:
data MyNumber = Intc
| Floatc
Int
Float
isInt :: MyNumber -> Bool
isInt (Intc m)
= True
isInt ( Floatc r)
= False
isFloat :: MyNumber -> Bool
isFloat (Intc m)
= False
isFloat ( Floatc r) = True
neg :: MyNumber -> MyNumber
neg (Intc m)
= Intc (-m)
neg ( Floatc r) = Floatc (-r)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
279
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Summentypen) (3)
plus :: MyNumber -> MyNumber -> MyNumber
plus (Intc m) (Intc n)
= Intc (m+n)
plus (Intc m) ( Floatc r)
=
Floatc (( fromInteger ( toInteger m))+r)
plus ( Floatc r) (Intc m)
=
Floatc (r+( fromInteger ( toInteger m)))
plus ( Floatc r) ( Floatc q) = Floatc (r+q)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
280
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung:
Konstruktorfunktionen oder Konstruktoren liefern Werte des neu
definierten Datentyps. Sie können in Mustern verwendet werden (z.B.:
Student, Intc).
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 verwendet stattdessen Muster/Pattern.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
281
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Weitere Operationen auf neu deklarierten Typen:
Konstruktoren und Selektoren erlauben das Aufbauen und Zerlegen
der Werte neu deklarierter Typen. Durch den Zusatz:
deriving (Eq ,Show)
liefert Haskell auch eine standardmäßige Gleichheit und die
Möglichkeit, Werte des neuen Typs mittels print auszugeben. Zum
Beipiel:
data MyNumber = Intc
Int
| Floatc Float
deriving (Eq ,Show)
Bemerkung:
Haskell ermöglich es dem Benutzer auch, die Gleichheit oder andere
Operationen auf neu definierten Typen selbst zu definieren.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
282
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Weitere Anwendungen der datatype-Deklaration:
Die datatype-Deklaration kann auch verwendet werden, um
Aufzählungstypen zu definieren, indem nur null-stellige
Konstruktoren benutzt werden.
Die Wertemenge eines Aufzählungstyps ist eine endliche Menge (von
Namen).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
283
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Aufzählungstypen)
data Wochentag =
Montag | Dienstag | Mittwoch | Donnerstag
| Freitag | Samstag | Sonntag
deriving (Eq ,Show)
istMittwoch :: Wochentag -> Bool
istMittwoch Mittwoch = True
istMittwoch _
= False
Oder knapper:
istMittwoch w
©Arnd Poetzsch-Heffter
=
(w== Mittwoch )
TU Kaiserslautern
284
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Konstruktoren mit beliebiger Stelligkeit
In einer Datentypdeklaration können Konstruktoren mit beliebiger
Stelligkeit kombiniert werden; z.B.:
data
MaybeInt =
|
Nothing
Just Int
Haskell sieht dafür im Prelude den folgenden parametrisierten Typ vor
(vgl. 3.3):
data Maybe a
©Arnd Poetzsch-Heffter
=
|
Nothing
Just a
TU Kaiserslautern
285
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Rekursive Datentypen
Von großer Bedeutung in allen Paradigmen der Programmierung sind
rekursive Datentypen. Sie erlauben es insbesondere:
• Listen beliebiger Länge
• Bäume beliebiger Höhe
behandeln zu können.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
286
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Definition: (rekursive Datentypen)
Eine Datentypdeklaration heißt direkt rekursiv, wenn der neu
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.
Ein Datentyp heißt rekursiv, wenn er mit einer rekursiven
Datentypdeklaration definiert wurde.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
287
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Listendatentypen)
1. Ein Datentyp für Integer-Listen:
data
Intlist =
Nil
| Cons Int Intlist
2. Ein Datentyp für homogene Listen mit Elementen von beliebigem
Typ:
data List a
|
©Arnd Poetzsch-Heffter
=
Nil
Cons
a (List a)
TU Kaiserslautern
288
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Baumartige Datenstrukturen:
Ottmann, Widmayer:
„Bäume gehören zu den wichtigsten in der Informatik auftretenden
Datenstrukturen“.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
289
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärungen: (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 ein Blatt, einen Knoten mit
Kindern einen inneren Knoten oder Zweig.
• Den Knoten ohne Elter nennt man Wurzel.
• Ein Baum heißt markiert, wenn jeder Knoten k eine Markierung
m(k ) besitzt.
• In einem Binärbaum hat jeder Knoten maximal zwei Kinder.
• Zu jedem Knoten k gehört ein Unterbaum, nämlich der Baum der
k als Wurzel hat.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
290
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Datentyp für markierte Binärbäume:
data
IntBBaum =
Blatt Int
| Zweig Int IntBBaum IntBBaum
deriving (Eq ,Show)
einbaum = Zweig 7 ( Zweig 3 ( Blatt 2) ( Blatt 4)) ( Blatt 5)
mark :: IntBBaum -> Int
mark (Blatt n)
= n
mark (Zweig n lk rk) = n
©Arnd Poetzsch-Heffter
TU Kaiserslautern
291
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Definition: (Sortiertheit markierter Binärbäume)
Ein mit ganzen Zahlen markierter Binärbaum heißt sortiert, wenn für
alle Knoten k gilt:
• Alle Markierungen der linken Nachkommen von k sind kleiner als
m(k ).
• Alle Markierungen der rechten Nachkommen von k sind größer
als m(k ).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
292
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Prüfung von Sortiertheit
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.
maxmark , minmark :: IntBBaum -> Int
maxmark ( Blatt n)
= n
maxmark ( Zweig n lk rk) =
n `max` ( maxmark lk `max` maxmark rk)
minmark ( Blatt n)
= n
minmark ( Zweig n lk rk) =
n `min` ( minmark lk `min` minmark rk)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
293
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Prüfung von Sortiertheit (2)
istsortiert :: IntBBaum
istsortiert ( Blatt n)
istsortiert ( Zweig n lk
istsortiert lk &&
( maxmark lk)<n &&
-> Bool
= True
rk) =
istsortiert rk &&
n <( minmark rk)
result = istsortiert einbaum
Wenig effiziente Lösung! Besser ist es, die Berechnung von Minima
und Maxima mit der Sortiertheitsprüfung zu verschränken.
Idee:
Entwickle eine Funktion istsortiert3 mit drei Ergebniswerten:
• Angabe, ob Baum sortiert
• minimale Markierung des Baums
• maximale Markierung des Baums
©Arnd Poetzsch-Heffter
TU Kaiserslautern
294
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Prüfung von Sortiertheit (3)
istsortiert3 :: IntBBaum -> (Bool ,Int ,Int)
-- Sei istsortiert3 b == (srt ,mn ,mx) ; dann ist
-srt das Ergebnis der Sortiertheitspruefung von b
-mn die minimale Markierung von b
-mx die maximale Markierung von b
istsortiert3 ( Blatt n)
= (True , n, n)
istsortiert3 ( Zweig n lk rk) =
let (lsrt , lmn , lmx) = istsortiert3 lk
(rsrt , rmn , rmx) = istsortiert3 rk
in (( lsrt && rsrt && lmx <n && n<rmn), lmn , rmx)
istsortiert b = let (srtflag ,_ ,_) = ( istsortiert3 b)
in srtflag
©Arnd Poetzsch-Heffter
TU Kaiserslautern
295
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Weitere Begriffe zu Bäumen)
Der leere Baum ist ein Baum ohne Knoten.
Die Tiefe eines Knotens in einem Baum ist sein Abstand zur Wurzel.
Der Wurzelknoten hat die Tiefe 0. Für alle anderen Knoten k gilt:
tiefe(k ) = tiefe(elternknoten(k )) + 1
Die Knoten gleicher Tiefe t nennt man das Niveau t.
Die Höhe des leeren Baumes ist 0. Die Höhe eines nicht-leeren
Baumes b ist die maximale Knotentiefe plus 1:
höhe( b ) = max { tiefe(k ) | k Knoten von b } + 1.
Die Größe eines Baums ist die Anzahl seiner Knoten.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
296
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Datentyp möglicherweise leerer Binärbäume:
data IntBBaum2 =
Leer
| Knoten Int IntBBaum2 IntBBaum2
©Arnd Poetzsch-Heffter
TU Kaiserslautern
297
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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:
data VBaum = Kn Int [ VBaum ]
deriving (Eq ,Show)
Der Rekursionsanfang ergibt sich durch Knoten mit leerer
Unterbaumliste.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
298
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bäume mit variabler Kinderzahl: (2)
zaehleKnVBaum
:: VBaum -> Int
zaehleKnVBaumLst :: [ VBaum ] -> Int
zaehleKnVBaum (Kn _ xs) =
1 + ( zaehleKnVBaumLst xs)
zaehleKnVBaumLst []
= 0
zaehleKnVBaumLst (x:xs) =
( zaehleKnVBaum x) + ( zaehleKnVBaumLst xs)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
299
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• 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 (vgl. nächstes Beispiel).
• Bäume mit variabler Kinderzahl werden auch zur Repräsentation
von strukturierten oder semi-strukturierter Daten verwendet (z.B.
XML, ...)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
300
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
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:
data Programm
deriving
data Wertdekl
deriving
data Ausdruck
= Prog [ Wertdekl ] Ausdruck
(Eq ,Show)
= Dekl String Ausdruck
(Eq ,Show)
= Bzn String
| Zahl Int
| Add Ausdruck Ausdruck
| Mul Ausdruck Ausdruck
deriving (Eq ,Show)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
301
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (abstrakte Syntaxbäume) (2)
Das Femto-Programm
a = 73;
main = print ( 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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
302
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Verschränkte Datentypdeklarationen:
Haskell unterstützt verschränkt rekursive Datentypdeklarationen.
Die Datentypdeklaration werden einfach hintereinander geschreiben
(wie verschränkt rekursiven Funktionsdeklarationen).
Bei abstrakten Syntaxbäumen wird häufig verschränkte Rekursion der
Datentypen benötigt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
303
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (verschränkte Datentypen)
Als Beispiel betrachten wir die abstrakte Syntax einer Erweiterung von
Femto um let-Ausdrücke:
data Programm
deriving
data Wertdekl
deriving
data Ausdruck
= Prog [ Wertdekl ] Ausdruck
(Eq ,Show)
= Dekl String Ausdruck
(Eq ,Show)
= Bzn String
| Zahl Int
| Add Ausdruck Ausdruck
| Mul Ausdruck Ausdruck
| Let Wertdekl Ausdruck
deriving (Eq ,Show)
Die Deklaration von Wertdekl benutzt Ausdruck; die Deklaration von
Ausdruck benutzt Wertdekl.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
304
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Unendliche Datenobjekte:
Zu einer nicht-leeren endlichen Liste kann man sich das erste Element
und eine Liste als Rest geben lassen.
Hat die endliche Liste xl die Länge n > 0, dann hat (tail xl) die
Länge n − 1.
Eine unendliche Liste besitzt keine natürlichzahlige Länge und wird
durch Anwendung von tail nicht kürzer.
Haskell unterstützt unendliche Liste und andere unendliche
Datenobjekte.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
305
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Strom)
Potenziell unendliche Listen werden meist als Ströme bezeichnet.
In Haskell kann man den Listendatentyp zur Realisierung von Strömen
verwenden.
Typische Operationen:
• Lesen des ersten Elements (head)
• Entfernen des ersten Elements (tail)
• Prüfen, ob noch Elemente im Strom vorhanden sind
Beispiel:
Strom von Eingabedaten
©Arnd Poetzsch-Heffter
TU Kaiserslautern
306
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (unendliche Liste)
Eine vorstellbare unendliche Liste ist die Liste der natürlichen Zahlen
[0,1,2,3,4,5,...]. In Haskell lässt sich diese Liste wie folgt definieren:
incr :: [ Integer ] -> [ Integer ]
incr []
= []
incr (x:xs) = (x+1):(incr xs)
natlist = 0:(incr natlist )
natlist2 = [0 ..]
©Arnd Poetzsch-Heffter
TU Kaiserslautern
307
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.5
Ein- und Ausgabe
©Arnd Poetzsch-Heffter
TU Kaiserslautern
308
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Ein- und Ausgabe
Der Haskell Prelude stellt Funktionen zur Verfügung, um
• Werte von der Standardeingabe zu lesen,
• Werte auf der Standardausgabe auszugeben und
• aus Dateien zu lesen und in Dateien zu schreiben.
Problem:
In einer rein funktionalen Sprache wie Haskell haben Funktionen keine
Seiteneffekte. Deshalb spielt auch die Reihenfolge des Aufrufs eine
weniger wichtige Rolle.
Ein- und Ausgabe erzeugen einen Seiteneffekt auf die
Programmumgebung. Sie müssen in der richtigen Reihenfolge
ausgeführt werden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
309
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Der Typ IO a
Um Funktionen mit Ein- und Ausgabe von Funktionen ohne Ein- und
Ausgabe zu trennen und die richtige Reihenfolge zu garantieren,
benutzt Haskell den vordefinierten parametrischen Typ
IO a
Zum Beispiel hat die Funktion putStr zur Ausgabe einer Zeichenreihe
die Signatur:
putStr :: String -> IO ()
putStr nimmt eine Zeichenreihe als Argument, schreibt die
Zeichenreihe in die Standardausgabe (erzeugt also einen
IO-Seiteneffekt) und liefert die Einheit als Ergebnis.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
310
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Der Typ IO a (2)
Wir nennen Funktionen und Konstanten mit Ergebnistyp IO a
IO-Aktionen.
Zum Beispiel ist die Konstante/null-stellige Funktion getLine eine
IO-Aktion:
getLine :: IO String
Ausführung der Aktion getLine liest von der Standardeingabe (erzeugt
also einen IO-Seiteneffekt) und liefert eine Zeichereihe.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
311
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Sequentielle Ausführung von IO-Aktionen
Für die sequentielle Ausführung von IO-Aktionen stellt Haskell die
do-Notation zur Verfügung:
do {
Anw1 ;
...
AnwN ;
IO - Aktion
}
do
Anw1
...
AnwN
IO - Aktion
Eine Anweisung Anw hat die Form
Bezeichner
<-
IO - Aktion
wobei der Bezeichner und Zuweisungpfeil “<-” entfallen können, wenn
das Ergebnis der IO-Aktion nicht gebraucht wird.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
312
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Anweisungssequenz)
main = do {
hSetBuffering stdout NoBuffering ; -- import System .IO
putStr "Gib eine Zeichenreihe ein: ";
s <- getLine ;
putStr " Revertierte Zeichereihe : ";
putStrLn ( reverse s)
}
Erläuterung:
• Die Anweisung s <- getLine bindet den gelesenen Wert an s.
• Da die letzte IO-Aktion den Typ IO () hat, hat auch main den Typ
IO ().
©Arnd Poetzsch-Heffter
TU Kaiserslautern
313
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Anweisungssequenz) (2)
Ausgabe aller Elemente einer String-Liste:
printStringList :: [ String ] -> IO ()
printStringList []
= putStr ""
printStringList (s:xs) =
do
putStr s
printStringList xs
©Arnd Poetzsch-Heffter
TU Kaiserslautern
314
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Parametrisierte IO-Aktionen
Bisher haben wir nur IO-Aktion für Zeichenreihen kennen gelernt.
Haskell unterstützt im Prelude und Bibliotheken viele weitere
IO-Aktionen. Z. B.:
getChar :: IO Char
putChar :: Char -> IO ()
Und für einen lesbaren und anzeigbaren Typ a die parametrischen
IO-Aktionen:
readLn :: IO a
print :: a -> IO ()
©Arnd Poetzsch-Heffter
TU Kaiserslautern
315
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Dateiein- und -ausgabe
Zum Lesen aus und Schreiben in Dateien stellt Haskell u. a. die
IO-Aktionen:
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
Dabei ist FilePath eine Zeichenreihe, die einen Dateinamen
bezeichnet:
type FilePath = String
©Arnd Poetzsch-Heffter
TU Kaiserslautern
316
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.6
Module
©Arnd Poetzsch-Heffter
TU Kaiserslautern
317
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Modulsystem)
Ein Programmmodul fasst mehrere Deklarationen zusammen und
stellt sie unter einem Namen zur Verfügung.
Ein Programmmodul sollte Programmierern eine Modul-Schnittstelle
bereitstellen, die unabhängig von der Implementierung dokumentiert
und benutzbar ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
318
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Module in Haskell
Vereinfacht dargestellt, hat ein Haskell-Modul die Form:
module <Modulname > ( <kommagetrennte Liste
exportierter Programmelemente >
) where
import <Modul1 >
...
import <Moduln >
<Deklarationen des Moduls >
Dabei gilt:
• Die Liste der importierten und exportierten Programmelemente
kann leer sein.
• Fehlt die Exportliste einschließlich der Klammern, werden alle in
dem Modul deklarierten Programmelemente exportiert.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
319
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Programm, die 2.)
Ein Haskell-Programm (vgl. F. 211) besteht aus einer Menge von
Modulen, wobei ein Modul
• den Namen Main haben muss und
• in diesem Modul der Name main deklariert sein muss.
Die Programmausführung beginnt mit der Ausführung der Deklaration
von main.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
320
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Haskell-Module)
module Alster where
avalue = 7;
data Art = Painting | Design | Sculpture
ache Design = False
ache _
= True
©Arnd Poetzsch-Heffter
TU Kaiserslautern
321
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Haskell-Module) (2)
module Breg where
import Alster
data Broterwerb = Designer | Maler | Bildhauer
beruf
beruf
beruf
beruf
:: Art ->
Design
Painting
Sculpture
Broterwerb
= Designer
= Maler
= Bildhauer
bflag = (ache Painting )
©Arnd Poetzsch-Heffter
TU Kaiserslautern
322
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Haskell-Module) (3)
module Main where
import Breg
main = print bflag
Beachte:
Programmelemente aus Alster, z.B. avalue, sind nicht sichtbar in
Modul Main.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
323
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Modul-Schnittstelle)
module Environment (Env , emptyEnv , insertI , insertB ,
lookUp , delete , IBValue (Intv , Boolv , None)
) where
emptyEnv :: Env
-- leere Bezeichnerumbegung
insertI :: String -> Int -> Env -> Env
-- ( insertI bez i e) traegt die Bindung (bez ,i)
-- in die Umgebung e ein
insertB :: String -> Bool -> Env -> Env
-- ( insertB bez b e) traegt die Bindung (bez ,b)
-- in die Umgebung e ein
©Arnd Poetzsch-Heffter
TU Kaiserslautern
324
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Modul-Schnittstelle) (2)
lookUp :: String -> Env -> IBValue
-- ( lookUp bez e) liefert den Wert v der ersten
-- gefundenen Bindung (bez ,v) mit Bezeichner bez
delete :: String -> Env -> Env
-- ( delete bez e) loescht alle Bindungen (bez ,_)
-- mit Bezeichner bez
©Arnd Poetzsch-Heffter
TU Kaiserslautern
325
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Modul-Implementierung)
-- Modulimplementierung ( Fortsetzung von Environment )
data IBValue = Intv Int
| Boolv Bool
| None
deriving (Eq , Show)
type Env = [ (String , IBValue ) ]
emptyEnv = []
insertI bez i e = (bez ,Intv i):e
insertB bez b e = (bez , Boolv b):e
©Arnd Poetzsch-Heffter
TU Kaiserslautern
326
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Modul-Implementierung) (2)
lookUp bez []
= None
lookUp bez ((bz ,val):e)
| bez == bz = val
| otherwise = lookUp bez e
delete bez []
= []
delete bez ((bz ,val):e)
| bez == bz = delete bez e
| otherwise = (bz ,val):( delete bez e)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
327
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.7
Zusammenfassung von 3.1
©Arnd Poetzsch-Heffter
TU Kaiserslautern
328
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Zusammenfassung von 3.1
Begriffe und Sprachmittel wie Ausdruck, Bezeichner, Vereinbarung,
Wert, Typ, Muster, Modul, . . . .
Wichtige Programmier- und Modellierungskonzepte:
• Basisdatenstrukturen
• rekursive Funktionen
• rekursive Datentypen (insbesondere Listen)
• Ein- und Ausgabe
©Arnd Poetzsch-Heffter
TU Kaiserslautern
329
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Abschnitt 3.2
Algorithmen auf Listen und Bäumen
©Arnd Poetzsch-Heffter
TU Kaiserslautern
330
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Algorithmen auf Listen und Bäumen
Sortieren und Suchen sind elementare Aufgaben, die in den meisten
Programmen anfallen.
Verfahren zum Suchen und Sortieren spielen eine zentrale Rolle in der
Algorithmik.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
331
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Lernziele:
• Intuitiver Algorithmusbegriff
• Kenntnis wichtiger/klassischer Algorithmen und
Algorithmenklassen
• Zusammenhang Algorithmus und Datenstruktur
• Wege vom Problem zum Algorithmus
• Implementierungstechniken für Datenstrukturen und Algorithmen
(vom Algorithmus zum Programm)
Bemerkung:
Wir führen in den Bereich Algorithmen und Datenstrukturen
ausgehend vom Problem ein. Andere Möglichkeit wäre gemäß der
benutzten Datenstrukturen (Listen, Bäume, etc.).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
332
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Unterabschnitt 3.2.1
Sortieren
©Arnd Poetzsch-Heffter
TU Kaiserslautern
333
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren
Sortieren ist eine Standardaufgabe, die Teil vieler speziellerer,
umfassenderer Aufgaben ist.
Untersuchungen zeigen, dass „mehr als ein Viertel der kommerziell
verbrauchten Rechenzeit auf Sortiervorgänge entfällt“
(Ottmann, Widmayer: Algorithmen und Datenstrukturen, Kap. 2).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
334
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Begriffsklärung: (Sortierproblem)
Gegeben ist eine Folge s1 , . . . , sN von sogenannten Datensätzen.
Jeder Satz sj hat einen Schlüssel kj . Wir gehen davon aus, dass die
Schlüssel ganzzahlig sind.
Aufgabe des Sortierproblems ist es, eine Permutation π zu finden, so
dass die Umordnung der Sätze gemäß π folgende Reihenfolge auf den
Schlüsseln ergibt:
kπ(1) ≤ kπ(2) ≤ · · · ≤ kπ(N)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
335
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
Offene Aspekte der Formulierung des Sortierproblem:
• Was heißt, eine Folge ist „gegeben“?
• Ist der Bereich der Schlüssel bekannt?
• Welche Operationen stehen zur Verfügung, um π zu bestimmen?
• Was genau heißt „Umordnung“?
©Arnd Poetzsch-Heffter
TU Kaiserslautern
336
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Aufgabenstellung:
Wir benutzen Datensätze folgenden Typs
type Dataset = (Int , String )
mit Vergleichsfunktion:
leq:: Dataset -> Dataset -> Bool
leq (kx , dx) (ky , dy) = (kx <=ky)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
337
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Aufgabenstellung: (2)
Entwickle eine Funktion
sort :: [ Dataset ] -> [ Dataset ]
so dass das Ergebnis von sort xl für alle Eingaben xl aufsteigend
sortiert ist und die gleichen Elemente enthält wie xl (mehrere Einträge
mit gleichem Schlüssel sind nicht ausgeschlossen).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
338
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Aufgabenstellung: (3)
Wir betrachten:
• Sortieren durch Auswahl (engl. selection sort)
• Sortieren durch Einfügen (engl. insertion sort)
• Bubblesort
• Sortieren durch rekursives Teilen (quick sort)
• Sortieren durch Mischen (merge sort)
• Heapsort
©Arnd Poetzsch-Heffter
TU Kaiserslautern
339
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Auswahl (selection sort)
Algorithmische Idee:
• Entferne einen minimalen Eintrag min aus der Liste.
• Sortiere die Liste, aus der min entfernt wurde.
• Füge min als ersten Element an die sortierte Liste an.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
340
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Auswahl (selection sort) (2)
select :: Dataset -> [ Dataset ] -> Dataset
-- Hilfsfunktion : liefert einen minimalen Eintrag der
-- Liste bzw. x, falls x minimal
select x []
= x
select x (y:yl) = if x `leq` y then select x yl
else select y yl
©Arnd Poetzsch-Heffter
TU Kaiserslautern
341
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Auswahl (selection sort) (3)
delete :: Dataset -> [ Dataset ] -> [ Dataset ]
-- Hilfsfunktion : loescht ein Vorkommen von x aus der
-- Liste , falls solches vorhanden
delete x []
= []
delete x (y:yl) = if (x==y) then
else
©Arnd Poetzsch-Heffter
TU Kaiserslautern
yl
y:( delete x yl)
342
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Auswahl (selection sort) (4)
selectionsort :: [ Dataset ] -> [ Dataset ]
-- Sortieren durch Auswahl
-mnm: ein minimaler Eintrag in Liste xl
-rest: die Liste xl ohne min
selectionsort []
= []
selectionsort (x:xl) =
let mnm = select x xl
rest = delete mnm (x:xl)
in
mnm : ( selectionsort rest)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
343
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Einfügen (insertion sort)
Algorithmische Idee:
• Sortiere zunächst den Rest der Liste.
• Füge dann den ersten Eintrag in die sortierte Liste ein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
344
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Einfügen (insertion sort) (2)
insert :: Dataset -> [ Dataset ] -> [ Dataset ]
-- Hilfsfunktion : fuegt Argument in sortierte Liste ein
-- Ergebnis : sortierte Liste
insert x []
=
insert x (y:yl) =
©Arnd Poetzsch-Heffter
[x]
if (x `leq` y)
then x : (y:yl)
else y : ( insert x yl)
TU Kaiserslautern
345
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Einfügen (insertion sort) (3)
insertionsort :: [ Dataset ] -> [ Dataset ]
-- Sortieren durch Einfuegen
insertionsort []
= []
insertionsort (x:xl) = insert x ( insertionsort xl)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
346
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bubblesort
Algorithmische Idee:
• Schiebe einen Eintrag nach rechts heraus:
I Beginne dazu mit dem ersten Eintrag x.
I Wenn Schieben von x auf einen gleichen oder größeren Eintrag y
stößt, schiebe y weiter.
I Ergebnis: maximaler Eintrag mxe und Liste ohne mxe
• Sortiere die Liste ohne mxe und hänge mxe an.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
347
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bubblesort (2)
bubble :: [ Dataset ] -> Dataset -> [ Dataset ]
-> ([ Dataset ], Dataset )
-- Hilfsfunktion : liefert einen maximalen Eintrag der
-- Liste und die Liste ohne den maximalen Eintrag
bubble rl x []
= (rl ,x)
bubble rl x (y:yl) = if (x `leq` y)
then bubble (rl ++[x]) y yl
else bubble (rl ++[y]) x yl
©Arnd Poetzsch-Heffter
TU Kaiserslautern
348
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bubblesort (3)
bubblesort :: [ Dataset ] -> [ Dataset ]
-- Sortieren durch Herausschieben der maximalen
-Elemente
bubblesort []
= []
bubblesort (x:xl) = let (rl ,mxe) = bubble [] x xl
in ( bubblesort rl)++[ mxe]
©Arnd Poetzsch-Heffter
TU Kaiserslautern
349
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Quicksort: Sortieren durch Teilen
Algorithmische Idee:
• Wähle einen beliebigen Datensatz mit Schlüssel k aus, das
sogenannte Pivotelement.
• Teile die Liste in zwei Teile:
I 1. Teil enthält alle Datensätze mit Schlüsseln < k
I 2. Teil enthält die Datensätze mit Schlüsseln ≥ k
• Wende quicksort rekursiv auf die Teillisten an.
• Hänge die resultierenden Listen und das Pivotelement zusammen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
350
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Quicksort: Sortieren durch Teilen (2)
split :: Dataset -> [ Dataset ] -> ([ Dataset ],[ Dataset ])
-- Hilfsfkt .: teilt Liste in zwei Listen (below , above )
-- below : alle Elemente in kleiner p
-- above : alle Elemente groesser gleich p
split p
split p
let
in
[]
= ([] ,[])
(x:xr) =
(blw ,abv) = split p xr
if p `leq` x then (blw ,x:abv)
else (x:blw ,abv)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
351
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Quicksort: Sortieren durch Teilen (3)
qsort :: [ Dataset ] -> [ Dataset ]
-- Sortieren nach der Strategie ``Teile und Herrsche ’’
qsort []
= []
qsort (p:rest) = let (below , above ) = split p rest
in ( qsort below ) ++ [p] ++ ( qsort above )
©Arnd Poetzsch-Heffter
TU Kaiserslautern
352
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
Quicksort ist ein typischer Algorithmus gemäß der
Divide-and-Conquer-Strategie:
• Zerlege das Problem in Teilprobleme.
• Wende den Algorithmus auf die Teilprobleme an.
• Füge die Ergebnisse zusammen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
353
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Mischen:
Algorithmische Idee:
• Hat die Liste mehr als ein Element, berechne die Länge der Liste
div 2 (halfsize).
• Teile die Liste in zwei Teile der Länge halfsize (+1).
• Sortiere die Teile.
• Mische die Teile zusammen.
Bemerkung:
Mergesort ist auch effizient für das Sortieren von Datensätzen, die auf
externen Speichermedien liegen und nicht vollständig in den
Hauptspeicher geladen werden können.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
354
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
merge :: [ Dataset ] -> [ Dataset ] -> [ Dataset ]
-- Hilfsfunktion : mischt zwei sortierte Listen
-- zu einer sortierten Liste zusammen
merge
merge
merge
merge
[] []
[] yl
xl []
(x:xl) (y:yl)
©Arnd Poetzsch-Heffter
=
=
=
=
[]
yl
xl
if (x `leq` y)
then x : ( merge xl (y:yl))
else y : ( merge (x:xl) yl)
TU Kaiserslautern
355
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung: (2)
mergesort :: [ Dataset ] -> [ Dataset ]
-- Sortieren durch Mischen
-halfsize : Haelfte der Listenlaenge
-front :
Vordere Haelfte der Liste
-back :
Hintere Haelfte der Liste
mergesort []
= []
mergesort (x:[]) = [x]
mergesort xl
=
let halfsize = ( length xl) `div` 2
front
= take halfsize xl
back
= drop halfsize xl
in
merge ( mergesort front ) ( mergesort back)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
356
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Heapsort
Heapsort verfeinert die Idee des Sortierens durch Auswahl:
• Minimum bzw. Maximum wird nicht durch lineare Suche gefunden,
• sondern mit logarithmischem Aufwand durch Verwendung einer
besonderen Datenstruktur, dem sogenannten Heap.
Algorithmische Idee:
• 1. Schritt: Erstelle den Heap zur Eingabeliste.
• 2. Schritt:
I Entferne Maximumelement aus Heap (konstanter Aufwand) und
hänge es an die Ausgabeliste.
I Stelle Heap-Bedingung wieder her (logarithmischer Aufwand).
I Fahre mit Schritt 2 fort bis der Heap leer.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
357
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
• Ziele des Vorgehens:
I Beispiel für komplexe, abstrakte Datenstruktur
I Zusammenhang der algorithmischen Idee und der Datenstruktur.
• Der Begriff „Heap“ist in der Informatik überladen.
Auch der Speicher für zur Laufzeit angelegte Variablen wird im
Englischen „heap“genannt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
358
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Begriffsklärung: (zu Bäumen)
Wir betrachten im Folgenden Binärbäume, die
• entweder leer sind oder
• aus einem markierten Knoten mit zwei Unterbäumen bestehen.
Ein Blatt ist dann ein Knoten mit zwei leeren Unterbäumen.
Ein Binärbaum heißt strikt, wenn jeder Knoten ein Blatt ist oder zwei
nicht-leere Unterbäume besitzt.
Ein Binärbaum der Höhe h heißt vollständig, wenn er strikt ist und
alle Blätter die Tiefe h − 1 haben.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
359
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Begriffsklärung: (zu Bäumen) (2)
Ein Binärbaum der Höhe h heißt fast vollständig, wenn
• jedes Blatt die Tiefe h − 1 oder h − 2 hat,
• jeder Knoten mit einer Tiefe kleiner h − 2 zwei nicht-leere
Unterbäume hat,
• für die Knoten K des Niveaus h − 2 gilt:
1. Hat K 2 nicht-leere Unterbäume, dann auch alle linken Nachbarn
von K .
2. Ist K ein Blatt, dann sind auch alle rechten Nachbarn von K Blätter.
3. Es gibt maximal ein K mit genau einem nicht-leeren Unterbaum
und der ist links.
Ein Baum der Größe n heißt indiziert, wenn man seine Knoten mittels
der Indizes 0, . . . , n − 1 ansprechen kann.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
360
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
Im Folgenden gehen wir bei indizierten Bäumen immer davon aus,
dass die Reihenfolge der Indices einem Breitendurchlauf folgt (siehe
Beispiel).
Beispiel: (Fast vollst., indizierter und markierter Binärbaum)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
361
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Modulschnittstelle für fast vollständige, markierte,
indizierte Binärbäume:
module FvBintree (FVBintree ,create ,size ,get ,swap ,
removeLast ,hasLeft ,hasRight ,left , right ) where
create :: [ Dataset ] -> FVBintree
-- Erzeugt fast vollstaendigen Binaerbaum , wobei
-- die Listenelemente zu Markierungen werden
size :: FVBintree -> Int
-- Anzahl der Knoten des Baums
get :: FVBintree -> Int -> Dataset
-- Liefert Markierung am Knoten mit Index i,
-- 0 <= i < size b
©Arnd Poetzsch-Heffter
TU Kaiserslautern
362
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Modulschnittstelle für fast vollständige, markierte,
indizierte Binärbäume: (2)
swap :: FVBintree -> Int -> Int -> FVBintree
-- Vertauscht Markierungen der Knoten mit Index i und j
-- Modifiziert fbt; 0 <= i,j < size b
removeLast :: FVBintree -> FVBintree
-- Entfernt letzten Knoten
hasLeft :: FVBintree -> Int -> Bool
-- Knoten mit Index i hat linkes Kind
-- 0 <= i < size b
hasRight :: FVBintree -> Int -> Bool
-- Knoten mit Index i hat rechtes Kind
-- 0 <= i < size b
©Arnd Poetzsch-Heffter
TU Kaiserslautern
363
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Modulschnittstelle für fast vollständige, markierte,
indizierte Binärbäume: (3)
left :: FVBintree -> Int -> Int
-- Liefert Index des linken Kinds von Knoten mit Index i
-- 0 <= i < size b && hasLeft i
right :: FVBintree -> Int -> Int
-- Liefert Index des rechten Kinds von Knoten mit Index i
-- 0 <= i < size b && hasRight i
©Arnd Poetzsch-Heffter
TU Kaiserslautern
364
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Vorgehen
• Wir gehen im Folgenden davon aus, dass wir eine
Implementierung von FvBintree haben (steht zum Testen bereit)
und realisieren heapsort damit; d.h. ohne die
Struktur/Implementierung zu kennen.
• Im Zusammenhang mit der objektorientierten Programmierung
werden wir dann eine sehr effiziente Implementierung für fast
vollständige Binärbäume kennen lernen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
365
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
• Wir benutzen die Datenstruktur ohne die Implementierung zu
kennen; man sagt die Datenstruktur ist für den Nutzer abstrakt
und spricht von abstrakter Datenstruktur.
• Abstrakte Datenstrukturen werden über ihre Schnittstelle
benutzt. Entwicklung und Benutzung von solchen Schnittstellen ist
ein zentraler Bestandteil der SW-Entwicklung.
• Eine abstrakte Datenstruktur kann unterschiedliche
Implementierungen haben. Implementierungen können
ausgetauscht werden, ohne dass der Nutzer seine Programme
ändern muss!
©Arnd Poetzsch-Heffter
TU Kaiserslautern
366
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Begriffsklärung: (Heap)
Ein markierter, fast vollständiger, indizierter Binärbaum mit n Knoten
heißt ein Heap der Größe n, wenn die folgende Heap-Eigenschaft
erfüllt ist:
Ist M ein Knoten und N ein Kind von M mit Markierungen kM und kN ,
dann gilt:
kM ≥ kN
Bei einem Heap sind die Knoten entsprechend einem Breitendurchlauf
indiziert (siehe Beispiel unten).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
367
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Beispiel: (Heap)
Heap der Größe 8:
©Arnd Poetzsch-Heffter
TU Kaiserslautern
368
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
Die Heap-Eigenschaft garantiert, dass der Schlüssel eines Knotens M
größer gleich aller Schlüssel in den Unterbäumen von M ist.
Insbesondere steht in der Wurzel ein Element mit einem maximalen
Schlüssel.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
369
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Herstellen der Heap-Eigenschaft:
Sei ein markierter, fast vollständiger Binärbaum gegeben, der die
Heap-Eigenschaft nur an der Wurzel verletzt.
Die Heap-Eigenschaft kann hergestellt werden, indem man den
Wurzelknoten M rekursiv in dem Unterbaum mit dem größeren
Schlüssel versickern lässt:
• Gibt es kein Kind, ist nichts zu tun.
• Gibt es genau ein Kind N, dann ist dies links und kinderlos:
Ist kM < kN , vertausche die Markierungen.
• Gibt es zwei Kinder und ist N das Kind mit dem größeren
Schlüssel:
Ist kM < kN , vertausche die Markierungen und fahre rekursiv mit
dem Unterbaum zu N fort.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
370
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Beispiel: (Versickern lassen)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
371
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Beispiel: (Versickern lassen) (2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
372
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Beispiel: (Versickern lassen) (3)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
373
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Funktion heapify formuliert auf Basis von FvBintree:
heapify :: FVBintree -> Int -> FVBintree
-- Stelle Heap - Eigenschaft im Knoten ix her
-- Annahme : die Kinder von ix erfuellen die
-- Heap - Eigenschaft
heapify b ix =
let ds = get b ix
in if hasLeft b ix && not ( hasRight b ix)
then let lx = left b ix
in if get b lx `leq` ds
then b
else swap b ix lx
-- else ...
©Arnd Poetzsch-Heffter
TU Kaiserslautern
374
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Funktion heapify formuliert auf Basis von FvBintree:
(2)
else -- hat linken und
-- oder ist Blatt
if hasRight b ix
then let lx
=
rx
=
largerKid =
rechten Unterbaum
left b ix
right b ix
if get b lx `leq` get b rx
then rx
else lx
in if get b largerKid `leq` ds
then b
else heapify (swap b ix largerKid ) largerKid
else b
©Arnd Poetzsch-Heffter
TU Kaiserslautern
375
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Konkretisierung des Heapsort-Algorithmus:
1. Schritt:
• Erzeuge Binärbaum-Repräsentation aus Eingabefolge.
• Stelle Heap-Eigenschaft her, indem heapify ausgehend von den
Blättern für jeden Knoten aufgerufen wird. Es reicht, nur Knoten
mit Kindern zu berücksichtigen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
376
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Konkretisierung des Heapsort-Algorithmus: (2)
2. Schritt:
• Schreibe den Wurzel-Datensatz in die Ausgabe.
• Schreibe den Datensatz des letzten Elementes in den
Wurzelknoten (swap)
• Entferne das letzte Element.
• Stelle die Heap-Eigenschaft wieder her.
• Fahre mit Schritt 2 fort, solange die Größe > 0.
Lemma:
In einem fast vollständigen Binärbaum der Größe n sind die Knoten mit
den Indizes (n div 2) bis n − 1 Blätter.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
377
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Heapsort: Abstrakte Version
Wir betrachten zunächst Heapsort auf Basis des abstrakten Datentyps
für markierte, fast vollständige, indizierte Binärbaume:
heapifyAll :: FVBintree -> FVBintree
hpfyEmb
:: FVBintree -> Int -> FVBintree
-- Hilfsfunktionen fuer den ersten Schritt
heapifyAll b = hpfyEmb b (( size b) `div` 2)
hpfyEmb b 0
then b
else heapify b 0
hpfyEmb b ix = hpfyEmb ( heapify b ix) (ix -1)
©Arnd Poetzsch-Heffter
=
if size b == 0
TU Kaiserslautern
378
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Heapsort: Abstrakte Version (2)
-- heapsort : sortiert gegebene Liste
heapsort :: [ Dataset ] -> [ Dataset ]
heapsort xl = reverse ( sortheap ( heapifyAll ( create xl)))
sortheap :: FVBintree -> [ Dataset ]
sortheap hp =
if
size hp == 0
then []
else let maxds = get hp 0
hp1
= swap hp 0 (size hp - 1)
hp2
= removeLast hp1
hp3
= heapify hp2 0
in maxds : ( sortheap hp3)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
379
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkungen:
• Wie wir in Kapitel 4 zeigen, profitiert Heapsort davon, dass sich
fast vollständige, markierte, indizierte Binärbäume sehr effizient
mit Feldern realisieren lassen.
• Zu einem algorithmischen Problem (hier Sortieren) gibt es im Allg.
viele Lösungen.
• Algorithmische Lösungen unterscheiden sich in:
I der Laufzeiteffizienz (messbar)
I der Speichereffizienz (messbar)
I der „Komplexität“ der Verfahrensidee (im Allg. nicht messbar).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
380
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkungen: (2)
In den folgenden Kapiteln werden wir demonstrieren,
• wie einige der obigen Algorithmen in anderen
Programmierparadigmen formuliert werden können;
• wie der Effizienz/Komplexitätsbegriff präzisiert werden kann.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
381
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Unterabschnitt 3.2.2
Suchen
©Arnd Poetzsch-Heffter
TU Kaiserslautern
382
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Suchen
Die Verwaltung von Datensätzen basiert auf drei grundlegenden
Operationen:
• Einfügen eines Datensatzes in eine Menge von Datensätzen;
• Suchen eines Datensatzes mit Schlüssel k ;
• Löschen eines Datensatzes mit Schlüssel k .
Bemerkung:
Weitere oft gewünschte Operationen sind: Sortierte Ausgabe, Suchen
aller Datensätze mit bestimmten Eigenschaften, Bearbeiten von Daten
ohne eindeutige Schlüssel, etc.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
383
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Schnittstell zum Suchen
Wir betrachten die folgende Schnittstelle:
module Dictionary (Dict ,emptyDict ,get ,put , remove ) where
type Dict = STree
-- type des Dictionarys
emptyDict :: Dict
-- leeres Dictionary
get :: Dict -> Int -> (Bool , String )
-- Nachschauen des Eintrags zu Schluessel i
put :: Dict -> Int -> String -> Dict
-- Einfuegen des Eintrags (i,s),
-- Ueberschreibt ggf. alten Eintrag zu i
remove :: Dict -> Int -> Dict
-- Loeschen des Eintrags zu Schluessel i
©Arnd Poetzsch-Heffter
TU Kaiserslautern
384
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
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)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
385
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
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 .
©Arnd Poetzsch-Heffter
TU Kaiserslautern
386
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
387
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Datenstruktur für Suchbäume:
Wir stellen Dictionaries als Binärbäume mit Markierungen vom Typ
Dataset dar:
data STree =
Node Dataset STree STree
| Empty
deriving (Eq , Show)
emptyDict = Empty
Die Konstante emptyDict repräsentiert das leere Dictionary.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
388
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Invariante für Suchbäume:
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 des Moduls Dictionary keinen Zugriff auf die Konstruktoren
geben.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
389
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Suchen eines Eintrags:
Wenn kein Eintrag zum Schlüssel existiert, liefere (False,""); sonst
liefere (True,s), wobei s der String zum Schlüssel ist:
get
get
|
|
|
Empty k
= (False ,"")
(Node (km ,s) l r) k
k < km
=
get l k
km < k
=
get r k
otherwise =
(True ,s)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
390
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Einfügen eines Eintrags:
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
I in den linken Unterbaum der Wurzel eingefügt, wenn sein Schlüssel
kleiner ist als der Schlüssel der Wurzel;
I in den rechten, wenn er größer ist.
Dieses Verfahren wird rekursiv fortgesetzt, bis die Einfügeposition
bestimmt ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
391
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Beispiel:
Einfügen von 33:
©Arnd Poetzsch-Heffter
TU Kaiserslautern
392
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Implementierung von put:
Die algorithmische Idee lässt sich direkt umsetzen.
Beachte aber, dass das Dictionary nicht verändert wird, sondern ein
neues erzeugt und abgeliefert wird:
put Empty k s
= Node (k,s)
put (Node (km ,sm) l r) k s
| k == km
= Node (k,s) l
| k < km
= Node (km ,sm)
| otherwise = Node (km ,sm)
©Arnd Poetzsch-Heffter
Empty Empty
r
(put l k s) r
l (put r k s)
TU Kaiserslautern
393
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkungen:
• Die Reihenfolge des Einfügens bestimmt das Aussehen des
binären Suchbaums:
Reihenfolgen:
2;3;1
©Arnd Poetzsch-Heffter
1;3;2
TU Kaiserslautern
1;2;3
394
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkungen: (2)
• 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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
395
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
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:
©Arnd Poetzsch-Heffter
TU Kaiserslautern
396
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Löschen: (2)
1. Fall: K ist ein Blatt.
Lösche K :
Entsprechend, wenn Knoten mit Schlüssel X in rechtem Unterbaum.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
397
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Löschen: (3)
2. Fall: K mit Schlüssel X hat genau einen Unterbaum.
K wird im Eltern-Knoten durch sein Kind ersetzt und gelöscht:
Die anderen links-rechts-Varianten entsprechend.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
398
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Löschen: (4)
3. Fall: K mit Schlüssel X hat genau zwei Unterbäume.
Problem:
Wo werden die beiden Unterbäume nach dem Löschen von K
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, möglich da für KR
einer der Fälle zutrifft.
(andere Variante: größten Schlüssel im linken UB)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
399
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Löschen: (5)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
400
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Umsetzung in Haskell
• Die Fälle 1 und 2 lassen sich direkt behandeln.
• Für Fall 3 realisiere Hilfsfunktion removemin, die
I nichtleeren binären Suchbaum b als Parameter nimmt;
I ein Paar (mnm, br ) als Ergebnis liefert, wobei
I
I
©Arnd Poetzsch-Heffter
mnm der kleinste Datensatz in b ist und
br der Baum ist, der sich durch Löschen von mnm aus b ergibt.
TU Kaiserslautern
401
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Umsetzung in Haskell (2)
removemin :: STree -> (Dataset , STree )
-- Parameter : nichtleerer binaerer Suchbaum b.
-- Liefert Eintrag d mit kleinstem Schluessel in b
-- und Baum nach Loeschen von d in b
removemin (Node d Empty r) = (d,r)
removemin (Node d l r) =
let (mnm ,ll) = removemin l
in (mnm , Node d ll r)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
402
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Umsetzung in Haskell (3)
remove Empty k
= Empty
remove (Node (km ,s) l r) k
| k < km
= Node (km ,s) ( remove l k) r
| km < k
= Node (km ,s) l ( remove r k)
| l == Empty = r
| r == Empty = l
| otherwise =
-- k == km && l /= Empty /= r
let (mnm ,rr) = removemin r
in Node mnm l rr
©Arnd Poetzsch-Heffter
TU Kaiserslautern
403
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
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 aus- gefü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) ≤ h ≤ N
für Knotenanzahl N.
Folgerung:
Bei degenerierten natürlichen Suchbäumen kann linearer Aufwand für
alle Grundoperationen entstehen.
Im Mittel verhalten sich Suchbäume aber wesentlich besser. Zusätzlich
versucht man durch gezielte Reorganisation eine gute Balancierung zu
erreichen (siehe Kapitel 5).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
404
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Abschnitt 3.3
Polymorphie und Funktionen höherer Ordnung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
405
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
406
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Unterabschnitt 3.3.1
Typisierung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
407
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Typisierung
Inhalte:
• Was ist ein Typ?
• Ziele der Typisierung
• Polymorphie und parametrische Typen
• Typsystem von Haskell und Typinferenz
Fast alle modernen Spezifikations- und Programmiersprachen
besitzen ein Typsystem.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
408
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
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-, Listen- und Funktionstypen).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
409
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Was ist ein Typ? (2)
• Parametrische Typen beschreiben bestimmte Eigenschaften und
lassen andere offen; z.B.:
I
Elemente vom Typ [a] sind homogene Listen; man kann also null,
head, tail anwenden. Offen bleibt z.B. der Ergebnistyp von head.
I
Elemente vom Typ (a,[a]) 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:
somefun :: ( a, [a] ) -> [a]
somefun p = let (fstc ,sndc) = p
in fstc : sndc
©Arnd Poetzsch-Heffter
TU Kaiserslautern
410
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
411
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
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 übereinstimmen.
Beispiele: (Typprüfung von Ausdrücken)
f :: Int -> Int
dann sind:
f 7, f (head [1,2,3]), f (f 78)+ 9
typkorrekt;
f True, [f,5.6], f head nicht typkorrekt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
412
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Bemerkung:
Typisierung war lange Zeit nicht unumstritten.
Hauptgegenargumente sind:
• zusätzlicher Schreib- und Entwurfsaufwand
• Einschränkung der Freiheit:
I inhomogene Listen
I Nutzen der Repräsentation von Daten im Rechner
©Arnd Poetzsch-Heffter
TU Kaiserslautern
413
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel:
Es gibt viele Programme, die nicht typkorrekt sind, sich aber trotzdem
zur Laufzeit gutartig verhalten; z.B:
Aufgabe 1:
Schreibe eine Funktion frp:
• Eingabe: Liste von Paaren entweder vom Typ
(Bool,Int) oder (Bool,Float)
• Zulässige Listen: Wenn 1. Komponente True, dann 2.
Komponente vom Typ Int, sonst vom Typ Float
• Summiere die Listenelemente und liefere ein Paar mit
beschriebener Eigenschaft.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
414
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel: (2)
Realisierung in Haskell-Notation (kein Haskell-Programm, da nicht
typkorrekt!):
frp :: ( Bool , ? ) -> ?
frp [ ]
= (True , 0)
frp ((True ,n):xs) =
case frp xs of
(True ,k) -> (True , k+n )
(False ,q) -> (False , q+( fromInteger n))
frp (( False ,r):xs) =
case frp xs of
(True ,k) -> (False , ( fromInteger k)+r )
(False ,q) -> (False , q+r )
©Arnd Poetzsch-Heffter
TU Kaiserslautern
415
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel: (3)
Aufgabe 2:
Schreibe eine Funktion,
• die ein n-Tupel (n≥2) nimmt und
• die erste Komponente des Tupels liefert.
Kann in Haskell nicht definiert werden:
arbitraryfst :: (a,b,.. .) -> a
arbitraryfst (n,.. .) = n
©Arnd Poetzsch-Heffter
TU Kaiserslautern
416
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Polymorphie und parametrische Typen
Programmierer möchten vom Typsystem nicht weiter eingeengt
werden als nötig. Ziel:
• mächtige und flexible Typsysteme
• insbesondere Polymorphie und Parametrisierung
Im Allg. bedeutet Polymorphie Vielgestaltigkeit.
In der Programmierung bezieht sich Polymorphie auf die Typisierung
bzw. das Typsystem.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
417
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
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 Ausdrücke zu Werten bzw.
Objekten unterschiedlichen Typs ausgewertet werden können.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
418
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Bemerkung:
• Man unterscheidet:
I Parametrische Polymorphie
I 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 Haskell den Typ
Int ->Int ->Int oder Float ->Float ->Float geben.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
419
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Bemerkung: (2)
• 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] ist spezieller als der parametrische Typ [a] .
Insbesondere gilt:
Jeder Wert vom Typ [Int] kann überall dort benutzt werden, wo
ein Wert vom Typ [a] erwartet wird.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
420
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Typsystem von Haskell und Typinferenz
Typen werden in Haskell durch Typausdrücke beschrieben:
• Typkonstanten sind die Basisdatentypen: Bool, Char,
Int, Integer, Float, Double
• Typvariablen: a, meineTypvar, gTyp
• Ausdrücke gebildet mit Typkonstruktoren:
Seien 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 ]
Listentyp
TA1 -> TA2
Funktionstyp
Einem Typausdruck TA kann ein Typconstraint für Typvariablen a
vorangestellt werden: (Eq a) => TA
©Arnd Poetzsch-Heffter
TU Kaiserslautern
421
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiele: (Typausdrücke)
Int ,
a,
Bool
b
(Int ,Bool) , (Int ,a) ,
(Int ,a,b,a)
[ Integer ] , [a] , [( Float , b, a)]
Int -> Int , [a] -> [( Float , b, a)]
(Char -> Char) -> (Int -> Int)
(Eq a) => a -> [a] -> Bool
Hinweis:
-> ist rechtsassoziativ.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
422
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
423
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiele:
[Int]
ist spezieller als
[a]
[a]
ist spezieller als
[b]
a -> a
ist spezieller als
a -> b
und umgekehrt
([Int],b) und ([c],Bool) sind nicht vergleichbar, d.h. der erste
Ausdruck ist nicht spezieller als der zweite und der zweite nicht
spezieller als der erste.
(Eq a) => TA
©Arnd Poetzsch-Heffter
ist spezieller als
TA
TU Kaiserslautern
424
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Typen in Haskell
Bezeichne TE die Menge der Typausdrücke in Haskell.
• Die „ist_spezieller_als“ Relation auf TE × 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.
Damit ist gesagt, was Typen in Haskell sind.
Bemerkung:
Das Typsystem von Haskell ist feiner als hier dargestellt (Typklassen
und benutzerdefinierte Typconstraints).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
425
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel:
foldplus :: (Num a) => [a] -> a
-- Addiert alle Elemente einer Liste .
-- Listenelemente muessen alle vom gleichen Zahltyp sein
foldplus []
=
foldplus (x:xs) =
©Arnd Poetzsch-Heffter
0
x + foldplus xs
TU Kaiserslautern
426
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Typregeln in Haskell:
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 Hakell muss gelten:
• Die Typen der formalen Parameter müssen gleich den Typen der
aktuellen Parameter sein.
• Bedingte Ausdrücke müssen korrekt typisiert sein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
427
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Begriffserklärung: (Typinferenz)
Typinferenz bedeutet das Ableiten der Typannotation für Ausdrücke
aus den gegebenen Deklarationsinformationen.
Bemerkung:
In Programmiersprachen mit parametrischem Typsystem kann
Typinferenz sehr komplex sein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
428
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiele:
1. Leere Liste:
x = [ ]
Inferierter Typ für x :: [a]
2. Enthalten sein in Liste:
enthalten
enthalten
|
|
p1 []
=
p1 (x:xs)
p1 == x
=
otherwise =
False
True
enthalten p1 xs
Inferierter Typ für enthalten :: (Eq a) => a -> [a] -> Bool
©Arnd Poetzsch-Heffter
TU Kaiserslautern
429
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiele: (2)
3. Einsortieren in geordnete Liste mit Vergleichsfunktion:
einsortieren vop p1 []
= [p1]
einsortieren vop p1 (x:xs)
| p1 `vop` x
= p1:x:xs
| otherwise = x : ( einsortieren vop p1 xs)
Inferierter Typ für einsortieren ::
(t -> t -> Bool) -> t -> [t] -> [t]
Bemerkung:
Bei der Typinferenz versucht man immer den allgemeinsten Typ
herauszufinden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
430
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Parametrische markierte Binärbaumtypen:
1. Alle Markierungen sind vom gleichen Typ:
data
BBaum a =
Blatt a
| Zweig a ( BBaum a) ( BBaum a)
2. Blatt- und Zweigmarkierungen sind möglicherweise von
unterschiedlichen Typen:
data
BBaum a b =
Blatt a
| Zweig b ( BBaum a b) ( BBaum a b)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
431
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Parametrische markierte Binärbaumtypen: (2)
3. Und was passiert hier?
data
BBaum a b =
Blatt a
| Zweig b ( BBaum b a) ( BBaum a b)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
432
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Unterabschnitt 3.3.2
Funktionen höherer Ordnung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
433
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Funktionen höherer Ordnung
Überblick:
• Einführung in Funktionen höherer Ordnung
• Wichtige Funktionen höherer Ordnung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
434
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
435
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Sprachliche Aspekte:
Alle wesentlichen Sprachmittel zum Arbeiten mit Funktionen höherer
Ordnung sind bereits bekannt:
• Funktionsabstraktion
• Funktionsdeklaration
• Funktionsanwendung
• Funktionstypen
©Arnd Poetzsch-Heffter
TU Kaiserslautern
436
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Konzeptionelle Aspekte:
Zwei konzeptionelle Aspekte liegen der Anwendung von Funktionen
höherer Ordnung in der Software- Entwicklung 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.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
437
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel: (Abstraktion, Wiederverwendung)
Aufgabe:
Sortiere eine Liste xl von Zahlen durch Einfügen (insertion sort)
Rekursionsidee:
• Sortiere zunächst den Rest der Liste.
• Das ergibt eine sortierte Liste xs.
• Sortiere das erste Element von xl in xs ein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
438
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel: (Abstraktion, Wiederverwendung) (2)
einsortieren :: Int -> [Int] -> [Int]
einsortieren p1 []
= [p1]
einsortieren p1 (x:xs)
| p1 <= x
= p1:x:xs
| otherwise = x : ( einsortieren p1 xs)
sort []
= []
sort (x:xr) = einsortieren x (sort xr)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
439
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel: (Abstraktion, Wiederverwendung) (3)
Frage:
Was ist zu tun, damit sort auch Werte der Typen Char, String, Float,
etc. sortieren kann?
Antwort:
Abstraktion des Algorithmus: Führe die Vergleichsoperation als
weiteren Parameter ein.
einsortieren :: (t -> t -> Bool) -> t -> [t] -> [t]
einsortieren vop p1 []
= [p1]
einsortieren vop p1 (x:xs)
| p1 `vop` x
= p1:x:xs
| otherwise = x : ( einsortieren vop p1 xs)
sort vop []
= []
sort vop (x:xr) = einsortieren vop x (sort vop xr)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
440
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Anwendung:
l1
l2
l3
l4
=
=
=
=
sort
sort
sort
sort
(<=) [ 2,3, 968 , -98 ,34 ,0 ]
(>=) [ 2,3, 968 , -98 ,34 ,0 ]
((>=):: Float -> Float -> Bool) [1.0 ,1e -4]
(>=) [1.0 ,1e -4]
strcmp :: String -> String -> Bool
strcmp = (<=)
l5 = sort strcmp [" Abbay ","Abba","Ara","ab"]
©Arnd Poetzsch-Heffter
TU Kaiserslautern
441
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Bemerkung:
Polymorphe Funktionen können häufig auch Funktionen als Parameter
nehmen.
Beispiele:
1. Funktion cons auf Listen : abs :fac :[]
2. Identitätsfunktion: id = (x-> x)(x->x)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
442
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Wichtige Funktionen höherer Ordnung
Dieser Abschnitt betrachtet einige Beispiele für Funktionen höherer
Ordnung und diskutiert das Arbeiten mit solchen Funktionen.
Applikationsoperator:
($) :: (a -> b) -> a -> b
rechtsassoziativ; erlaubt andere Schreibweise/Klammersetzung:
fac $ fac $ n+1 statt fac (fac (n+1))
Funktionskomposition:
(.) :: (b -> c) -> (a -> b) -> a -> c
(f.g) x = f (g x)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
443
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Wichtige Funktionen höherer Ordnung (2)
Map:
Anwendung einer Funktion auf die Elemente einer Liste:
map f [x1,x2,x3] == [f x1, f x2, f x3]
map :: (a -> b) -> [a] -> [b]
map f []
= []
map f (x:xs) = (f x): map f xs
Beispiele:
map length [" Schwerter ","zu"," Pflugscharen "]
double n = 2*n
map (map double )
©Arnd Poetzsch-Heffter
[ [1 ,2] , [34829] ]
TU Kaiserslautern
444
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Currying und Schönfinkeln:
Funktionen mit einem Argumenttupel kann man die Argumente auch
sukzessive geben. Dabei entstehen Funktionen höherer Ordnung.
Beispiele:
times :: Integer -> Integer -> Integer
times m n = m * n
double :: Integer -> Integer
double = times 2
double 5
©Arnd Poetzsch-Heffter
TU Kaiserslautern
445
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Zwei Varianten von map im Vergleich:
1. Die beiden Argumente als Paar:
mapp :: (b -> a, [b]) -> [a]
mapp (f ,[])
= []
mapp (f,x:xs) = (f x): mapp (f,xs)
2. Die Argumente nacheinander („gecurryt“):
map :: (a -> b) -> [a] -> [b]
map f []
= []
map f (x:xs) = (f x): map f xs
©Arnd Poetzsch-Heffter
TU Kaiserslautern
446
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
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:
double :: Integer -> Integer
double = times 2
intListSort :: [Int] -> [Int]
intListSort = sort ((<=)::Int ->Int ->Bool)
doublelist :: [Int] -> [Int]
doublelist = map double
©Arnd Poetzsch-Heffter
TU Kaiserslautern
447
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Curryen von Funktionen:
Die Funktion curry liefert zu einer Funktion auf Paaren die zugehörige
gecurryte Funktion:
curry :: ((a, b) -> c) -> a -> b -> c
curry f x y = f (x,y)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
448
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Prüfen und Filtern von Listen:
any :: (a -> Bool) -> [a] -> Bool
-- Pruefe , ob Liste ein Element enthaelt , das
-- das gegebene Praedikat erfuellt
any pred [ ]
= False
any pred (x:xs) = pred x
||
any pred xs
all :: (a -> Bool) -> [a] -> Bool
-- Pruefe , ob jedes Listenelement das gegebene
-- Praedikat erfuellt
all pred [ ]
= True
all pred (x:xs) = pred x
©Arnd Poetzsch-Heffter
&&
all pred xs
TU Kaiserslautern
449
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Prüfen und Filtern von Listen: (2)
filter :: (a -> Bool) -> [a] -> [a]
-- Liste alle Elemente , die das gegebene Praedikat
-- erfuellen
filter pred [ ]
= []
filter pred (x:xs)
| pred x
= x : ( filter pred xs)
| otherwise
= ( filter pred xs)
Beispiele:
ismember x xs = any (\y-> x==y) xs
split p xs =
( filter (\y-> y<p) xs , filter (\y-> p<=y) xs)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
450
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Punktweise Veränderung von Funktionen:
Die "Veränderung" einer Funktion an einem Punkt des
Argumentbereichs:
update :: (Eq a) => (a -> b) -> a -> b -> a -> b
update f x v y
| x == y
=
| otherwise =
©Arnd Poetzsch-Heffter
v
f y
TU Kaiserslautern
451
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Falten von Listen:
Eine häufig benötigte Funktion ist das Falten einer Liste mittels einer
binären Funktion und einem neutralen Element bzw. Anfangselement:
foldr ⊗ n [e1, e2, . . . ,en] = e1 ⊗ (e2 ⊗ (...(en ⊗ n) . . . ))
Deklaration von foldr:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f n [ ]
= n
foldr f n (x:xs) = f x ( foldr f n xs)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
452
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Falten von Listen: (2)
Üblicherweise steht auch eine Funktion für das Falten von links zur
Verfügung:
foldl ⊗ n [e1, e2, . . . ,en] = (. . . ((n ⊗ e1) ⊗ e2) ... ⊗ en)
Deklaration von foldl:
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f n [ ]
= n
foldl f n (x:xs) = foldl f (f n x) xs
©Arnd Poetzsch-Heffter
TU Kaiserslautern
453
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Falten von Listen: (3)
Mittels der Faltungsfunktionen foldr und foldl lassen sich viele
Listenfunktionen direkt, d.h. ohne Rekursion definieren:
sum , product :: (Num a) => [a] -> a
sum
= foldr (+) 0
product = foldr (*) 1
(++) :: [a] -> [a] -> [a]
(++) l1 l2 = foldr (:) l2 l1
©Arnd Poetzsch-Heffter
TU Kaiserslautern
454
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Falten von Listen: (4)
Bäume mit variabler Kinderzahl lassen sich allgemeiner und
kompakter behandeln (vgl. Folien 298f):
data VBaum = Kn Int [ VBaum ]
deriving (Eq ,Show)
zaehleKnVBaum :: VBaum -> Int
zaehleknVBaum (Kn (_,xs)) =
foldr (+) 1 (map zaehleknVBaum xs)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
455
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Bemerkungen: (Funktionen höherer Ordnung)
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:
I die Bausteine müssen bekannt sein,
I die Bausteine müssen ausreichend generisch sein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
456
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Bemerkungen: (zur Haskell-Einführung)
• Ziel des Kapitels war es nicht, eine umfassende
Haskell-Einführung zu geben. Haskell dient hier vor allem als
Hilfsmittel, wichtige Konzepte zu erläutern.
• Die meisten zentralen Konstrukte wurden behandelt.
• Es fehlt insbesondere:
I Fehlerbehandlung
I Typklassen
I Aspekte imperativer und interaktiver Programmierung
• Viele Aspekte der Programmierumgebung wurden nicht näher
erläutert.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
457
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Abschnitt 3.4
Semantik, Testen und Verifikation
©Arnd Poetzsch-Heffter
TU Kaiserslautern
458
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Übersicht
• Einführung in Semantik von Programmiersprachen
• Testen und Verifikation
©Arnd Poetzsch-Heffter
TU Kaiserslautern
459
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Unterabschnitt 3.4.1
Zur Semantik funktionaler Programme
©Arnd Poetzsch-Heffter
TU Kaiserslautern
460
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Zur Semantik funktionaler Programme
Lernziele in diesem Unterabschnitt:
• Was bedeutet Auswertungssemantik?
• Wie sieht sie im Falle von Haskell aus?
• Welche Bedeutung haben Bezeichnerumgebungen dabei?
©Arnd Poetzsch-Heffter
TU Kaiserslautern
461
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Zur Semantik funktionaler Programme (2)
In erster Näherung definiert eine Funktionsdeklaration eine partielle
Funktion. Gründe für Partialität:
1. Der Ausdruck, der die Funktion definiert, ist bereits partiell:
division dd dr
hd x:xs = x
=
dd `div` dr
2. Behandlung rekursiver Deklarationen:
a. Insgesamt unbestimmt:
f :: a -> a
f x = f x
b. Teilweise unbestimmt (hier für negative Zahlen):
fac :: Integer -> Integer
fac n = if n==0 then 1 else n * fac(n -1)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
462
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Ziel:
Ordne jeder syntaktisch korrekten Funktionsdeklaration eine partielle
Funktion zu. Die Semantik beschreibt diese Zuordnung.
Wir unterscheiden hier denotationelle und operationelle Semantik.
Statt operationeller Semantik spricht man häufig von
Auswertungssemantik.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
463
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (denotationelle Semantik)
Eine Semantik, die jeder Funktionsdeklaration explizit eine partielle
Funktion als Bedeutung zuordnet, d.h. eine Abbildung von
Funktionsdeklarationen auf partielle Funktionen definiert, nennen wir
denotationell.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
464
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (denotationelle Semantik)
Eine denotationelle Semantik würde der obigen Funktionsdeklaration
von fac eine Funktion f
f : Z⊥ → Z⊥
zuordnen, wobei
Z⊥ = { x | x ist Wert vom Typ Integer } ∪ {⊥}
Diese Funktion muss die Gleichung für fac erfüllen.
Das Symbol ⊥ steht dabei für ündefiniertünd wird häufig als bottom
bezeichnet.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
465
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (denotationelle Semantik) (2)
Zwei mögliche Lösungen f1 und f2 :
(
f1 (k ) =
⊥ , falls k =⊥ oder k < 0
k ! , sonst


⊥ , falls k =⊥



0 ,k < 0
f2 (k ) = 


 k ! , sonst
©Arnd Poetzsch-Heffter
TU Kaiserslautern
466
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (denotationelle Semantik) (3)
Wir zeigen, dass f2 eine Lösung der Gleichung ist:
n =⊥: links:
f2 (⊥) =⊥
rechts: if ⊥= 0 then 1 else ⊥ ∗f2 (⊥ −1) = ⊥
n < 0:
links:
rechts:
f2 (n) = 0
if n = 0 then 1 else n ∗ f2 (n − 1) = n ∗ 0 = 0
n = 0:
links:
rechts:
f2 (0) = 0! = 1
if 0 = 0 then 1 else 0 ∗ f2 (0 − 1) = 1
n > 0:
links:
rechts:
f2 (n) = n!
if n = 0 then 1 else n ∗ f2 (n − 1)
= n ∗ (n − 1)! = n!
Genauso lässt sich zeigen, dass f1 eine Lösung ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
467
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (denotationelle Semantik) (4)
Die denotationelle Semantik muss sicherstellen,
• dass es für jede Funktionsdeklaration mindestens eine Lösung
gibt, und
• eine Lösung auszeichnen, wenn es mehrere gibt.
In den meisten Programmiersprachen wählt man die Lösung, die an
den wenigsten Stellen definiert ist, und betrachtet nur so genannte
strikte Funktionen als Lösung:
©Arnd Poetzsch-Heffter
TU Kaiserslautern
468
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (strikte Funktionen)
Eine n-stellige Funktion oder Operation heißt strikt, wenn sie ⊥ als
Ergebnis liefert, sobald eines der Argumente ⊥ ist.
Beispiele: (nicht-strikte Funktionen)
• Die dreistellige “Funktion” if-then-else und die boolschen
Operatoren && und || sind in fast allen Programmiersprachen
nicht strikt.
• In Haskell deklarierte Funktionen sind im Allg. nicht strikt:
ite :: Bool -> a -> a -> a
ite b x y = if b then x else y
Prelude >
45
©Arnd Poetzsch-Heffter
ite False (4 `div` 0) 45
TU Kaiserslautern
469
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkungen:
• Denotationelle Semantik basiert auf einer Theorie partieller
strikter Funktionen und Fixpunkttheorie.
I
I
Vorteil: Für Beweise besser geeignet.
Nachteil: Theoretisch aufwendiger zu handhaben.
• ⊥ steht für undefiniert, unabhängig davon, welcher der Gründe für
Partialität vorliegt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
470
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (operationelle Semantik)
Eine Semantik, die erklärt, wie eine Funktion oder ein Programm
auszuwerten ist, nennen wir operationell oder
Auswertungssemantik .
Wir erläutern
• eine Auswertungsstrategie für funktionale Programme,
• welche Rolle Bezeichnerumgebungen dabei spielen, und
• führen wichtige Begriffe ein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
471
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (formaler/aktueller Parameter)
Ein Bezeichner, der in einer Funktionsdeklaration einen Parameter
bezeichnet, wird formaler Parameter genannt.
Der Ausdruck oder Wert, der einer Funktion bei einer Anwendung
übergeben wird, wird aktueller Parameter genannt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
472
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (Auswertungsstrategie)
Die Auswertungsstrategie legt fest,
• in welchen Schritten die Ausdrücke ausgewertet werden und
• wie die Parameterübergabe geregelt ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
473
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: (Parameterübergabeverfahren)
Parameterübergabe:
1. Call-by-Value:
I
I
I
Werte die aktuellen Parameter aus.
Benutze die Ergebnisse anstelle der formalen Parameter im
definierenden Ausdruck/Rumpf.
Werte den Rumpf aus.
2. Call-by-Name:
I
I
Ersetze alle Vorkommen der formalen Parameter durch die
(unausgewerteten) aktuellen Parameterausdrücke.
Werte den Rumpf aus.
Unterschiedliche Auswertungsstrategien führen im Allg. zu
unterschiedlichen Ergebnissen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
474
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Auswertungsstrategien
Betrachte:
f (x,y) =
if x==0 then 1 else f (x-1,f(x-y,y))
Werte den Ausdruck f (1,0) aus:
1. Call-by-Value:
f (1 ,0)
=
=
=
if 1==0
then 1
else
f(1-1,f(1 -0 ,0))
if False then 1
else
f(1-1,f(1 -0 ,0))
f (1-1, f(1 -0 ,0) )
©Arnd Poetzsch-Heffter
TU Kaiserslautern
475
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Auswertungsstrategien (2)
=
=
=
=
=
=
f (0, f(1 -0 ,0) )
f (0, f(1 ,0) )
f (0, if 1==0
then 1
else
f(1-1,f(1 -0 ,0)))
.. ..
f (0, f(0, f (1 ,0) ))
.. ..
Diese Auswertung kommt nicht zum Ende, d.h. sie terminiert nicht.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
476
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Auswertungsstrategien (3)
2. Call-by-Name:
=
=
=
=
f (1 ,0)
if 1==0
then 1 else f(1-1,f(1 -0 ,0))
if False then 1 else f(1-1,f(1 -0 ,0))
f( 1-1, f(1 -0 ,0) )
if 1-1==0 then
else
©Arnd Poetzsch-Heffter
1
f(1-1-1,f(1-1-f(1-0, 0) ,f(1 -0 ,0)))
TU Kaiserslautern
477
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Auswertungsstrategien (4)
=
=
=
if 1-1==0
then
else
1
f(1-1-1,f(1-1-f(1-0, 0) ,f(1 -0 ,0)))
if True
then
else
1
f(1 -1 -1,f(1-1-f(1 -0 ,0) ,f(1 -0 ,0)))
1
Mit Call-by-Name terminiert die Auswertung von f(1,0).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
478
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (Normalform)
Der Ergebnisausdruck einer terminierenden Auswertung wird
Normalform genannt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
479
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Informelle Auswertungssemantik von Haskell
Haskell benutzt Call-by-Need zur Parameterübergabe und
Auswertung.
Call-by-Need ist eine verfeinerte Form von Call-by-Value, bei der ein
aktueller Parameter, wenn er mehrfach benötigt wird, nur einmal
ausgewertet wird.
In einer Sprache ohne Seiteneffekte wie Haskell unterscheiden sich
Call-by-Need und Call-by-Value aber nicht im Ergebnis, sondern nur in
der Effizienz der Auswertung.
Die Ausdrücke werden von
• von links nach rechts (engl. leftmost),
• von außen nach innen (engl. outermost) und
• nur, wenn sie gebraucht werden,
ausgewertet.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
480
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Eine Teilsprache von Haskell
data Exp =
Cond
Exp Exp Exp
| Ident
String
| Binary Op Exp Exp
| Lambda String Exp
| Appl
Exp Exp
| Let
String Exp Exp
| BConst Bool
| IConst Integer
| Closure String Exp Env
deriving (Eq , Show)
data Op = Plus | Mult | Eq
deriving (Eq , Show)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
481
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispielprogramme
-- let a = 5
-- in let b = a + 7
-in let a = 0 in b
letx = Let "a" ( IConst 5)
(Let "b" ( Binary Plus ( Ident "a") ( IConst 7))
(Let "a" ( IConst 0) ( Ident "b")))
©Arnd Poetzsch-Heffter
TU Kaiserslautern
482
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispielprogramme (2)
-- let fac = \n -> if n==0 then 1 else n * fac(n+( -1))
-- in fac 10
facx =
Let "fac"
( Lambda "n"
(Cond ( Binary Eq ( Ident "n") ( IConst 0))
( IConst 1)
( Binary Mult ( Ident "n")
(Appl ( Ident "fac")
( Binary Plus ( Ident "n")( IConst ( -1)
))
)
)
) )
(Appl ( Ident "fac") ( IConst 10))
©Arnd Poetzsch-Heffter
TU Kaiserslautern
483
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispielprogramme (3)
-- let o = \f -> \g -> \x -> f (g x) in
-- in let fac = \n->if n==0 then 1 else n*fac(n+( -1))
-in (fac `o` fac) 5
compx =
Let "o" ( Lambda "f"
( Lambda "g"
( Lambda "x" (Appl ( Ident "f") (Appl (
Ident "g") ( Ident "x"))))))
(Let "fac" ... -- wie oben
(Appl (Appl (Appl ( Ident "o") ( Ident "fac")) (
Ident "fac")) ( IConst 5))
)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
484
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Auswertungsbeispiel
=
=
=
=
=
=
=
eval (let a=5 in let b=a+7 in let a=0 in b)
eval (let b=a+7 in let a=0 in b)
[]
[ a=(5 ,[]) ]
eval (let a=0 in b) [ b=(a+7 ,[a=(5 ,[]) ]) , a=(5 ,[]) ]
eval b
[ a=(0 ,[ .. .]) , b=(a+7 ,[a=(5 ,[]) ]) , a=(5 ,[]) ]
eval (a+7)
(eval a
[ a=(5 ,[]) ]
[ a=(5 ,[]) ]) + (eval 7
[ a=(5 ,[]) ])
(eval 5 []) + 7
5 + 7 =
©Arnd Poetzsch-Heffter
12
TU Kaiserslautern
485
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Datentyp für Bezeicherumgebung
data Env = Ec [ (String ,(Exp ,Env)) ]
deriving (Eq , Show)
emptyEnv :: Env
-- leere Bezeichnerumbegung
insert :: String -> (Exp ,Env) -> Env -> Env
-- ( insert bez xe e) traegt die Bindung (bez ,xe)
-- in die Umgebung e ein
lookUp :: String -> Env -> (Exp ,Env)
-- ( lookUp bez e) liefert das Paar xe der ersten
-- gefundenen Bindung (bez ,xe) mit Bezeichner bez
©Arnd Poetzsch-Heffter
TU Kaiserslautern
486
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Funktionsabschlüsse:
Fragen:
Wie wird das Ergebnis eines funktionswertigen Ausdrucks dargestellt?
Wie wird eine benutzerdeklarierte Funktion in der
Bezeichnerumgebung dargestellt?
©Arnd Poetzsch-Heffter
TU Kaiserslautern
487
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Funktionsabschlüsse: (2)
Vorgehen:
Das Ergebnis eines funktionswertigen Ausdrucks wird als Triple
Closure s r e
dargestellt, den sogenannten Funktionsabschluss (engl. Closure):
• s bezeichnet den formalen Parameter
• r bezeichnet den Funktionsrumpf
• e bezeichnet die aktuell gültige Umgebung.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
488
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel:
=
=
=
=
=
=
eval (let a = 6 in let ida = \x -> (x+a) in ida 9)
eval (let ida = \x -> (x+a) in ida 9)
eval (ida 9)
[]
[ a=(6 ,[]) ]
[ ida = (\x->(x+a) ,[a=(6 ,[]) ]) ,a=(6 ,[])]
wende (eval ida [ida=(\x->(x+a) ,[a=(6 ,[]) ]) ,a=(6 ,[]) ])
auf 9 mit e = [ida=(\x->(x+a) ,[a=(6 ,[]) ]) , a=(6 ,[])] an
wende
(eval (\x -> (x+a)) [a=(6 ,[]) ])
auf 9 mit e an
wende
( Closure x (x+a) [a=(6 ,[])] )
auf 9 mit e an
eval (x+a) [ x=(9,e), a=(6 ,[]) ]
=
(eval x [x=(9,e),a=(6 ,[]) ]) + (eval a [x=(9,e),a=(6 ,[]) ])
©Arnd Poetzsch-Heffter
TU Kaiserslautern
489
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Auswertungssemantik für die Haskell-Teilsprache:
eval :: Exp -> Env -> Exp
eval (Cond
bx tx ex) e = let BConst b = eval bx e
in if b then eval tx e
else eval ex e
eval (Ident s
) e = let (xv ,ev) = ( lookUp s e)
in eval xv ev
eval ( Binary bo lx rx) e = let IConst li = eval lx e
IConst ri = eval rx e
in evalOp bo li ri
eval ( Lambda s bx
) e = Closure s bx e
eval (Appl
fx px
) e = let Closure s b ce = eval fx e
in eval b ( insert s (px ,e) ce)
eval (Let
s dx bx ) e = let en = ( insert s (dx ,en) e)
in eval bx en
eval x
e = x
©Arnd Poetzsch-Heffter
TU Kaiserslautern
490
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Auswertungssemantik für die Haskell-Teilsprache: (2)
evalOp :: Op -> Integer -> Integer -> Exp
evalOp Plus li ri = IConst (li+ri)
evalOp Mult li ri = IConst (li*ri)
evalOp Eq
li ri = BConst (li==ri)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
491
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkungen:
• Wir haben die Auswertungssemantik von Haskells Teilsprache in
Haskell selbst definiert, weil wir keine andere
Beschreibungstechnik kennen.
• Üblicherweise wird man einen anderen
Beschreibungsformalismus wählen.
• Mit Ausnahme der Gleichung für die rekursiven Definitionen von
Let-Ausdrücken lassen sich alle Gleichungen als
Ersetzungsregeln benutzen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
492
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Spezifikation der zulässigen Parameter
Bei jeder (partiellen) Funktion muss man sich überlegen und
dokumentieren, welche aktuellen Parameter bei einer Anwendung
zulässig sein sollen.
Der Anwender der Funktion hat dann die Verantwortung, dass die
Komponente nie mit unzulässigen Parametern angewendet wird.
Üblicherweise sollte die Komponente für zulässige Parameter normal
terminieren.
Ggf. sind möglicherweise auftretende Ausnahmen zu dokumentieren.
Entsprechendes gilt für andere parametrisierte Softwarekomponenten.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
493
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkungen:
Es ist eine Entwurfsentscheidung,
• welche Parameter zulässig und welche unzulässig sind;
• wie man mit unzulässigen Paramtern umgeht.
Beispiel:
Varianten einer Funktion foo:
1. foo(m,n) = if m<n then m `div` n else foo(m-n,n)
Zulässigkeitsbereich
(m < n && n /=0)|| n > 0
2. foo(m,n) = if m < 0 || n <= 0
then -1
else if m<n then m `div` n else foo(m-n,n)
Zulässkeitsbereich (m >=0 && n > 0), vervollständigt durch -1.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
494
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkungen: (2)
3. foo(m,n) =
if (m <= 0 || n==0) && n <= 0 then undefined
else if m<n then m `div` n else foo (m-n,n)
Zulässigkeitsbereich (m < n && n /=0)|| n > 0;
Fehlermeldung, wenn Zulässigkeitsbereich verlassen wird.
Das Abprüfen der Zulässigkeit von Parametern (defensive
Programmierung) führt zu besserer Stabilität, allerdings oft auf
Kosten der Lesbarkeit und Effizienz der Softwarekomponente.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
495
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Design by Contract:
Entwerfe Programmteile zusammen mit genauen Spezifikationen ihrer
Schnittstellen.
Insbesondere spezifiziere bei Funktionen, welche Parameter zulässig
sind und was das Ergebnis im Zulässigkeitsbereich ist.
Die Spezifikation kann als Vertrag zwischen
• dem Anwender der Funktion/Komponente (client),
• demjenigen, der die Funktion/Komponente realisiert (provider)
verstanden werden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
496
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Spezifikation)
Spezifikation der Schnittstelle der Funktion heapifiy:
heapify :: FVBintree -> Int -> FVBintree
Vorbedingung, die ein Anwender von heapify b ix erfüllen sollte:
• ix ist ein Index von b sein, d.h. 0 ≤ ix < size b
• die Kinder von ix in b erfüllen die Heap-Eigenschaft
Nachbedingung an das Ergebnis e von heapify b ix umfasst:
• size e =size b
• die Markierungen der Knoten, die sich nicht im Unterbaum von ix
befinden, sind in e und b gleich;
• die Menge der Markierungen der Knoten, die sich im Unterbaum
von ix befinden, sind in e und b gleich;
• der Knoten mit Index ix und alle seine Kinder in b erfüllen die
Heap-Eigenschaft.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
497
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Unterabschnitt 3.4.2
Testen und Verifikation
©Arnd Poetzsch-Heffter
TU Kaiserslautern
498
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Testen und Verifikation
Ein zentraler Teil der Software-Entwicklung besteht darin zu prüfen, ob
die entwickelte Software auch den gestellten Anforderungen
entspricht.
Überblick:
• Einführende Bemerkungen zur Qualitätssicherung
• Testen
• Verifikation von Programmeigenschaften
©Arnd Poetzsch-Heffter
TU Kaiserslautern
499
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Qualitätssicherung: Kleine Einführung
Bei der Qualitätssicherung in der Softwareentwicklung spielen zwei
Fragen eine zentrale Rolle:
• Wird das richtige System entwickelt?
• Wird das System richtig entwickelt?
©Arnd Poetzsch-Heffter
TU Kaiserslautern
500
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Qualitätssicherung: Kleine Einführung (2)
Validation hat die Beantwortung der ersten Frage zum Ziel.
Beispielsweise ist zu klären, ob
• die benutzten Anforderungen die Vorstellungen des Auftraggebers
richtig wiedergeben,
• die Anforderungen von allen Beteiligten gleich interpretiert
werden,
• Unterspezifizierte Aspekte richtig konkretisiert wurden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
501
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Qualitätssicherung: Kleine Einführung (3)
Validation prüft also die Übereinstimmung von Software mit den
Vorstellungen der Auftraggeber/ Benutzer bzw. mit der
Systemumgebung, in der die Software eingesetzt wird.
Unter Verifikation verstehen wir den Nachweis, dass Software
bestimmte, explizit beschriebene Eigenschaften besitzt.
Beispiele:
• Mit Testen kann man prüfen, ob ein Programm zu gegebenen
Eingaben die erwarteten Ausgaben liefert (Beschreibung:
Testfälle).
• Mittels mathematischen Beweisen kann man zeigen, dass ein
Programm für alle Eingaben ein bestimmtes Verhalten besitzt
(Beschreibung: boolesche Ausdrücke, logische Formeln).
• Nachweis, dass ein Programm einen gegebenen Entwurf und die
darin festgelegten Eigenschaften besitzt (Entwurfsbeschreibung).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
502
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkungen:
• Liegen die beschriebenen Eigenschaften in einer formalen
Sprache vor, kann die Verifikation automatisiert werden.
• Zu prüfende Eigenschaften bei Funktionen:
I Terminierung für zulässige Parameter
I Verhalten wie im Entwurf festgelegt
• Grundsätzlich können sich Validation und Verifikation auf alle
Phasen der Softwareentwicklung beziehen.
• Wir betrachten im Folgenden Testen und Verifikation durch
Beweis anhand einfacher Beispiele im Kontext der funktionalen
Programmierung. Eine systematischere Betrachtung von
Aspekten der Qualitätssicherung ist Gegenstand von SE 2.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
503
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (Testen)
Testen bedeutet die Ausführung eines Programms oder
Programmteils mit bestimmten Eingabedaten.
Testen kann sowohl zur Validation als auch zur Verifikation dienen.
Bei funktionalen Programmen bezieht sich Testen überwiegend auf
Eingabe- und Ausgabeverhalten von Funktionen.
Wir betrachten:
• Testen mit Testfällen
• Testen durch dynamisches Prüfen
©Arnd Poetzsch-Heffter
TU Kaiserslautern
504
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Testen mit Testfällen
• Beschreibe das “Soll-Verhalten” der Software durch eine
(endliche) Menge von Eingabe-Ausgabe-Paaren.
• Prüfe, ob die Software zu den Eingaben der Testfälle die
entsprechenden Ausgaben liefert.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
505
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel:
Testen der folgenden Funktionsdeklaration:
fac :: Int -> Int
-- Berechnet fuer n in [0 .. 12] die Fakultaet
fac 0 = 1
fac n = if 0<n || n <= 12
then n * fac (n -1)
else undefined
©Arnd Poetzsch-Heffter
TU Kaiserslautern
506
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Testfälle:
0
12
13
-1
-1073741824
©Arnd Poetzsch-Heffter
→
→
→
→
→
1
479001600
Fehler: undefiniert/unzulaessiger Parameter
Fehler: undefiniert/unzulaessiger Parameter
Fehler: undefiniert/unzulaessiger Parameter
TU Kaiserslautern
507
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beobachtetes Verhalten:
0
12
13
-1
-1073741824
©Arnd Poetzsch-Heffter
→
→
→
→
→
1
479001600
1932053504
*** Exception: stack overflow
*** Exception: stack overflow
TU Kaiserslautern
508
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkung:
• Das Verhalten von Funktionen mit unendlichem Argumentbereich
kann durch Testen nur teilweise verifiziert werden. Testen kann im
Allg. nicht die Abwesenheit von Fehlern zeigen.
• Wichtig ist die Auswahl der Testfälle. Sie sollten die “relevanten”
Argumentbereiche abdecken.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
509
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Testen durch dynamisches Prüfen
• Beschreibe Eigenschaften von Zwischen- oder Ergebniswerten
mit den Mitteln der Programmiersprache (meist boolsche
Ausdrücke); d.h. implementiere Prüfprädikate.
• Rufe die Prüfprädikate an den dafür vorgesehenen Stellen im
Programm auf.
• Lasse die Prüfprädikate in der Testphase des zu testenden
Programms auswerten.
Bei negativem Prüfergebnis muss ein Fehler erzeugt werden.
Anders als beim Testen mit Testfällen wird also das Verhalten des
Programms an bestimmten Stellen automatisch während der
Auswertung geprüft.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
510
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Prüfungen:
1. Prüfung der Zulässigkeit von Parametern beim Aufruf
2. Prüfung durch Ergebniskontrolle
Bemerkung:
Viele moderne Programmiersprachen bieten spezielle
Sprachkonstrukte für das Testen durch dynamische Prüfung an.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
511
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Verifikation durch Beweis
Verifikation im engeren Sinne meint meist Verifikation durch Beweis.
Hier verwenden wir den Begriff in diesem engeren Sinne.
Im Gegensatz zum Testen erlaubt Verifikation (durch Beweis) die
Korrektheit zu zeigen, d.h. insbesondere die Abwesenheit von Fehlern.
Wir betrachten hier nur Programmverifikation, d.h. den Nachweis, dass
ein Programm eine spezifizierte Eigenschaft besitzt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
512
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Verifikation durch Beweis (2)
Die Spezifikation kommt üblicherweise aus dem Entwurf bzw. den
Anforderungen.
Zwei zentrale Eigenschaften:
• Programm liefert die richtigen Ergebnisse, wenn es terminiert
(partielle Korrektheit).
• Programm terminiert für die zulässigen Eingaben.
Beide Eigenschaften zusammen ergeben totale Korrektheit.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
513
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: (Spezifikation)
Spezifikation:
Eine Funktion ggt soll implementiert werden.
Für m, n mit m ≥ 0, n ≥ 0, m, n nicht beide null, soll gelten:
ggt m n = max { k | k teilt m und n }
©Arnd Poetzsch-Heffter
TU Kaiserslautern
514
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Implementierung)
Euklidscher Algorithmus:
ggt :: Integer -> Integer -> Integer
-- m, n >= 0, nicht beide gleich 0
ggt m n = if m==0 then n else ggt (n `mod` m) m
©Arnd Poetzsch-Heffter
TU Kaiserslautern
515
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beweisverfahren:
Bei funktionalen Programmen spielen zwei Beweisverfahren eine
zentrale Rolle:
1. Strukturelle Induktion oder Parameterinduktion
2. Berechnungsinduktion (computational induction)
Wir stellen nur die Paramterinduktion/strukturelle Induktion vor.
Bei der Parameterinduktion werden die Eigenschaften einer Funktion
für alle Parameter gezeigt, indem man eine Induktion über die Menge
der zulässigen Parameter führt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
516
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Verifikation von ggt)
Vorüberlegung:
Für n ≥ 0, m > 0 gilt:
k teilt m und n
⇔
k teilt m und k teilt (n mod m)
Induktion über den Parameterbereich:
Wir zeigen:
a) ggt ist korrekt für m = 0 und beliebiges n.
b) Vorausgesetzt: ggt ist korrekt für alle Paare (k , n) mit k ≤ m und n
beliebig;
dann auch für alle Paare (m + 1, n) mit n beliebig.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
517
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Verifikation von ggt) (2)
• Ad (a) – Induktionsanfang:
=
=
=
ggt 0 n
n
max{ k | k teilt n}
max{ k | k teilt 0 und n}
©Arnd Poetzsch-Heffter
TU Kaiserslautern
518
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (Verifikation von ggt) (3)
• Ad (b) – Induktionsschritt:
Voraussetzung: Sei m gegeben.
Für alle Paare (k , n) mit k ≤ m gilt: ggt ist korrekt für (k , n)
Zeige: Für alle n gilt: ggt ist korrekt für (m + 1, n)!
ggt (m+1) n
=
(* Deklaration von ggt *)
ggt (n mod (m+1)) (m+1)
=
(* n mod (m+1) ≤ m und Induktionsvoraussetzung *)
max { k | k teilt (n mod (m+1)) und (m+1) }
=
(* Vorueberlegung *)
max { k | k teilt (m+1) und n}
QED.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
519
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkung:
So wie Testen Testfälle oder Prüfprädikate voraussetzt, so benötigt
Verifikation mit Beweis eine Spezifikation oder andere Beschreibung
der zu zeigenden Eigenschaften.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
520
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Äquivalente Funktionsdeklarationen:
Wir haben gesehen, dass unterschiedliche Funktionsdeklarationen das
gleiche Ein- und Ausgabeverhalten haben können.
Zum Beispiel kann eine Deklaration in einer “aufwendigeren”
Rekursionsformen einfacher zu lesen sein, aber eine entsprechende
lineare oder repetitive Funktionsdeklaration performanter sein.
Transformiert man die eine in die andere Form ist es wichtig, die
Äquivalenz zu zeigen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
521
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkung:
• Semantisch äquivalente Programme können große Unterschiede
bei der Effizienz aufweisen.
• Bedeutungserhaltende Transformationen spielen in der
Programmoptimierung und dem Refactoring von Software eine
wichtige Rolle.
Korrektheit von Transformationen:
Wir transformieren die rekursive Funktionsdeklarationen in einfachere
Deklarationen und zeigen Äquivalenz.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
522
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (linear → repetitiv)
Wir betten die Fakultätsfunktion
fac :: Integer -> Integer
fac n = if n == 0 then 1 else n * fac (n -1)
in eine Funktion mit einem weiteren Argument ein, das
Zwischenergebnisse aufsammelt:
facrep :: Integer -> Integer -> Integer
facrep n res = if n==0 then res
else facrep (n -1) (res*n)
Damit lässt sich die Fakultät auch definieren als:
fac1 n = facrep n 1
Dadurch wurde eine lineare Rekursion in eine repetitive Form
gebracht.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
523
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Lemma:
Für die obigen Deklarationen von fac und facrep gilt:
∀n mit n ≥ 0, r mit r ≥ 0 :
insbesondere: ∀n mit n ≥ 0 :
©Arnd Poetzsch-Heffter
(fac n) ∗ r = facrep n r
fac n = fac1 n
TU Kaiserslautern
524
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beweis: mittels Parameterinduktion nach n
Induktionsanfang:
Zu zeigen: ∀r mit r ≥ 0 : (fac 0) ∗ r = facrep 0 r
=
=
=
=
(fac 0) * r
(* Deklaration von fac *)
(if 0==0 then 1 else 0 * (fac (0 -1))) * r
(* Ausdrucksauswertung *)
r
(* Ausdrucksauswertung *)
if 0==0 then r else facrep (0 -1) (r*0)
(* Deklaration von facrep *)
facrep 0 r
©Arnd Poetzsch-Heffter
TU Kaiserslautern
525
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beweis: (Forts.)
Induktionsschritt: k → k + 1
Induktionsvoraussetzung:
Für k ≥ 0 : ∀r mit r ≥ 0 : (fac k)*r =facrep k r
Zu zeigen: fac(k+1)* r = facrep (k+1)r
fac(k+1) * r
=
(* Deklaration von fac *)
(if k+1==0 then 1 else (k+1) * fac (k+1 -1)) * r
=
(* Ausdrucksumformung *)
(k+1) * (fac k) *r
©Arnd Poetzsch-Heffter
TU Kaiserslautern
526
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beweis: (Forts.)
(k+1) * (fac k) *r
=
(* Kommutativitaet + Assoziativitaet der Multipl . *)
(( fac k)* r) * (k+1)
=
(* Induktionsvoraussetzung *)
( facrep k r) * (k+1))
=
(* Ausdrucksumformung *)
if
=
k+1==0
then r else facrep k (r*(k+1))
(* Deklaration von facrep *)
facrep (k+1) r
©Arnd Poetzsch-Heffter
TU Kaiserslautern
527
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (kaskadenartig → linear)
Tranformation der Fibonacci-Funktion fib in eine lineare Form.
Idee:
Führe zwei zusätzliche Parameter ein, in denen ausgehend von 0 und
1 die Zwischenergebnisse berechnet werden.
Es soll also gelten:
fib1 n
=
fibemb n 0 1
Wir definieren fibemb zu:
fibemb 0 letzt res = 0
fibemb 1 letzt res = res
fibemb n letzt res = fibemb (n -1) res ( letzt +res)
Beweis: mittels Parameterinduktion nach n.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
528
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Terminierung
Zentrale Eigenschaft einer Funktionsdeklaration ist, dass ihre
Anwendung auf die zulässigen Parameter terminiert.
Diese Eigenschaft gilt für alle nicht-rekursiven Funktionsdeklarationen,
die sich nur auf terminierende Funktionen abstützen.
Bei rekursiven Funktionsdeklarationen muss die Terminierung
nachgewiesen werden.
Idee:
Die Parameter sollten bei jedem rekursiven Aufruf “kleiner” werden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
529
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: ("kleiner werdende" Parameter)
1. foldrplus [ ]
=
foldrplus (x:xs) =
0
x + foldrplus xs
2. einfuegen [] z ix
=
einfuegen xs z 0
=
einfuegen (x:xs) z ix =
[z]
z:xs
einfuegen xs z (ix -1)
3. foo m n = if m<n then m `div` n else foo (m-n) n
©Arnd Poetzsch-Heffter
TU Kaiserslautern
530
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Definition: (Ordnung)
Eine Teilmenge R von M × N heißt eine (binäre) Relation.
Gilt M = N, dann nennt man R homogen.
Eine homogene Relation heißt:
• reflexiv, wenn für alle x ∈ M gilt: (x, x) ∈ R
• antisymmetrisch, wenn für alle x, y ∈ M gilt:
wenn (x, y ) ∈ R und (y , x) ∈ R, dann x = y
• transitiv, wenn für alle x, y , z ∈ M gilt:
wenn (x, y ) ∈ R und (y , z) ∈ R, dann (x, z) ∈ R
©Arnd Poetzsch-Heffter
TU Kaiserslautern
531
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Definition: (Ordnung) (2)
Eine reflexive, antisymmetrische und transitive homogene Relation auf
M × M heißt eine (partielle) Ordnungsrelation.
Eine Menge M mit einer Ordnungsrelation R heißt eine (partielle)
Ordnung.
Meist benutzt man Infixoperatoren wie ≤ (oder ⊆) zur Darstellung der
Relation und schreibt
x ≤ y statt (x, y ) ∈ R
©Arnd Poetzsch-Heffter
TU Kaiserslautern
532
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Definition: (Kette, noethersche Ordnung)
Sei (M, ≤) eine Ordnung. Eine Folge ϕ : N → M heißt eine (abzählbar
unendliche) aufsteigende Kette, wenn für alle i ∈ N gilt:
ϕ(i) ≤ ϕ(i + 1)
(absteigende Kette: entsprechend).
Eine Kette ϕ wird stationär, falls es ein j ∈ N gibt, so dass
ϕ(j) = ϕ(j + k ) für alle k ∈ N
©Arnd Poetzsch-Heffter
TU Kaiserslautern
533
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Definition: (Kette, noethersche Ordnung) (2)
Sei N eine Teilmenge von M. x ∈ N heißt:
• größtes Element von N, wenn ∀y ∈ N gilt: y ≤ x.
• kleinstes Element von N, wenn ∀y ∈ N gilt: x ≤ y .
• maximales Element von N, wenn ∀y ∈ N gilt:
x ≤ y impliziert x = y .
• minimales Element von N, wenn ∀y ∈ N gilt:
y ≤ x impliziert x = y .
Eine Ordnung (M, ≤) heißt noethersch, wenn jede nicht-leere
Teilmenge von M ein minimales Element besitzt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
534
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Lemma:
Eine Ordnung ist genau dann noethersch, wenn jede absteigende
Kette stationär wird.
Beweis: (siehe Theorievorlesung)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
535
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Terminierungskriterium:
Sei f : S → T eine rekursive Funktionsdeklaration mit formalem
Parameter n und sei P die Menge der zulässigen Parameter von f .
Jede Anwendung von f auf Elemente von P terminiert,
• wenn es eine noethersche Ordnung (M, ≤)
• und eine Abb. δ : P → M gibt,
• so dass für jede rekursive Anwendung f (G(n)) im Rumpf der
Deklaration gilt:
i) G(n) ist ein zulässiger Parameter, d.h. G(n) ∈ P.
ii) Die aktuellen Parameter werden echt kleiner, d.h.
δ(G(n)) < δ(n)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
536
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkung:
• Da die aktuellen Parameter nur endlich oft echt kleiner werden
können (und dann stationär werden), garantiert das obige
Kriterium die Terminierung.
• Um die Terminierung nachzuweisen, muss man also eine
geeignete noethersche Ordnung und eine geeignete Abbildung δ
finden.
• Ist der Argumentbereich bereits noethersch geordnet, kann δ
selbstverständlich auch die Identität sein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
537
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: (Terminierungsbeweis)
1. foldrplus xs =
if null xs then 0
else ( headd xs) + foldrplus (tail xs)
P ist die Menge aller endlichen Listen über Integer.
Noethersche Ordnung (N, ≤).
Als δ wähle die Funktion länge (Länge einer Liste).
Zu zeigen:
i) tail xs ist ein zulässiger Parameter: ok.
ii) länge (tail xs) < länge xs: ok.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
538
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: (Terminierungsbeweis) (2)
2. einfuegen ([] ,z,ix)
=
einfuegen (xs ,z ,0)
=
einfuegen (x:xs ,z,ix) =
[z]
z:xs
einfuegen (xs ,z,ix -1)
P ist die Menge aller Tripel aus (a,[a],Int).
Noethersche Ordnung (N, ≤).
Als δ wähle die Funktion längefst, die länge auf die erste
Komponente anwendet.
Zu zeigen:
i) (xs,z,ix-1) ist ein zulässiger Parameter : ok.
ii) längefst(xs,z,ix-1) < längefst(x:xs,z,ix) : ok.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
539
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: (Terminierungsbeweis) (3)
Bemerkung:
Hätte man stattdessen für δ die Selektion auf die dritte Komponente
gewählt, hätte man Terminierung nur für eine kleinere Menge
zulässiger Parameter zeigen können, nämlich z.B. für Parametertripel
(xl,el,ix) mit ix ≥ 0.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
540
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: (Terminierungsbeweis) (4)
3. foo(m,n) =
if m<n then m `div` n else foo (m-n,n)
P ist die Menge aller Paare (m, n) aus (Integer,Integer)
mit (m < n und n , 0) oder n > 0.
Noethersche Ordnung (N, ≤).
δ : Z × Z → N mit
(
δ(m, n) =
©Arnd Poetzsch-Heffter
0
, falls m < n
m − n + 1 , falls m ≥ n
TU Kaiserslautern
541
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: (Terminierungsbeweis) (5)
Zu zeigen:
i) (m − n, n) ist ein zulässiger Parameter: ok.
ii) Unter der Voraussetzung m ≥ n und n > 0:
1. Fall: m − n ≥ n:
δ(m − n, n) = m − n − n + 1 < m − n + 1 = δ(m, n)
2. Fall: m − n < n:
δ(m − n, n) = 0 < m − n + 1 = δ(m, n)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
542
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Bemerkung:
• Terminierungsbeweise sind bei der Entwicklung von
Qualitätssoftware sehr wichtig, und zwar unabhängig vom
verwendeten Modellierungs- bzw. Programmierparadigma.
• Es sollte zur Routine der Softwareentwicklung, gehören, den
zulässigen Parameterbereich festzulegen und dafür Terminierung
zu zeigen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
543
Zugehörige Unterlagen
Herunterladen