Funktionales Programmieren

Werbung
3. Funktionales Programmieren
3.0
3. Funktionales Programmieren
Kapitel 3
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
Funktionales
Programmieren
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
3. Funktionales Programmieren
157
©Arnd Poetzsch-Heffter
3.0
TU Kaiserslautern
3. Funktionales Programmieren
Übersicht (2)
158
3.1 Grundkonzepte funktionaler Programmierung
Abschnitt 3.1
Zur Semantik funktionaler Programme
Testen und Verifikation
Grundkonzepte funktionaler Programmierung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
159
©Arnd Poetzsch-Heffter
TU Kaiserslautern
160
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Unterabschnitt 3.1.1
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
Zentrale Begriffe und Einführung
• 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
3. Funktionales Programmieren
161
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Definition: (partielle Funktion)
162
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:
Ein Funktion heißt partiell, wenn sie nur auf einer Untermenge ihres
Argumentbereichs definiert ist.
(
Andernfalls heißt sie total.
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
163
©Arnd Poetzsch-Heffter
TU Kaiserslautern
164
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (partielle Funktion) (2)
3.1 Grundkonzepte funktionaler Programmierung
Definition: (Funktionsanwendung, -auswertung,
Terminierung, Nichtterminierung)
Bezeichne f eine Funktion, a ein zulässiges Argument von f .
2. Bezeichne Float die Menge der auf dem Rechner darstellbaren
Gleitkommazahlen. Dann ist die Funktion
Die Anwendung von f auf a nennen wir eine Funktionsanwendung
(engl. function application); meist schreibt man dafür f (a) oder f a.
sqrt :: Float → Float ,
Den Prozess der Berechnung des Funktionswerts nennen wir
Auswertung (engl. evaluation). Die Auswertung kann:
die die Quadratwurzel (engl. square root) berechnet, partiell.
• nach endlich vielen Schritten terminieren und ein Ergebnis liefern
3. Bezeichne String die Menge der Zeichenreihen. Dann ist die
Funktion abschneide2, die die ersten beiden Zeichen einer
Zeichenreihe abschneidet partiell (warum?)
(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
3. Funktionales Programmieren
165
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen:
166
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Wert, Value)
• 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
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.
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
168
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Bemerkungen:
Begriffsklärung: (Typ, engl. type)
• In anderen Sprachparadigmen gibt es auch Werte, allerdings
Ein Typ (engl. type) fasst Werte zusammen, auf denen die gleichen
Funktionsanwendungen zulässig sind.
werden Funktionen nicht immer als Werte betrachtet (z.B. in der
objektorientierten Programmierung: „immutable objects“).
Typisierte Sprachen besitzen ein Typsystem, das für jeden Wert
festlegt, welchen Typ er hat.
• Im Mittelpunkt der funktionalen Programmierung steht die
Definition von Wertemengen (Datentypen) und Funktionen.
Funktionale Programmiersprachen stellen dafür Sprachmittel zur
Verfügung.
In funktionalen Programmiersprachen gibt es drei Arten von Werten
bzw. Typen, mit denen man rechnen kann:
• Basisdatentypen ( Int, Bool, String, . . . )
• Wie für abstrakte Objekte oder Begriffe typisch, besitzen Werte
I
I
I
I
• benutzerdef., insbesondere rekursive Datentypen
keinen Ort,
keine Lebensdauer,
keinen veränderbaren Zustand,
kein Verhalten.
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
• Funktionstypen, z.B.
Int → Bool oder
( Int → Int ) → ( Int → Int )
169
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
170
3.1 Grundkonzepte funktionaler Programmierung
Datenstrukturen
Definition: (Signatur einer Datenstruktur)
Eine Struktur fasst Typen und Werte zusammen, insbesondere also
auch Funktionen.
Die Signatur (T, F) einer Datenstruktur besteht aus
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.
• 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 ,
In der Mathematik nennt man solche Gebilde Algebren oder einfach
nur Strukturen.
Ti ∈ T,
0 ≤ i ≤ n,
definiert ist. n gibt die Stelligkeit von f an.
In der Informatik spricht man auch von einer Rechenstruktur.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
171
©Arnd Poetzsch-Heffter
TU Kaiserslautern
172
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Definition: (Datenstruktur mit Signatur)
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Wir betrachten zunächst die Basisdatenstrukturen, wie man sie in
jeder Programmier-, Spezifikations- und Modellierungssprache
findet.
Eine (partielle) Datenstruktur mit Signatur (T, F) ordnet
• Die Basisdatenstrukturen (engl. basic / primitive data structures)
bilden die Grundlage zur Definition weiterer Typen, Funktionen
und Datenstrukturen.
• 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.
• 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
3. Funktionales Programmieren
173
3.1 Grundkonzepte funktionaler Programmierung
Bool
::
::
::
::
::
174
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der booleschen Werte: (2)
Dem Typbezeichner Bool ist die Wertemenge {True, False} zugeordnet.
Funktionen:
(==)
(/=)
(&&)
(||)
not
TU Kaiserslautern
3. Funktionales Programmieren
Die Datenstruktur der booleschen Werte:
Typ:
©Arnd Poetzsch-Heffter
Bool → Bool → Bool
Bool → Bool → Bool
Bool → Bool → Bool
Bool → Bool → Bool
Bool → Bool
Konstanten:
(==)
(/=)
(&&)
(||)
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
True :: Bool
False :: Bool
©Arnd Poetzsch-Heffter
TU Kaiserslautern
175
©Arnd Poetzsch-Heffter
TU Kaiserslautern
176
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Bemerkungen:
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der ganzen Zahlen:
• Operatorsymbole werden meist mit Infix-Notation verwendet:
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.:
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
Typ:
Integer
Funktionen:
(==), (/=)
(<), (<=), (>), (>=)
(+), (*), (-)
div, mod
negate, signum, abs
• 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
::
::
::
::
::
Integer → Integer → Bool
Integer → Integer → Bool
Integer → Integer → Integer
Integer → Integer → Integer
Integer → Integer
Funktionsbezeichner und bezeichneter Funktion, wenn dies aus
Gründen der Klarheit nötig ist.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
177
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Die Datenstruktur der ganzen Zahlen: (2)
178
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:
Konstanten:
Funktionen:
– in Dezimaldarstellung: 0, 127, -23
– in Hexadezimaldarstellung: 0x0, 0x7F, −0x17
– in Oktaldarstellung: 0o0, 0o177, −0o27
(==), (/=)
(<), (<=), (>), (>=)
(+), (*), (-)
div, mod
negate, signum, abs
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
Int
179
::
::
::
::
::
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
3. Funktionales Programmieren
Die Datenstruktur der beschränkten ganzen Zahlen:
(2)
Begriffsklärung:
Wenn unterschiedliche Funktionen oder andere Programmelemente
den gleichen Bezeichner haben, spricht man vom Überladen des
Bezeichners (engl. Overloading).
Dem Typbezeichner Int ist eine rechnerabhängige Wertemenge
zugeordnet, die mindestens die ganzen Zahlen von −229 bis 229 − 1
enthalten muss.
Beispiel: (Überladung von Bezeichnern)
Innerhalb der Wertemenge sind die Funktionen der Datenstruktur der
beschränkten ganzen Zahlen verlaufsgleich mit den Funktionen auf
den ganzen Zahlen.
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, (==), (+)
Außerhalb der Wertemenge ist ihr Verhalten nicht definiert.
Insbesondere können (+), (∗), abs, negate partiell sein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
181
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Die Datenstruktur der Gleitkommazahlen:
182
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Gleitkommazahlen: (2)
Die Datenstruktur der Gleitkommazahlen erweitert die Datenstruktur
der ganzen Zahlen und bietet u. a.:
Typ:
Float
Konstanten:
Funktionen:
(==), (/=)
(<), (<=), (>), (>=)
(+), (*), (-), (/)
negate, signum, abs
fromInteger
truncate, round
ceiling, floor
exp, log, sqrt
(**), logBase
sin, cos, tan
©Arnd Poetzsch-Heffter
::
::
::
::
::
::
::
::
::
::
pi :: Float
– mit Dezimalpunkt: 0.0, 1000.0, 128.9, -2.897
– mit Exponenten: 0e0, 1e3, 1289e-1, -2897e-3
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
Dem Typbezeichner Float ist in Haskell eine rechnerabhängige
Wertemenge zugeordnet.
Entsprechendes gilt für die präzise Bedeutung der Funktionen und
Konstanten.
183
©Arnd Poetzsch-Heffter
TU Kaiserslautern
184
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Bemerkung:
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.:
• Die ganzen Zahlen sind in der Programmierung keine Teilmenge
Typ:
der reellen Zahlen!
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
(==), (/=)
:: Char → Char → Bool
(<), (<=), (>), (>=) :: Char → Char → Bool
succ, pred
:: Char → Char
toEnum
:: Int → Char
fromEnum
:: Char → Int
185
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Die Datenstruktur der Zeichen: (2)
186
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:
Konstanten:
– in Zeichendarstellung: 'A', 'a', '0', 'ß', '"'
– spezielle Zeichen: '\'', '\n', '\t', '\b', '\\'
– in numerischer Darstellung: '\65', '\x41', '\o101'
minBound, maxBound :: Char
Typ:
Dem Typbezeichner Char ist die Menge der Unicode-Zeichen
zugeordnet. Jedes Unicode-Zeichen besitzt eine Nummer im Bereich
von 0 bis 1.114.111 .
String oder [Char]
Funktionen:
(==), (/=)
:: String → String → Bool
(<), (<=), (>), (>=) :: String → String → Bool
head
:: String → Char
tail
:: String → String
length
:: String → Int
(++)
:: String → String → String
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.
TU Kaiserslautern
Char
Funktionen:
• 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
3.1 Grundkonzepte funktionaler Programmierung
187
©Arnd Poetzsch-Heffter
TU Kaiserslautern
188
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Die Datenstruktur der Zeichenreihen: (2)
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Zeichenreihen: (3)
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"
Den Vergleichsoperationen liegt die lexikographische Ordnung
zugrunde, wobei die Ordnung auf den Zeichen auf deren
Nummerierung basiert (siehe Datenstruktur Char).
– 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
3. Funktionales Programmieren
189
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkung:
190
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:
• Es wird unterschieden zwischen Zeichen und Zeichenreihen der
Länge 1.
• Jede Programmier-, Modellierungs- und Spezifikationssprache
I
besitzt Basisdatenstrukturen. Die Details variieren aber teilweise
deutlich.
Funktionen):
I
rechnerabhängig sind, entstehen Portabilitätsprobleme.
x = 7;
• der Definitionen von Typen:
I type String = [Char]
I data MyType = . . .
• Der Trend bei den Basisdatenstrukturen geht zur
Standardisierung.
TU Kaiserslautern
z.B. (7+23), 30
• Vereinbarung von Bezeichnern für Werte (einschließlich
• Wenn Basisdatenstrukturen implementierungs- oder
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
191
©Arnd Poetzsch-Heffter
TU Kaiserslautern
192
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beschreibung von Werten:
Beschreibung von Werten: (2)
• durch geschachtelte Anwendung von Funktionen:
• mittels Konstanten oder Bezeichnern für Werte:
45.67 + 6857 * (-9)
floor ( -3.4) * truncate ( -3.4)
toEnum ((( fromEnum (last("Urin"++" stinkt ")))+2))::Char
23
"Ich bin eine Zeichenreihe "
True
x
• durch Verwendung des bedingten Ausdrucks (engl. conditional
• durch direkte Anwendung von Funktionen:
expression):
abs ( -28382)
"Urin" ++ " stinkt "
not True
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
if <boolAusdruck > then <Ausdruck >
else <Ausdruck >
TU Kaiserslautern
3. Funktionales Programmieren
193
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Ausdruck, expression)
Begriffsklärung: (Ausdruck, expression) (2)
Ausdrücke sind das Sprachmittel zur Beschreibung von Werten. Ein
Ausdruck (engl. expression) in Haskell ist
Jeder Ausdruck hat einen Typ:
• Der Typ einer Konstanten ergibt sich aus der Signatur.
• eine Konstante,
• Der Typ eines Bezeichners ergibt sich aus dem Wert, den er
bezeichnet.
• ein Bezeichner (Variable, Name),
• Der Typ einer Funktionsanwendung ist der Ergebnistyp der
• die Anwendung einer Funktion auf einen Ausdruck,
Funktion.
• ein bedingter Ausdruck gebildet
• Der Typ eines if-then-else-Ausdrucks ist gleich dem Typ des
• oder ist mit Sprachmitteln aufgebaut, die erst später behandelt
Ausdruck im then- bzw. else-Zweig.
werden.
©Arnd Poetzsch-Heffter
194
TU Kaiserslautern
195
©Arnd Poetzsch-Heffter
TU Kaiserslautern
196
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Präzedenzregeln:
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:
Wenn Ausdrücke nicht vollständig geklammert sind, ist im Allg. nicht
klar, wie ihr Syntaxbaum aussieht.
infixl 7 ∗, /, div, mod
infixl 6 +, −
infix 4 ==, / =, <, >, <=, >=
infixr 3 &&
infixr 2 ||
Je höher die Präzedenzzahl, desto stärker binden die
Operationen.
Beispiele:
3 == 5 == True
False == True || True
False && True || True
• 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
3. Funktionales Programmieren
197
3.1 Grundkonzepte funktionaler Programmierung
198
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.
Bisher haben wir Ausdrücke formuliert, die sich auf die vordefinierten
Funktions- und Konstantenbezeichner von Haskell gestützt haben.
Syntaktisch gesehen heißt Programmierung:
Dadurch entsteht eine Bindung (n, e) zwischen dem Bezeichner n
und dem bezeichneten Programmelement e.
• neue Typen, Werte und Funktionen zu definieren,
• die neu definierten Elemente unter Bezeichnern zugänglich zu
An allen Programmstellen, an denen die Bindung sichtbar ist, kann der
Bezeichner benutzt werden, um sich auf das Programmelement zu
beziehen.
machen.
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Deklaration und Bezeichnerbindung:
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
199
©Arnd Poetzsch-Heffter
TU Kaiserslautern
200
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Bemerkung:
Wertvereinbarungen:
• Die verschiedenen Arten an Programmelementen, die in
• Wertvereinbarungen haben (u.a.) die Form:
Deklarationen vorkommen können, hängen von der
Programmiersprache ab.
<Bezeichner >
• In Haskell sind es im Wesentlichen:
1. Bezeichnervereinbarungen (nur zusammen mit
Wert-/Funktionsvereinbarung)
2. Wertvereinbarungen
3. Vereinbarungen (rekursiver) Funktionen
4. Vereinbarungen benutzerdeklarierter Typen
3. Funktionales Programmieren
<Ausdruck > ;
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
festlegen, sind ebenfalls sprachabhängig und können sehr
komplex sein. Wir führen die Sichtbarkeitsregeln schrittweise ein.
TU Kaiserslautern
=
• Wertvereinbarungen kann man eine Bezeichnervereinbarung
• Die Regeln, die die Sichtbarkeit von Bindungen bzw. Bezeichern
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
enthalten.
201
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiele: (Wertvereinbarungen)
202
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Wertvereinbarungen) (2)
Einzeilige Vereinbarungen im Interpreter ghci:
let b = 56 ;
b = 56 ;
Mehrzeilige Vereinbarungen im Interpreter ghci:
a::Int
a = 7
:{
let {
a::Int
a = 7
sieben
sieben
flag
dkv
}
:}
sieben
sieben
flag
dkv
:: Float ;
= 7.0 ;
= floor sieben == truncate (- sieben )
= " Deutscher Komiker Verein e.v."
©Arnd Poetzsch-Heffter
TU Kaiserslautern
203
©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
3. Funktionales Programmieren
Funktionsvereinbarungen:
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (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.
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)
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.
plus2 :: Integer -> Integer
plus2 = (+) 2
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
3. Funktionales Programmieren
205
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Typvereinbarungen:
Beispiele: (Typvereinbarungen)
Zwei Probleme:
type IntPaar
= (Int ,Int) ;
type CharList
= [Char] ;
type Telefonbuch =
[(( String ,String ,String ,Int) ,[ String ])] ;
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.
type IntegerNachInteger
Lösungen:
Zu 1. Führe “Ausdrücke” für Typen ein (z.B. Int -> Int).
Genaueres dazu in Unterabschnitt 3.1.4. (Folien 269ff).
TU Kaiserslautern
Integer -> Integer ;
fakultaet :: IntegerNachInteger ;
-- Argument muss >= 0 sein
fakultaet = fac ;
Zu 2. Benutze spezielle Syntax für rekursive Typdeklarationen.
©Arnd Poetzsch-Heffter
=
206
207
©Arnd Poetzsch-Heffter
TU Kaiserslautern
208
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Begriffsklärung: (Bezeichnerumgebung)
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 ist eine Abbildung von Bezeichnern auf
Werte (einschl. Funktionen) und Typen, ggf. auch auf andersartige
Programmelemente.
• Eine Bezeichnerumgebung wird häufig als Liste von Bindungen
Oft spricht man auch von Namensumgebung oder einfach von
Umgebung (engl. environment).
modelliert (vgl. Folie 200).
• Jede Datenstruktur und jedes Modul definiert eine
Bezeichnerumgebung.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
209
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Begriffsklärung: (Programm)
210
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)
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
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
3. Funktionales Programmieren
Unterabschnitt 3.1.2
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.
Rekursive Funktionen
Diesen Schritt von einem Ausdruck zu der Beschreibung einer
Funktion nennt man Funktionsabstraktion oder λ-Abstraktion.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
213
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: Funktionsabstraktion
214
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: Funktionsabstraktion (2)
2. Volumenberechnung eines Kegelstumpfes:
1. Quadratfunktion:
Ausdruck:
Abstraktion:
Haskell-Notation:
Formel:
Sei h die Höhe, rk , rg die Radien; dann ergibt sich das Volumen v zu
x ∗x
λx.(x ∗ x)
\ x -> (x * x)
π∗h
∗ (rk 2 + rk ∗ rg + rg 2 )
3
Haskell-Ausdruck für die rechte Seite:
v=
Vereinbarung eines Bezeichners für die Funktion:
(pi * h) / 3.0 * ( rk **2 + rk*rg + rg **2 )
quadrat = \ x -> (x * x)
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
215
©Arnd Poetzsch-Heffter
TU Kaiserslautern
216
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiele: Funktionsabstraktion (3)
Funktionsdeklaration
3. Abstraktion über Funktionsbezeichner:
Ausdruck:
Abstraktion:
In Haskell gibt es unterschiedliche syntaktische Formen für die
Funktionsdeklaration:
f (f x)
\ f x -> f (f x)
1. mittels direkter Wertvereinbarung:
Mit Bezeichnervereinbarung:
<Funktionsbez >
twice = \ f x -> f (f x)
erg
= ( twice sqrt) 3.0
Beispiel:
twice2 = \ f -> \ x -> f (f x)
erg
= ( twice sqrt) 3.0
TU Kaiserslautern
3. Funktionales Programmieren
=
<Ausdruck von Funktionstyp >
fib = \ n -> if
n == 0 then 0
else if n == 1 then 1
else fib (n -1) + fib (n -2)
Äquivalente Vereinbarung:
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
217
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Funktionsdeklaration (2)
218
3.1 Grundkonzepte funktionaler Programmierung
Funktionsdeklaration (3)
3. mittels formalen Parametern und Fallunterscheidung über
Wächtern:
<Funktionsbez > <Parameterbez1 > ...
| <boolscher Ausdruck > = <Ausdruck >
...
| <boolscher Ausdruck > = <Ausdruck >
2. mittels einem oder mehreren formalen Parametern:
<Funktionsbez > <Parameterbez1 > ...
=
<Ausdruck >
Beispiel:
Die boolschen Ausdrücke in der Deklaration heißen Wächter,
engl. guards.
fib n = if
n == 0 then 0
else if n == 1 then 1
else fib (n -1) + fib (n -2)
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
219
©Arnd Poetzsch-Heffter
TU Kaiserslautern
220
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Funktionsdeklaration (4)
3.1 Grundkonzepte funktionaler Programmierung
Funktionsdeklaration (5)
4. mittels Fallunterscheidung über Mustern:
<Funktionsbez > <Parametermuster > ... =
...
<Funktionsbez > <Parametermuster > ... =
5. mittels Kombinationen der Formen 3 und 4.
<Ausdruck >
Beispiel:
<Ausdruck >
fib
fib
|
|
Muster sind ein mächtiges Programmierkonstrukt, das weiter
unten genauer behandelt wird.
Beispiel:
0 = 0
n
n==1
= 1
otherwise = fib (n -1) + fib (n -2)
fib 0 = 0
fib 1 = 1
fib n = fib (n -1) + fib (n -2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
221
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen:
222
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (rekursive Funktionsdeklaration)
1. Einzelne rekursive Funktionsdeklaration:
• Jeder Funktionsdeklaration sollte die Funktionssignatur
vorangestellt werden und ein Kommentar, der mindestens die
Voraussetzungen an die Parameter beschreibt.
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)
Beispiel:
fib :: Integer -> Integer
-- fib k verlangt : k >= 0
fib 0 = 0
fib 1 = 1
fib n = fib (n -1) + fib (n -2)
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)
• Die Form einer Funktionsdeklaration sollte so gewählt werden,
dass die Deklaration gut lesbar ist.
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
TU Kaiserslautern
223
©Arnd Poetzsch-Heffter
TU Kaiserslautern
224
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Deklaration rekursiver Funktionen
3.1 Grundkonzepte funktionaler Programmierung
Definition: (rekursive Funktionsdekl.)
Begriffsklärung: (rekursive Definition)
Eine Funktionsdeklaration heißt direkt rekursiv, wenn der
definierende Ausdruck eine Anwendung der definierten Funktion
enthält.
Eine Definition oder Deklaration nennt man rekursiv, wenn der
definierte Begriff bzw. das deklarierte Programmelement im
definierenden Teil verwendet wird.
Eine Menge von Funktionsdeklarationen heißt verschränkt rekursiv
oder indirekt rekursiv (engl. mutually recursive), wenn die
Deklarationen gegenseitig voneinander abhängen.
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
Eine Funktionsdeklaration heißt rekursiv, wenn sie direkt rekursiv ist
oder Element einer Menge verschränkt rekursiver
Funktionsdeklarationen ist.
Datentypdeklarationen betrachten.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
225
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (rekursive Funktion)
Zur Auswertung von Funktionsanwendungen:
Eine Funktion heißt rekursiv, wenn es rekursive
Funktionsdeklarationen gibt, mit denen sie definiert werden kann.
Sei f x = A[x] ;
Eine Funktionsanwendungen f e kann nach unterschiedlichen
Strategien durch Verwendung der Deklarationsgleichungen
ausgewertet werden, zum Beispiel call-by-value:
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
226
• 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
3. Funktionales Programmieren
Begriffsklärung: (lineare/repetitive Rekursion)
Beispiele:
Vereinfachend betrachten wir hier nur Funktionsdeklarationen, bei
denen die Fallunterscheidung „außen“ und die rekursiven Aufrufe in
den Zweigen der Fallunterscheidung stehen.
• 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:
• Eine rekursive Funktionsdeklaration heißt linear rekursiv, wenn
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)
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
3. Funktionales Programmieren
fac n = facrep n 1
229
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
230
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (kaskadenartige 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
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
rekursiv, wenn sie Teilausdrücke der Form
h(. . . f (. . . ) . . . f (. . . ) . . . ) enthält.
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Begriffsklärung: (Geschachtelte Rekursion)
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
folgende Jahr ein weiteres Pärchen Nachwuchs erzeugt und
• die Kaninchen nie sterben.
231
©Arnd Poetzsch-Heffter
TU Kaiserslautern
232
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (kaskadenartige Rekursion) (2)
3.1 Grundkonzepte funktionaler Programmierung
Bemerkung:
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
• Aus Beschreibungssicht spielt die Form der Rekursion keine Rolle;
wichtig ist eine möglichst am Problem orientierte Beschreibung.
• Aus Programmierungssicht spielt Auswertungseffizienz eine
• nach dem n. Jahr:
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).
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 =
if n<=1 then 1 else ibo (n -1) + ibo (n -2)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
233
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Unterabschnitt 3.1.3
234
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.
Listen und Tupel
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
235
©Arnd Poetzsch-Heffter
TU Kaiserslautern
236
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Die Datenstruktur der Listen (2)
Typ:
Die Datenstruktur der Listen (3)
[a] , a ist Typparameter
Dem Typ [a] ist als Wertemenge die Menge aller Listen über
Elementen vom Typ a zugeordnet.
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]
3.1 Grundkonzepte funktionaler Programmierung
wenn (==) auf a definiert
Notation:
In Haskell gibt es eine vereinfachende Notation für Listen:
statt
x1 : x2 : ... : xn : []
kann man schreiben:
[ x1 , x2 , ..., xn ]
Konstanten:
[]
©Arnd Poetzsch-Heffter
:: [a]
TU Kaiserslautern
3. Funktionales Programmieren
237
3.1 Grundkonzepte funktionaler Programmierung
238
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionen auf Listen) (2)
1. Addiere alle Zahlen einer Liste vom Typ [Int] mit neutralem
Element 0:
3. Zusammenhängen zweier Listen (engl. append):
foldplus :: [Int] -> Int
foldplus xl = if null xl then 0
else (head xl) + foldplus (tail xl)
append :: [a] -> [a] -> [a]
append l1 l2 = if l1 == [] then l2
else (head l1):( append (tail l1) l2)
foldplus [1 ,2 ,3 ,4 ,5 ,6]
4. Umkehren einer Liste:
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
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: (Funktionen auf Listen)
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
239
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
3. Funktionales Programmieren
Beispiele: (Funktionen auf Listen) (3)
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
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))
• Rekursive Funktionsdeklaration sind bei Listen angemessen, weil
Listen rekursive Datenstrukturen sind.
6. Wende eine Liste von Funktionen vom Typ Int -> Int
nacheinander auf eine ganze Zahl an:
• Mit Mustern lassen sich die obigen Deklaration noch eleganter
fassen (s. unten).
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
3. Funktionales Programmieren
241
3.1 Grundkonzepte funktionaler Programmierung
242
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstrukturen der Paare (2)
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.
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
Als Typkonstruktor wird (a,b) in Mixfix-Schreibweise benutzt.
Konstanten:
Haskell stellt standardmäßig eine Datenstruktur für Paare bereit, die
bzgl. der Elementtypen parametrisiert ist.
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Die Datenstrukturen der Paare
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
keine
Dem Typ (a,b) ist die Menge der geordneten Paare mit Elementen
vom Typ a und b zugeordnet.
243
©Arnd Poetzsch-Heffter
TU Kaiserslautern
244
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Funktionen auf Paaren)
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der n-Tupel
Haskell unterstützt n-Tupel für alle n ≥ 3:
Transformiere eine Liste von Paaren in ein Paar von Listen:
Typ:
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))) )
Funktionen:
(==), (/=) :: (a, b, ...) → (a, b, ...) → Bool
wenn (==) auf a, b, ... definiert
(_,_,...) :: a → b → ... → (a, b, ...)
Konstanten:
it = unzip [(1 , 2), (3, 4) , (9, 10)]
(auch das geht erheblich schöner mit Mustern)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
(a,b,...) , a, b, ... sind Typparameter
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.
245
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen:
246
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Einheit
Haskell unterstützt eine Datenstruktur mit einem definierten Wert:
Typ:
• Es gibt keine Funkionen, um Elemente aus einem Tupel zu
()
Funktionen:
selektieren. Dafür benötigt man Muster (siehe unten).
(==), (/=) :: () → () → Bool
• Paare sind wie 2-Tupel, auf ihnen sind aber die Selektorfunktionen
Konstanten:
fst und snd definiert.
()
• Es gibt keine 1-Tupel: Klammern um Ausdrücke dienen nur der
:: ()
Dem Typbezeichner () ist eine einelementige Wertemenge
zugeordnet. Der Wert wird als Einheit (engl. unity) bezeichnet.
Strukturierung und haben darüber hinaus keine Bedeutung; d.h.
wenn e ein Ausdruck ist, ist ( e ) gleichbedeutend mit e.
Bemerkung:
Die Einheit wird oft als Ergebnis verwendet, wenn es keine relevanten
Ergebniswerte gibt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
247
©Arnd Poetzsch-Heffter
TU Kaiserslautern
248
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Geschachtelte Tupel)
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Funktionen auf n-Tupeln)
1. Flache ein Paar von Paaren in ein 4-Tupel aus:
Mit der Tupelbildung lassen sich „baumstrukturierte“Werte,
sogenannte Tupelterme, aufbauen. So entspricht der Tupelterm:
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) )
( (8,True), (("Tupel", "sind", "toll"), "aha"))
dem Baum:
it = ausflachen ( (True ,7) , (’x’ ,5.6) )
Alternative Deklaration mit Mustern:
True
8
"Tupel"
©Arnd Poetzsch-Heffter
ausflachen ((a, b) ,(c, d)) = (a, b, c, d)
"aha"
"sind"
TU Kaiserslautern
3. Funktionales Programmieren
2. Funktion zur Paarbildung:
"toll"
paarung :: a -> b -> (a,b)
paarung lk rk = (lk ,rk)
249
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionstypen:
Beispiel:
Eine zweistellige Funktion f mit Argumenten vom Typ a und Typ b und
Ergebnistyp c kann in Haskell auf zwei Arten typisiert werden:
Die Additionsoperation (+) auf Typ Int hat in Haskell den Typ
1. Gecurryte Form:
(+) :: Int -> Int -> Int
f :: a -> b -> c
In der Mathematik typisiert man die Additionsoperation plus
üblichweise mit:
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.
plus :: (Int ,Int) -> Int
Diese Variante kann man in Haskell wie folgt definieren:
Ist x::a und y::b, dann sind (f x) y oder gleichbedeutend
f x y korrekte Anwendungen.
plus ip = (fst ip) + (snd ip)
2. Tupel-Form:
Oder eleganter mit Mustern:
f :: (a,b) -> c
plus (m,n) = m + n
In diesem Fall ist für x::a und y::b, f (x,y) eine korrekte
Anwendungen.
©Arnd Poetzsch-Heffter
250
TU Kaiserslautern
251
©Arnd Poetzsch-Heffter
TU Kaiserslautern
252
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Muster in Deklarationen und Ausdrücken
Begriffsklärung: (Muster in Haskell)
Muster sind ein Sprachkonstrukt um strukturierte Werte einfacher
handhaben zu können (siehe Funktion ausflachen).
Muster (engl. Pattern) in Haskell sind Ausdrücke gebildet über
Bezeichnern, Konstanten und Konstruktoren.
Ein Wert heißt hier strukturiert, wenn er mittels Konstruktoren aus
anderen Werten zusammengebaut wurde.
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
Konstruktoren sind spezielle Haskell-Funktionen.
Bisher behandelte Konstruktoren:
• der Listkonstruktor (:) (daher der Name “cons”)
• die Tupelbildung durch (_,...,_)
M[v1 /b1 , . . . , vk /bk ] = w
In 3.1.4 werden wir benutzerdefinierte Konstruktoren kennen lernen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
253
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (ML-Muster, Passen)
254
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"] .
1. (x,y) passt auf (4,5) mit Substitution x=4, y=5.
6. ((8,x), (y,"aha")) passt
auf ((8,True), (("Tupel","sind","toll"), "aha"))
mit x = True und y = ("Tupel","sind","toll").
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] .
8
©Arnd Poetzsch-Heffter
TU Kaiserslautern
255
©Arnd Poetzsch-Heffter
True
y
TU Kaiserslautern
"aha"
256
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Wertvereinbarungen mit Mustern
3.1 Grundkonzepte funktionaler Programmierung
Funktionsvereinbarung mit Mustern
Muster können in Haskell-Wertvereinbarungen verwendet werden:
<Muster >
=
<Ausdruck > ;
Muster können in Haskell-Funktionsdeklarationen verwendet werden
(vgl. Folie 221):
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.
<Funktionsbez > <Parametermuster > ... =
...
<Funktionsbez > <Parametermuster > ... =
<Ausdruck >
<Ausdruck >
Wenn das Muster auf den Wert des Ausdrucks nicht passt, wird eine
Ausnahme erzeugt, sobald auf einen der deklarierten Bezeichner
zugegriffen wird.
Bei der Funktionsanwendung wird der Reihe nach geprüft, auf welches
Parametermuster der aktuelle Parameter passt (vgl. Folie 228).
Beispiel: (Wertvereinbarung mit Muster)
Die Gleichung zum ersten passenden Fall wird verwendet.
(x, y)
=
©Arnd Poetzsch-Heffter
(4, 5);
TU Kaiserslautern
3. Funktionales Programmieren
257
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: (Funktionsdeklaration mit Mustern)
258
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionsdeklaration mit Mustern) (2)
2. Deklaration von ist_sortiert::[Int] ->Bool mit drei Mustern:
foldplus :: [Int] -> Int
foldplus xl = if null xl then 0
else (head xl) + foldplus (tail xl)
ist_sortiert []
= True
ist_sortiert (x:[])
= True
ist_sortiert (x1:x2:xs) = if x1 <= x2
then ist_sortiert (x2:xs)
else False
Deklaration von foldplus mit Muster:
Deklaration mit drei Mustern und Wächtern:
foldplus :: [Int] -> Int
foldplus []
= 0
foldplus (x:xl) = x + foldplus xl
ist_sortiert []
ist_sortiert (x:[])
ist_sortiert (x1:x2:xs)
| x1 <= x2
| otherwise
1. Deklaration von foldplus ohne Muster:
©Arnd Poetzsch-Heffter
TU Kaiserslautern
259
©Arnd Poetzsch-Heffter
=
=
True
True
=
=
ist_sortiert (x2:xs)
False
TU Kaiserslautern
260
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiele: (Funktionsdeklaration mit Mustern) (3)
Beispiele: (Funktionsdeklaration mit Mustern) (4)
Deklaration von ist_sortiert::[Int]->Bool mit zwei Mustern und
Wächtern:
ist_sortiert (x1:x2:xs)
| x1 <= x2
=
| otherwise
=
ist_sortiert x
=
3.1 Grundkonzepte funktionaler Programmierung
4. Verwendung geschachtelter Muster:
unzip :: [(a, b)] -> ([a],[b])
unzip []
= ([], [])
unzip ((x,y):ps) = ( (x : (fst (unzip ps))),
(y : (snd (unzip ps))) )
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
3. Funktionales Programmieren
261
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
let-Ausdruck
Beispiele: (let-Ausdruck)
Der Mustermechanismus kann auch innerhalb von Ausdrücken
eingesetzt werden.
a = let a = 2*3
in a*a
Syntax des let-Ausdrucks:
b = let a = 2*3
in let (b,c) = (a,a+1)
in a*b*c
let <Liste von Deklarationen >
in <Ausdruck >
TU Kaiserslautern
3.1 Grundkonzepte funktionaler Programmierung
unzip :: [(a, b)] -> ([a], [b])
unzip []
= ([] , [])
unzip ((x,y):ps) = let (xs , ys) = unzip ps
in ((x:xs), (y:ys))
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
262
263
©Arnd Poetzsch-Heffter
TU Kaiserslautern
264
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
case-Ausdruck
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (case-Ausdruck)
Syntax des case-Ausdrucks:
case <Ausdruck0 >
of
<Muster1 > -> <Ausdruck1 >
...
<MusterN > -> <AusdruckN >
ist_sortiert xl =
case xl of
[]
-> True
(x:[])
-> True
(x1:x2:xs) -> if x1 <= x2
then ist_sortiert (x2:xs)
else False
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
3. Funktionales Programmieren
265
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen:
266
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.4
• 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)
=
Benutzerdefinierte Datentypen
\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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
268
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Benutzerdefinierte Datentypen
3.1 Grundkonzepte funktionaler Programmierung
Vereinbarung von Typbezeichnern
Haskell erlaubt es, Bezeichner für Typen zu deklarieren (vgl. F. 208):
Fast alle modernen Spezifikations- und Programmiersprachen
gestatten es dem Benutzer, „neue“ Typen zu definieren.
type IntPaar
= (Int ,Int) ;
type CharList
= [Char] ;
type Telefonbuch =
[(( String ,String ,String ,Int) ,[ String ])] ;
Übersicht:
• Vereinbarung von Typbezeichnern
type IntegerNachInteger
• Deklaration neuer Typen
=
Integer -> Integer ;
fakultaet :: IntegerNachInteger ;
-- Argument muss >= 0 sein
fakultaet = fac ;
• Summentypen
• Rekursive Datentypen
Dabei wird kein neuer Typ definiert, sondern nur ein “neuer”
Bezeichner an einen bekannten Typ gebunden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
269
3.1 Grundkonzepte funktionaler Programmierung
3.1 Grundkonzepte funktionaler Programmierung
Neue Typen werden in Haskell mit der datatype-Deklaration definiert,
die im Folgenden schrittweise erläutert wird.
Verdeutlichung benutzt werden (siehe Typ Telefonbuch).
Definition eines neuen Typs und Konstruktors:
• Zwei unterschiedliche Bezeichner können den gleichen Typ
data <NeuerTyp > =
bezeichnen; z.B.:
<Konstruktor >
<Typ1 > ... <TypN >
Die obige Datatypdeklaration definiert:
(Int ,Int ,Int)
(Int ,Int ,Int)
• einen neuen Typ und bindet ihn an <NeuerTyp>
• eine Konstruktorfunktion mit Signatur
kalenderwoche :: Date -> Int
-- Parameter muss existierenden Kalendertag sein
kalenderwoche (tag ,monat ,jahr) = ...
<Konstruktor >:: <Typ1 > -> ... -> <TypN > -> <NeuerTyp >
Die Konstruktorfunktion ist injektiv.
kalenderwoche (11 ,12 ,2003)
TU Kaiserslautern
270
Deklaration neuer Typen
• Typvereinbarungen können zur Abkürzung oder zur
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen: (Typvereinbarungen)
type IntTriple =
type Date
=
©Arnd Poetzsch-Heffter
Typ- und Konstruktorbezeichner müssen mit einem Großbuchstaben
beginnen.
271
©Arnd Poetzsch-Heffter
TU Kaiserslautern
272
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Definition von Typ, Konstruktor, Selektoren)
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
data Person = Student String String Int Int
definiert den neuen Typ Person und den Konstruktor
Student :: String -> String -> Int -> Int -> Person
Jede Datentypdeklaration definiert einen neuen Typ, d.h.
insbesondere:
Wir definieren dazu folgende Selektorfunktionen:
vorname :: Person -> String
vorname ( Student v n g m)
• die Werte des neuen Typs sind inkompatibel mit allen anderen
= v
Typen;
• auch Werte strukturgleicher benutzerdefinierter Typen sind
name :: Person -> String
name ( Student v n g m)
inkompatibel.
= 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
3. Funktionales Programmieren
273
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: (Typkompatibilität)
274
3.1 Grundkonzepte funktionaler Programmierung
Bemerkung:
1. Der Typ Person ist inkompatibel mit dem Tupeltyp
type Person2 =
(String ,String ,Int ,Int)
Den Konstruktor kann man sich als eine Markierung der Werte seines
Argumentbereichs vorstellen.
Insbesondere ist vorname ("Niels","Bohr",18851007,221) nicht
typkorrekt.
Dabei werden Werte mit unterschiedlicher Markierung als verschieden
betrachtet.
2. Person ist inkompatibel mit dem strukturgleichen Typ Adresse:
data Adresse = Wohnung String String Int Int
Konstruktoren erlauben es in gewisser Weise neue Produkttypen zu
definieren.
Insbesondere ist
name ( Wohnung " Casimirring " " Lautern " 27 67663 )
nicht typkorrekt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
275
©Arnd Poetzsch-Heffter
TU Kaiserslautern
276
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Summentypen
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (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.
1. Ein anderer Datentyp zur Behandlung von Personen:
In Haskell definiert man Summentypen durch Angabe von Alternativen
bei der datatype-Deklaration:
data <NeuerTyp > =
<Konstruktor1 >
|
<Konstruktor2 >
...
|
<KonstruktorM >
©Arnd Poetzsch-Heffter
data Person2 =
Student
String String Int Int
| Mitarbeiter String String Int Int
| Professor
String String Int Int String
<Typ1_1 > ... <Typ1_N1 >
<Typ2_1 > ... <Typ2_N2 >
<TypM_1 > ... <TypM_NM >
TU Kaiserslautern
3. Funktionales Programmieren
277
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: (Summentypen) (2)
278
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Summentypen) (3)
2. Eine benutzerdefinierte Datenstruktur für Zahlen:
data MyNumber = Intc
| Floatc
Int
Float
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)
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
280
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Begriffsklärung:
3.1 Grundkonzepte funktionaler Programmierung
Weitere Operationen auf neu deklarierten Typen:
Konstruktorfunktionen oder Konstruktoren liefern Werte des neu
definierten Datentyps. Sie können in Mustern verwendet werden (z.B.:
Student, Intc).
Konstruktoren und Selektoren erlauben das Aufbauen und Zerlegen
der Werte neu deklarierter Typen. Durch den Zusatz:
deriving (Eq ,Show)
Diskriminatorfunktionen oder Diskriminatoren prüfen, ob der Wert
eines benutzerdefinierten Datentyps zu einer bestimmten Alternative
gehört (Beispiel: isInt).
liefert Haskell auch eine standardmäßige Gleichheit und die
Möglichkeit, Werte des neuen Typs mittels print auszugeben. Zum
Beipiel:
Selektorfunktionen oder Selektoren liefern Komponenten von
Werten des definierten Datentyps (z.B.: vorname, name, . . . ).
data MyNumber = Intc
Int
| Floatc Float
deriving (Eq ,Show)
Bemerkung:
In funktionalen Sprachen kann man meist auf Selektorfunktionen
verzichten. Man verwendet stattdessen Muster/Pattern.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkung:
Haskell ermöglich es dem Benutzer auch, die Gleichheit oder andere
Operationen auf neu definierten Typen selbst zu definieren.
281
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Weitere Anwendungen der datatype-Deklaration:
3.1 Grundkonzepte funktionaler Programmierung
data Wochentag =
Montag | Dienstag | Mittwoch | Donnerstag
| Freitag | Samstag | Sonntag
deriving (Eq ,Show)
istMittwoch :: Wochentag -> Bool
istMittwoch Mittwoch = True
istMittwoch _
= False
Die Wertemenge eines Aufzählungstyps ist eine endliche Menge (von
Namen).
Oder knapper:
istMittwoch w
TU Kaiserslautern
282
Beispiel: (Aufzählungstypen)
Die datatype-Deklaration kann auch verwendet werden, um
Aufzählungstypen zu definieren, indem nur null-stellige
Konstruktoren benutzt werden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
283
©Arnd Poetzsch-Heffter
=
(w== Mittwoch )
TU Kaiserslautern
284
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Konstruktoren mit beliebiger Stelligkeit
Rekursive Datentypen
In einer Datentypdeklaration können Konstruktoren mit beliebiger
Stelligkeit kombiniert werden; z.B.:
data
MaybeInt =
|
Von großer Bedeutung in allen Paradigmen der Programmierung sind
rekursive Datentypen. Sie erlauben es insbesondere:
Nothing
Just Int
• Listen beliebiger Länge
• Bäume beliebiger Höhe
Haskell sieht dafür im Prelude den folgenden parametrisierten Typ vor
(vgl. 3.3):
data Maybe a
©Arnd Poetzsch-Heffter
=
|
behandeln zu können.
Nothing
Just a
TU Kaiserslautern
3. Funktionales Programmieren
285
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
286
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Listendatentypen)
Eine Datentypdeklaration heißt direkt rekursiv, wenn der neu
definierte Typ in einer der Alternativen der Datentypdeklaration
vorkommt.
1. Ein Datentyp für Integer-Listen:
data
Wie bei Funktionen gibt es auch verschränkt rekursive
Datentypdeklarationen.
Intlist =
Nil
| Cons Int Intlist
2. Ein Datentyp für homogene Listen mit Elementen von beliebigem
Typ:
Eine Datentypdeklaration heißt rekursiv, wenn sie direkt rekursiv ist
oder Element einer Menge verschränkt rekursiver
Datentypdeklarationen ist.
data List a
Ein Datentyp heißt rekursiv, wenn er mit einer rekursiven
Datentypdeklaration definiert wurde.
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Definition: (rekursive Datentypen)
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
|
287
©Arnd Poetzsch-Heffter
=
Nil
Cons
a (List a)
TU Kaiserslautern
288
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Baumartige Datenstrukturen:
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärungen: (zu Bäumen)
• In einem endlich verzweigten Baum hat jeder Knoten endlich
Ottmann, Widmayer:
„Bäume gehören zu den wichtigsten in der Informatik auftretenden
Datenstrukturen“.
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
3. Funktionales Programmieren
289
3.1 Grundkonzepte funktionaler Programmierung
290
3.1 Grundkonzepte funktionaler Programmierung
Definition: (Sortiertheit markierter Binärbäume)
data
IntBBaum =
Blatt Int
| Zweig Int IntBBaum IntBBaum
deriving (Eq ,Show)
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
einbaum = Zweig 7 ( Zweig 3 ( Blatt 2) (Blatt 4)) (Blatt 5)
m(k ).
• Alle Markierungen der rechten Nachkommen von k sind größer
mark :: IntBBaum -> Int
mark ( Blatt n)
= n
mark ( Zweig n lk rk) = n
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Datentyp für markierte Binärbäume:
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
als m(k ).
291
©Arnd Poetzsch-Heffter
TU Kaiserslautern
292
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Prüfung von Sortiertheit
Prüfung von Sortiertheit (2)
istsortiert :: IntBBaum
istsortiert (Blatt n)
istsortiert (Zweig n lk
istsortiert lk &&
( maxmark lk)<n &&
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.
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
minmark ( Blatt n)
= n
minmark ( Zweig n lk rk) =
n `min` ( minmark lk `min` minmark rk)
TU Kaiserslautern
3. Funktionales Programmieren
-> Bool
= True
rk) =
istsortiert rk &&
n<( minmark rk)
result = istsortiert einbaum
maxmark , minmark :: IntBBaum -> Int
maxmark ( Blatt n)
= n
maxmark ( Zweig n lk rk) =
n `max` ( maxmark lk `max` maxmark rk)
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
• minimale Markierung des Baums
• maximale Markierung des Baums
293
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Prüfung von Sortiertheit (3)
Begriffsklärung: (Weitere Begriffe zu Bäumen)
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
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
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)
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.
istsortiert b = let (srtflag ,_ ,_) = ( istsortiert3 b)
in srtflag
©Arnd Poetzsch-Heffter
TU Kaiserslautern
294
Die Größe eines Baums ist die Anzahl seiner Knoten.
295
©Arnd Poetzsch-Heffter
TU Kaiserslautern
296
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Datentyp möglicherweise leerer Binärbäume:
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)
data IntBBaum2 =
Leer
| Knoten Int IntBBaum2 IntBBaum2
• 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
3. Funktionales Programmieren
297
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Bäume mit variabler Kinderzahl: (2)
298
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
• Bäume mit variabler Kinderzahl lassen sich z.B. zur
zaehleKnVBaum
:: VBaum -> Int
zaehleKnVBaumLst :: [ VBaum ] -> Int
zaehleKnVBaum (Kn _ xs) =
©Arnd Poetzsch-Heffter
Repräsentation von Syntaxbäumen verwenden, indem das
Terminal- bzw. Nichtterminalsymbol als Markierung verwendet
wird.
1 + ( zaehleKnVBaumLst xs)
Besser ist es allerdings, die Information über die Symbole mittels
Konstruktoren auszudrücken (vgl. nächstes Beispiel).
zaehleKnVBaumLst []
= 0
zaehleKnVBaumLst (x:xs) =
( zaehleKnVBaum x) + ( zaehleKnVBaumLst xs)
• 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
299
©Arnd Poetzsch-Heffter
TU Kaiserslautern
300
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (abstrakte Syntaxbäume)
Beispiel: (abstrakte Syntaxbäume) (2)
Der abstrakte Syntaxbaum eines Programms repräsentiert die
Programmstruktur unter Verzicht auf Schlüsselworte und
Trennzeichen.
Das Femto-Programm
Rekursive Datentypen eignen sich sehr gut zur Beschreibung von
abstrakter Syntax.
a = 73;
main = print ( a + 12 )
Als Beispiel betrachten wir die abstrakte Syntax von Femto:
Wird durch folgenden Baum repräsentiert:
data Programm
deriving
data Wertdekl
deriving
data Ausdruck
Prog
= 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
3. Funktionales Programmieren
[Dekl "a" (Zahl 73)]
(Add (Bzn "a") (Zahl 12))
Die Baumrepräsentation eignet sich besser als die
Zeichenreihenrepräsentation zur weiteren Verarbeitung von
Programmen.
301
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Verschränkte Datentypdeklarationen:
302
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
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.
= 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
303
©Arnd Poetzsch-Heffter
TU Kaiserslautern
304
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Unendliche Datenobjekte:
Begriffsklärung: (Strom)
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.
3. Funktionales Programmieren
In Haskell kann man den Listendatentyp zur Realisierung von Strömen
verwenden.
• Lesen des ersten Elements (head)
• Entfernen des ersten Elements (tail)
• Prüfen, ob noch Elemente im Strom vorhanden sind
Haskell unterstützt unendliche Liste und andere unendliche
Datenobjekte.
TU Kaiserslautern
Potenziell unendliche Listen werden meist als Ströme bezeichnet.
Typische Operationen:
Eine unendliche Liste besitzt keine natürlichzahlige Länge und wird
durch Anwendung von tail nicht kürzer.
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
Beispiel:
Strom von Eingabedaten
305
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiel: (unendliche Liste)
306
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.5
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)
Ein- und Ausgabe
natlist = 0:(incr natlist )
natlist2 = [0 ..]
©Arnd Poetzsch-Heffter
TU Kaiserslautern
307
©Arnd Poetzsch-Heffter
TU Kaiserslautern
308
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Ein- und Ausgabe
3.1 Grundkonzepte funktionaler Programmierung
Der Typ IO a
Der Haskell Prelude stellt Funktionen zur Verfügung, um
• Werte auf der Standardausgabe auszugeben und
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
• aus Dateien zu lesen und in Dateien zu schreiben.
IO a
• Werte von der Standardeingabe zu lesen,
Problem:
In einer rein funktionalen Sprache wie Haskell haben Funktionen keine
Seiteneffekte. Deshalb spielt auch die Reihenfolge des Aufrufs eine
weniger wichtige Rolle.
Zum Beispiel hat die Funktion putStr zur Ausgabe einer Zeichenreihe
die Signatur:
Ein- und Ausgabe erzeugen einen Seiteneffekt auf die
Programmumgebung. Sie müssen in der richtigen Reihenfolge
ausgeführt werden.
Zeichenreihe in die Standardausgabe (erzeugt also einen
IO-Seiteneffekt) und liefert die Einheit als Ergebnis.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
309
putStr :: String -> IO ()
putStr nimmt eine Zeichenreihe als Argument, schreibt die
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Der Typ IO a (2)
310
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:
Wir nennen Funktionen und Konstanten mit Ergebnistyp IO a
IO-Aktionen.
do {
Anw1 ;
...
AnwN ;
IO - Aktion
}
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.
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
311
©Arnd Poetzsch-Heffter
TU Kaiserslautern
312
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Anweisungssequenz)
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Anweisungssequenz) (2)
main = do {
hSetBuffering stdout NoBuffering ; -- import System .IO
putStr "Gib eine Zeichenreihe ein: ";
s <- getLine ;
putStr " Revertierte Zeichereihe : ";
putStrLn ( reverse s)
}
Ausgabe aller Elemente einer String-Liste:
printStringList :: [ String ] -> IO ()
printStringList []
= putStr ""
printStringList (s:xs) =
do
putStr s
printStringList xs
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
3. Funktionales Programmieren
313
3.1 Grundkonzepte funktionaler Programmierung
314
3.1 Grundkonzepte funktionaler Programmierung
Dateiein- und -ausgabe
Bisher haben wir nur IO-Aktion für Zeichenreihen kennen gelernt.
Zum Lesen aus und Schreiben in Dateien stellt Haskell u. a. die
IO-Aktionen:
Haskell unterstützt im Prelude und Bibliotheken viele weitere
IO-Aktionen. Z. B.:
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
getChar :: IO Char
putChar :: Char -> IO ()
Dabei ist FilePath eine Zeichenreihe, die einen Dateinamen
bezeichnet:
Und für einen lesbaren und anzeigbaren Typ a die parametrischen
IO-Aktionen:
type FilePath = String
readLn :: IO a
print :: a -> IO ()
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Parametrisierte IO-Aktionen
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
315
©Arnd Poetzsch-Heffter
TU Kaiserslautern
316
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Unterabschnitt 3.1.6
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Modulsystem)
Ein Programmmodul fasst mehrere Deklarationen zusammen und
stellt sie unter einem Namen zur Verfügung.
Module
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
Ein Programmmodul sollte Programmierern eine Modul-Schnittstelle
bereitstellen, die unabhängig von der Implementierung dokumentiert
und benutzbar ist.
317
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Module in Haskell
318
3.1 Grundkonzepte funktionaler Programmierung
Begriffsklärung: (Programm, die 2.)
Vereinfacht dargestellt, hat ein Haskell-Modul die Form:
module <Modulname > ( <kommagetrennte Liste
exportierter Programmelemente >
) where
import <Modul1 >
...
import <Moduln >
<Deklarationen des Moduls >
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.
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
320
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Haskell-Module)
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Haskell-Module) (2)
module Breg where
module Alster where
import Alster
avalue = 7;
data Broterwerb = Designer | Maler | Bildhauer
data Art = Painting | Design | Sculpture
beruf
beruf
beruf
beruf
ache Design = False
ache _
= True
:: Art ->
Design
Painting
Sculpture
Broterwerb
= Designer
= Maler
= Bildhauer
bflag = (ache Painting )
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
321
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Haskell-Module) (3)
Beispiel: (Modul-Schnittstelle)
module Main where
module Environment (Env , emptyEnv , insertI , insertB ,
lookUp , delete , IBValue (Intv , Boolv , None)
) where
emptyEnv :: Env
-- leere Bezeichnerumbegung
import Breg
main = print bflag
insertI :: String -> Int -> Env -> Env
-- ( insertI bez i e) traegt die Bindung (bez ,i)
-- in die Umgebung e ein
Beachte:
Programmelemente aus Alster, z.B. avalue, sind nicht sichtbar in
Modul Main.
©Arnd Poetzsch-Heffter
322
TU Kaiserslautern
insertB :: String -> Bool -> Env -> Env
-- ( insertB bez b e) traegt die Bindung (bez ,b)
-- in die Umgebung e ein
323
©Arnd Poetzsch-Heffter
TU Kaiserslautern
324
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Modul-Schnittstelle) (2)
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Modul-Implementierung)
-- Modulimplementierung ( Fortsetzung von Environment )
data IBValue = Intv Int
| Boolv Bool
| None
deriving (Eq , Show)
lookUp :: String -> Env -> IBValue
-- ( lookUp bez e) liefert den Wert v der ersten
-- gefundenen Bindung (bez ,v) mit Bezeichner bez
type Env = [ (String , IBValue ) ]
delete :: String -> Env -> Env
-- ( delete bez e) loescht alle Bindungen (bez ,_)
-- mit Bezeichner bez
emptyEnv = []
insertI bez i e = (bez ,Intv i):e
insertB bez b e = (bez ,Boolv b):e
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
325
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiel: (Modul-Implementierung) (2)
326
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.7
lookUp bez []
= None
lookUp bez ((bz ,val):e)
| bez == bz = val
| otherwise = lookUp bez e
Zusammenfassung von 3.1
delete bez []
= []
delete bez ((bz ,val):e)
| bez == bz = delete bez e
| otherwise = (bz ,val):( delete bez e)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
327
©Arnd Poetzsch-Heffter
TU Kaiserslautern
328
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Zusammenfassung von 3.1
3.2 Algorithmen auf Listen und Bäumen
Abschnitt 3.2
Begriffe und Sprachmittel wie Ausdruck, Bezeichner, Vereinbarung,
Wert, Typ, Muster, Modul, . . . .
Wichtige Programmier- und Modellierungskonzepte:
Algorithmen auf Listen und Bäumen
• Basisdatenstrukturen
• rekursive Funktionen
• rekursive Datentypen (insbesondere Listen)
• Ein- und Ausgabe
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
329
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Algorithmen auf Listen und Bäumen
330
3.2 Algorithmen auf Listen und Bäumen
Lernziele:
• Intuitiver Algorithmusbegriff
• Kenntnis wichtiger/klassischer Algorithmen und
Algorithmenklassen
Sortieren und Suchen sind elementare Aufgaben, die in den meisten
Programmen anfallen.
• Zusammenhang Algorithmus und Datenstruktur
Verfahren zum Suchen und Sortieren spielen eine zentrale Rolle in der
Algorithmik.
• 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
331
©Arnd Poetzsch-Heffter
TU Kaiserslautern
332
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Unterabschnitt 3.2.1
Sortieren
Sortieren ist eine Standardaufgabe, die Teil vieler speziellerer,
umfassenderer Aufgaben ist.
Sortieren
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Untersuchungen zeigen, dass „mehr als ein Viertel der kommerziell
verbrauchten Rechenzeit auf Sortiervorgänge entfällt“
(Ottmann, Widmayer: Algorithmen und Datenstrukturen, Kap. 2).
333
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Begriffsklärung: (Sortierproblem)
334
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
Gegeben ist eine Folge s1 , . . . , sN von sogenannten Datensätzen.
Offene Aspekte der Formulierung des Sortierproblem:
Jeder Satz sj hat einen Schlüssel kj . Wir gehen davon aus, dass die
Schlüssel ganzzahlig sind.
• Was heißt, eine Folge ist „gegeben“?
Aufgabe des Sortierproblems ist es, eine Permutation π zu finden, so
dass die Umordnung der Sätze gemäß π folgende Reihenfolge auf den
Schlüsseln ergibt:
• Ist der Bereich der Schlüssel bekannt?
• Welche Operationen stehen zur Verfügung, um π zu bestimmen?
• Was genau heißt „Umordnung“?
kπ(1) ≤ kπ(2) ≤ · · · ≤ kπ(N)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
335
©Arnd Poetzsch-Heffter
TU Kaiserslautern
336
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Aufgabenstellung:
Aufgabenstellung: (2)
Wir benutzen Datensätze folgenden Typs
Entwickle eine Funktion
type Dataset = (Int , String )
sort :: [ Dataset ] -> [ Dataset ]
mit Vergleichsfunktion:
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).
leq:: Dataset -> Dataset -> Bool
leq (kx , dx) (ky , dy) = (kx <=ky)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
337
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Aufgabenstellung: (3)
338
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Auswahl (selection sort)
Wir betrachten:
• Sortieren durch Auswahl (engl. selection sort)
Algorithmische Idee:
• Sortieren durch Einfügen (engl. insertion sort)
• Entferne einen minimalen Eintrag min aus der Liste.
• Bubblesort
• Sortiere die Liste, aus der min entfernt wurde.
• Füge min als ersten Element an die sortierte Liste an.
• Sortieren durch rekursives Teilen (quick sort)
• Sortieren durch Mischen (merge sort)
• Heapsort
©Arnd Poetzsch-Heffter
TU Kaiserslautern
339
©Arnd Poetzsch-Heffter
TU Kaiserslautern
340
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Auswahl (selection sort) (2)
Sortieren durch Auswahl (selection sort) (3)
select :: Dataset -> [ Dataset ] -> Dataset
-- Hilfsfunktion : liefert einen minimalen Eintrag der
-- Liste bzw. x, falls x minimal
delete :: Dataset -> [ Dataset ] -> [ Dataset ]
-- Hilfsfunktion : loescht ein Vorkommen von x aus der
-- Liste , falls solches vorhanden
select x []
= x
select x (y:yl) = if x `leq` y then select x yl
else select y yl
delete x []
= []
delete x (y:yl) = if (x==y) then
else
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
341
3.2 Algorithmen auf Listen und Bäumen
342
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Einfügen (insertion sort)
selectionsort :: [ Dataset ] -> [ Dataset ]
-- Sortieren durch Auswahl
-mnm: ein minimaler Eintrag in Liste xl
-rest: die Liste xl ohne min
Algorithmische Idee:
• Sortiere zunächst den Rest der Liste.
selectionsort []
= []
selectionsort (x:xl) =
let mnm = select x xl
rest = delete mnm (x:xl)
in
mnm : ( selectionsort rest)
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Sortieren durch Auswahl (selection sort) (4)
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
yl
y:( delete x yl)
• Füge dann den ersten Eintrag in die sortierte Liste ein.
343
©Arnd Poetzsch-Heffter
TU Kaiserslautern
344
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Sortieren durch Einfügen (insertion sort) (2)
Sortieren durch Einfügen (insertion sort) (3)
insert :: Dataset -> [ Dataset ] -> [ Dataset ]
-- Hilfsfunktion : fuegt Argument in sortierte Liste ein
-- Ergebnis : sortierte Liste
insertionsort :: [ Dataset ] -> [ Dataset ]
-- Sortieren durch Einfuegen
insert x []
=
insert x (y:yl) =
insertionsort []
= []
insertionsort (x:xl) = insert x ( insertionsort xl)
©Arnd Poetzsch-Heffter
[x]
if (x `leq` y)
then x : (y:yl)
else y : ( insert x yl)
TU Kaiserslautern
3. Funktionales Programmieren
345
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bubblesort
Bubblesort (2)
Algorithmische Idee:
bubble :: [ Dataset ] -> Dataset -> [ Dataset ]
-> ([ Dataset ], Dataset )
-- Hilfsfunktion : liefert einen maximalen Eintrag der
-- Liste und die Liste ohne den maximalen Eintrag
• 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
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
• Sortiere die Liste ohne mxe und hänge mxe an.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
346
347
©Arnd Poetzsch-Heffter
TU Kaiserslautern
348
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Bubblesort (3)
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
bubblesort :: [ Dataset ] -> [ Dataset ]
-- Sortieren durch Herausschieben der maximalen
-Elemente
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
bubblesort []
= []
bubblesort (x:xl) = let (rl ,mxe) = bubble [] x xl
in ( bubblesort rl)++[ mxe]
• Wende quicksort rekursiv auf die Teillisten an.
• Hänge die resultierenden Listen und das Pivotelement zusammen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
349
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Quicksort: Sortieren durch Teilen (2)
Quicksort: Sortieren durch Teilen (3)
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
qsort :: [ Dataset ] -> [ Dataset ]
-- Sortieren nach der Strategie ``Teile und Herrsche ’’
split p
split p
let
in
qsort []
= []
qsort (p:rest) = let (below ,above) = split p rest
in (qsort below) ++ [p] ++ (qsort above )
[]
= ([] ,[])
(x:xr) =
(blw ,abv) = split p xr
if p `leq` x then (blw ,x:abv)
else (x:blw ,abv)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
350
351
©Arnd Poetzsch-Heffter
TU Kaiserslautern
352
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Bemerkung:
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).
Quicksort ist ein typischer Algorithmus gemäß der
Divide-and-Conquer-Strategie:
• Teile die Liste in zwei Teile der Länge halfsize (+1).
• Zerlege das Problem in Teilprobleme.
• Sortiere die Teile.
• Wende den Algorithmus auf die Teilprobleme an.
• Mische die Teile zusammen.
• Füge die Ergebnisse 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
3. Funktionales Programmieren
353
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
Bemerkung: (2)
merge :: [ Dataset ] -> [ Dataset ] -> [ Dataset ]
-- Hilfsfunktion : mischt zwei sortierte Listen
-- zu einer sortierten Liste zusammen
mergesort :: [ Dataset ] -> [ Dataset ]
-- Sortieren durch Mischen
-halfsize : Haelfte der Listenlaenge
-front :
Vordere Haelfte der Liste
-back :
Hintere Haelfte der Liste
merge
merge
merge
merge
[] []
[] yl
xl []
(x:xl) (y:yl)
©Arnd Poetzsch-Heffter
=
=
=
=
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)
[]
yl
xl
if (x `leq` y)
then x : (merge xl (y:yl))
else y : (merge (x:xl) yl)
TU Kaiserslautern
354
355
©Arnd Poetzsch-Heffter
TU Kaiserslautern
356
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Heapsort
3.2 Algorithmen auf Listen und Bäumen
Bemerkung:
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:
• Der Begriff „Heap“ist in der Informatik überladen.
• 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
3. Funktionales Programmieren
• Ziele des Vorgehens:
I Beispiel für komplexe, abstrakte Datenstruktur
I Zusammenhang der algorithmischen Idee und der Datenstruktur.
Auch der Speicher für zur Laufzeit angelegte Variablen wird im
Englischen „heap“genannt.
357
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Begriffsklärung: (zu Bäumen)
358
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,
Wir betrachten im Folgenden Binärbäume, die
• jeder Knoten mit einer Tiefe kleiner h − 2 zwei nicht-leere
• entweder leer sind oder
Unterbäume hat,
• für die Knoten K des Niveaus h − 2 gilt:
• aus einem markierten Knoten mit zwei Unterbäumen bestehen.
Ein Blatt ist dann ein Knoten mit zwei leeren Unterbäumen.
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 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
Ein Baum der Größe n heißt indiziert, wenn man seine Knoten mittels
der Indizes 0, . . . , n − 1 ansprechen kann.
359
©Arnd Poetzsch-Heffter
TU Kaiserslautern
360
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Bemerkung:
3.2 Algorithmen auf Listen und Bäumen
Modulschnittstelle für fast vollständige, markierte,
indizierte Binärbäume:
Im Folgenden gehen wir bei indizierten Bäumen immer davon aus,
dass die Reihenfolge der Indices einem Breitendurchlauf folgt (siehe
Beispiel).
module FvBintree (FVBintree ,create ,size ,get ,swap ,
removeLast ,hasLeft ,hasRight ,left ,right ) where
Beispiel: (Fast vollst., indizierter und markierter Binärbaum)
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
3. Funktionales Programmieren
361
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Modulschnittstelle für fast vollständige, markierte,
indizierte Binärbäume: (2)
362
3.2 Algorithmen auf Listen und Bäumen
Modulschnittstelle für fast vollständige, markierte,
indizierte Binärbäume: (3)
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
left :: FVBintree -> Int -> Int
-- Liefert Index des linken Kinds von Knoten mit Index i
-- 0 <= i < size b && hasLeft i
hasLeft :: FVBintree -> Int -> Bool
-- Knoten mit Index i hat linkes Kind
-- 0 <= i < size b
right :: FVBintree -> Int -> Int
-- Liefert Index des rechten Kinds von Knoten mit Index i
-- 0 <= i < size b && hasRight i
hasRight :: FVBintree -> Int -> Bool
-- Knoten mit Index i hat rechtes Kind
-- 0 <= i < size b
©Arnd Poetzsch-Heffter
TU Kaiserslautern
363
©Arnd Poetzsch-Heffter
TU Kaiserslautern
364
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Vorgehen
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.
• 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
• Abstrakte Datenstrukturen werden über ihre Schnittstelle
Struktur/Implementierung zu kennen.
benutzt. Entwicklung und Benutzung von solchen Schnittstellen ist
ein zentraler Bestandteil der SW-Entwicklung.
• 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
3. Funktionales Programmieren
• Eine abstrakte Datenstruktur kann unterschiedliche
Implementierungen haben. Implementierungen können
ausgetauscht werden, ohne dass der Nutzer seine Programme
ändern muss!
365
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Begriffsklärung: (Heap)
366
3.2 Algorithmen auf Listen und Bäumen
Beispiel: (Heap)
Heap der Größe 8:
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
368
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Bemerkung:
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 garantiert, dass der Schlüssel eines Knotens M
größer gleich aller Schlüssel in den Unterbäumen von M ist.
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.
Insbesondere steht in der Wurzel ein Element mit einem maximalen
Schlüssel.
• 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
3. Funktionales Programmieren
369
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Beispiel: (Versickern lassen)
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
370
3.2 Algorithmen auf Listen und Bäumen
Beispiel: (Versickern lassen) (2)
371
©Arnd Poetzsch-Heffter
TU Kaiserslautern
372
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Beispiel: (Versickern lassen) (3)
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
3. Funktionales Programmieren
373
3.2 Algorithmen auf Listen und Bäumen
374
3.2 Algorithmen auf Listen und Bäumen
Konkretisierung des Heapsort-Algorithmus:
rechten Unterbaum
1. Schritt:
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
3. Funktionales Programmieren
Funktion heapify formuliert auf Basis von FvBintree:
(2)
else -- hat linken und
-- oder ist Blatt
if hasRight b ix
then let lx
=
rx
=
largerKid =
©Arnd Poetzsch-Heffter
TU Kaiserslautern
375
• 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
3. Funktionales Programmieren
Konkretisierung des Heapsort-Algorithmus: (2)
Heapsort: Abstrakte Version
2. Schritt:
Wir betrachten zunächst Heapsort auf Basis des abstrakten Datentyps
für markierte, fast vollständige, indizierte Binärbaume:
• Schreibe den Wurzel-Datensatz in die Ausgabe.
• Schreibe den Datensatz des letzten Elementes in den
heapifyAll :: FVBintree -> FVBintree
hpfyEmb
:: FVBintree -> Int -> FVBintree
-- Hilfsfunktionen fuer den ersten Schritt
Wurzelknoten (swap)
• Entferne das letzte Element.
• Stelle die Heap-Eigenschaft wieder her.
heapifyAll b = hpfyEmb b (( size b) `div` 2)
• Fahre mit Schritt 2 fort, solange die Größe > 0.
hpfyEmb b 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
3. Funktionales Programmieren
377
then b
else heapify b 0
hpfyEmb b ix = hpfyEmb ( heapify b ix) (ix -1)
if size b == 0
TU Kaiserslautern
3. Funktionales Programmieren
378
3.2 Algorithmen auf Listen und Bäumen
Bemerkungen:
-- heapsort : sortiert gegebene Liste
heapsort :: [ Dataset ] -> [ Dataset ]
• Wie wir in Kapitel 4 zeigen, profitiert Heapsort davon, dass sich
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)
TU Kaiserslautern
=
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
Heapsort: Abstrakte Version (2)
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
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).
379
©Arnd Poetzsch-Heffter
TU Kaiserslautern
380
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Bemerkungen: (2)
3.2 Algorithmen auf Listen und Bäumen
Unterabschnitt 3.2.2
In den folgenden Kapiteln werden wir demonstrieren,
• wie einige der obigen Algorithmen in anderen
Suchen
Programmierparadigmen formuliert werden können;
• wie der Effizienz/Komplexitätsbegriff präzisiert werden kann.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
381
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Suchen
382
3.2 Algorithmen auf Listen und Bäumen
Schnittstell zum Suchen
Wir betrachten die folgende Schnittstelle:
module Dictionary (Dict ,emptyDict ,get ,put , remove ) where
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 ;
type Dict = STree
-- type des Dictionarys
emptyDict :: Dict
-- leeres Dictionary
get :: Dict -> Int -> (Bool , String )
-- Nachschauen des Eintrags zu Schluessel i
• 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.
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
383
©Arnd Poetzsch-Heffter
TU Kaiserslautern
384
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Bemerkung:
3.2 Algorithmen auf Listen und Bäumen
Begriffsklärung: (Binärer Suchbaum)
In der Literatur zur funktionalen Programmierung wir „get“ oft „lookup“
oder „search“, „put“ oft „insert“ und „remove“ oft „delete“genannt.
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:
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:
• 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 .
• lineare Datenstrukturen (Übung)
• (natürliche) binäre Suchbäume (Vorlesung)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
385
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkung:
386
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:
• „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).
data STree =
Node Dataset STree STree
Empty
deriving (Eq , Show)
• In einem binären Suchbaum gibt es zu einem Schlüssel maximal
einen Knoten mit entsprechender Markierung.
|
emptyDict = Empty
Die Konstante emptyDict repräsentiert das leere Dictionary.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
387
©Arnd Poetzsch-Heffter
TU Kaiserslautern
388
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Invariante für Suchbäume:
3.2 Algorithmen auf Listen und Bäumen
Suchen eines Eintrags:
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.
Wenn kein Eintrag zum Schlüssel existiert, liefere (False,""); sonst
liefere (True,s), wobei s der String zum Schlüssel ist:
Die Funktionen müssen garantieren, dass die Eigenschaft auch für
Ergebnisse gilt.
get
get
|
|
|
Man sagt:
Die Suchbaum-Eigenschaft ist eine Datenstrukturinvariante von
Dictionaries.
Empty k
= (False ,"")
(Node (km ,s) l r) k
k < km
=
get l k
km < k
=
get r k
otherwise =
(True ,s)
Wir guarantieren die Datenstrukturinvariante u.a. dadurch, dass wir
Nutzern des Moduls Dictionary keinen Zugriff auf die Konstruktoren
geben.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
389
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Einfügen eines Eintrags:
390
3.2 Algorithmen auf Listen und Bäumen
Beispiel:
Algorithmisches Vorgehen:
Einfügen von 33:
• 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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
392
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Implementierung von put:
3.2 Algorithmen auf Listen und Bäumen
Bemerkungen:
• Die Reihenfolge des Einfügens bestimmt das Aussehen des
Die algorithmische Idee lässt sich direkt umsetzen.
binären Suchbaums:
Beachte aber, dass das Dictionary nicht verändert wird, sondern ein
neues erzeugt und abgeliefert wird:
Reihenfolgen:
2;3;1
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
1;2;3
Empty Empty
r
(put l k s) r
l (put r k s)
TU Kaiserslautern
3. Funktionales Programmieren
1;3;2
393
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen: (2)
394
3.2 Algorithmen auf Listen und Bäumen
Löschen:
Löschen ist die schwierigste Operation, da
• Es gibt sehr viele Möglichkeiten, aus einer vorgegebenen
• ggf. innere Knoten entfernt werden und dabei
Schlüsselmenge einen binären Suchbaum zu erzeugen.
• die Suchbaum-Eigenschaft erhalten werden muss.
• Bei sortierter Einfügereihenfolge entartet der binäre Suchbaum
zur linearen Liste.
Algorithmisches Vorgehen:
• Der Algorithmus zum Einfügen ist schnell, insbesondere weil
• Die Position eines zu löschenden Knotens K mit Schlüssel X wird
keine Ausgleichs- oder Reorganisationsoperationen
vorgenommen werden müssen.
nach dem gleichen Verfahren wie beim Suchen eines Knotens
bestimmt.
• Dann sind drei Fälle zu unterscheiden:
©Arnd Poetzsch-Heffter
TU Kaiserslautern
395
©Arnd Poetzsch-Heffter
TU Kaiserslautern
396
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
Löschen: (2)
Löschen: (3)
1. Fall: K ist ein Blatt.
2. Fall: K mit Schlüssel X hat genau einen Unterbaum.
Lösche K :
K wird im Eltern-Knoten durch sein Kind ersetzt und gelöscht:
Entsprechend, wenn Knoten mit Schlüssel X in rechtem Unterbaum.
Die anderen links-rechts-Varianten entsprechend.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
397
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Löschen: (4)
398
3.2 Algorithmen auf Listen und Bäumen
Löschen: (5)
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
400
3. Funktionales Programmieren
3.2 Algorithmen auf Listen und Bäumen
3. Funktionales Programmieren
Umsetzung in Haskell
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
• 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
3.2 Algorithmen auf Listen und Bäumen
removemin (Node d Empty r) = (d,r)
removemin (Node d l r) =
let (mnm ,ll) = removemin l
in (mnm , Node d ll r)
mnm der kleinste Datensatz in b ist und
br der Baum ist, der sich durch Löschen von mnm aus b ergibt.
TU Kaiserslautern
3. Funktionales Programmieren
401
©Arnd Poetzsch-Heffter
3.2 Algorithmen auf Listen und Bäumen
TU Kaiserslautern
3. Funktionales Programmieren
Umsetzung in Haskell (3)
402
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
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
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
403
©Arnd Poetzsch-Heffter
TU Kaiserslautern
404
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Abschnitt 3.3
3.3 Polymorphie und Funktionen höherer Ordnung
Abstraktion mittels Polymorphie und Funktionen
höherer Ordnung
Überblick:
• Grundbegriffe der Typisierung
Polymorphie und Funktionen höherer Ordnung
• Polymorphie als Abstraktionsmittel
• Typsysteme und Typinferenz
• Einführung in Funktionen höherer Ordnung
• Wichtige Funktionen höherer Ordnung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
405
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Unterabschnitt 3.3.1
406
3.3 Polymorphie und Funktionen höherer Ordnung
Typisierung
Inhalte:
• Was ist ein Typ?
• Ziele der Typisierung
Typisierung
• Polymorphie und parametrische Typen
• Typsystem von Haskell und Typinferenz
Fast alle modernen Spezifikations- und Programmiersprachen
besitzen ein Typsystem.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
407
©Arnd Poetzsch-Heffter
TU Kaiserslautern
408
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Was ist ein Typ?
3.3 Polymorphie und Funktionen höherer Ordnung
Was ist ein Typ? (2)
• Parametrische Typen beschreiben bestimmte Eigenschaften und
lassen andere offen; z.B.:
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
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]
Elemente ( Tupel-, Listen- und Funktionstypen).
somefun p = let (fstc ,sndc) = p
in fstc : sndc
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
409
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Ziele der Typisierung
410
3.3 Polymorphie und Funktionen höherer Ordnung
Zentrale Idee:
• Für jeden Ausdruck und jede Funktion wird ein Typ festgelegt.
Die Typisierung verfolgt drei Ziele:
• Prüfe, ob die Typen der aktuellen Parameterausdrücke mit der
Signatur der angewendeten Funktion übereinstimmen.
• Automatische Erkennung von Programmierfehlern (durch
Übersetzer, Interpreter);
Beispiele: (Typprüfung von Ausdrücken)
• Verbessern der Lesbarkeit von Programmen;
f :: Int -> Int
• Ermöglichen effizienterer Implementierungen.
dann sind:
Wir konzentrieren uns hier auf das erste Ziel.
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
411
©Arnd Poetzsch-Heffter
TU Kaiserslautern
412
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Bemerkung:
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:
Typisierung war lange Zeit nicht unumstritten.
Hauptgegenargumente sind:
Aufgabe 1:
Schreibe eine Funktion frp:
• zusätzlicher Schreib- und Entwurfsaufwand
• Eingabe: Liste von Paaren entweder vom Typ
(Bool,Int) oder (Bool,Float)
• Einschränkung der Freiheit:
I inhomogene Listen
I Nutzen der Repräsentation von Daten im Rechner
• 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
3. Funktionales Programmieren
413
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiel: (2)
414
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel: (3)
Realisierung in Haskell-Notation (kein Haskell-Programm, da nicht
typkorrekt!):
Aufgabe 2:
Schreibe eine Funktion,
frp :: ( Bool , ? ) -> ?
• die ein n-Tupel (n≥2) nimmt und
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
• die erste Komponente des Tupels liefert.
Kann in Haskell nicht definiert werden:
arbitraryfst :: (a,b,...) -> a
arbitraryfst (n,...) = n
415
©Arnd Poetzsch-Heffter
TU Kaiserslautern
416
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Polymorphie und parametrische Typen
Begriffsklärung: (polymorphes Typsystem)
Programmierer möchten vom Typsystem nicht weiter eingeengt
werden als nötig. Ziel:
Das Typsystem einer Sprache S beschreibt,
• mächtige und flexible Typsysteme
• welche Typen es in S gibt bzw. wie neue Typen deklariert werden;
• insbesondere Polymorphie und Parametrisierung
• wie den Ausdrücken von S ein Typ zugeordnet wird;
• welche Regeln typisierte Ausdrücke erfüllen müssen.
Im Allg. bedeutet Polymorphie Vielgestaltigkeit.
Ein Typsystem heißt polymorph, wenn Ausdrücke zu Werten bzw.
Objekten unterschiedlichen Typs ausgewertet werden können.
In der Programmierung bezieht sich Polymorphie auf die Typisierung
bzw. das Typsystem.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
417
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkung:
418
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:
• Man unterscheidet:
I Parametrische Polymorphie
I Subtyp-Polymorphie (vgl. Typisierung in Java)
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).
• Oft spricht man im Zusammenhang mit der Überladung von
Beispiel:
Der Typ [Int] ist spezieller als der parametrische Typ [a] .
Insbesondere gilt:
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
Jeder Wert vom Typ [Int] kann überall dort benutzt werden, wo
ein Wert vom Typ [a] erwartet wird.
419
©Arnd Poetzsch-Heffter
TU Kaiserslautern
420
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Typsystem von Haskell und Typinferenz
Beispiele: (Typausdrücke)
Typen werden in Haskell durch Typausdrücke beschrieben:
Int ,
• Typkonstanten sind die Basisdatentypen: Bool, Char,
Int, Integer, Float, Double
a,
• Typvariablen: a, meineTypvar, gTyp
(Char -> Char) -> (Int -> Int)
(Eq a) => a -> [a] -> Bool
Hinweis:
-> ist rechtsassoziativ.
421
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
422
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiele:
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.
[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.
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.
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Begriffserklärung: (Vergleich von Typen)
©Arnd Poetzsch-Heffter
(Int ,a,b,a)
Int -> Int , [a] -> [( Float , b, a)]
Einem Typausdruck TA kann ein Typconstraint für Typvariablen a
vorangestellt werden: (Eq a) => TA
3. Funktionales Programmieren
b
[ Integer ] , [a] , [( Float , b, a)]
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
TU Kaiserslautern
Bool
(Int ,Bool) , (Int ,a) ,
• Ausdrücke gebildet mit Typkonstruktoren:
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
423
(Eq a) => TA
©Arnd Poetzsch-Heffter
ist spezieller als
TA
TU Kaiserslautern
424
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Typen in Haskell
3.3 Polymorphie und Funktionen höherer Ordnung
Beispiel:
Bezeichne TE die Menge der Typausdrücke in Haskell.
• Die „ist_spezieller_als“ Relation auf TE × TE ist reflexiv und
transitiv, aber nicht antisymmetrisch.
foldplus :: (Num a) => [a] -> a
-- Addiert alle Elemente einer Liste.
-- Listenelemente muessen alle vom gleichen Zahltyp sein
• 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.
foldplus []
=
foldplus (x:xs) =
Damit ist gesagt, was Typen in Haskell sind.
0
x + foldplus xs
Bemerkung:
Das Typsystem von Haskell ist feiner als hier dargestellt (Typklassen
und benutzerdefinierte Typconstraints).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
425
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Typregeln in Haskell:
426
3.3 Polymorphie und Funktionen höherer Ordnung
Begriffserklärung: (Typinferenz)
Wir nennen eine Ausdruck A typannotiert, wenn A und allen
Teilausdrücken von A ein Typ zugeordnet ist.
Beispiel:
Die Typannotationen von
ist
Typinferenz bedeutet das Ableiten der Typannotation für Ausdrücke
aus den gegebenen Deklarationsinformationen.
abs (-2)
((abs::Int->Int)(-2::Int))::Int
Bemerkung:
In Programmiersprachen mit parametrischem Typsystem kann
Typinferenz sehr komplex sein.
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
428
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Beispiele:
Beispiele: (2)
3. Einsortieren in geordnete Liste mit Vergleichsfunktion:
1. Leere Liste:
einsortieren vop p1 []
= [p1]
einsortieren vop p1 (x:xs)
| p1 `vop` x
= p1:x:xs
| otherwise = x : ( einsortieren vop p1 xs)
x = [ ]
Inferierter Typ für x :: [a]
2. Enthalten sein in Liste:
enthalten
enthalten
|
|
p1 []
=
p1 (x:xs)
p1 == x
=
otherwise =
Inferierter Typ für einsortieren ::
(t -> t -> Bool) -> t -> [t] -> [t]
False
True
enthalten p1 xs
Bemerkung:
Bei der Typinferenz versucht man immer den allgemeinsten Typ
herauszufinden.
Inferierter Typ für enthalten :: (Eq a) => a -> [a] -> Bool
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
429
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Parametrische markierte Binärbaumtypen:
430
3.3 Polymorphie und Funktionen höherer Ordnung
Parametrische markierte Binärbaumtypen: (2)
1. Alle Markierungen sind vom gleichen Typ:
data
BBaum a =
Blatt a
| Zweig a ( BBaum a) ( BBaum a)
3. Und was passiert hier?
data
2. Blatt- und Zweigmarkierungen sind möglicherweise von
unterschiedlichen Typen:
data
BBaum a b =
Blatt a
| Zweig b ( BBaum b a) (BBaum a b)
BBaum a b =
Blatt a
| Zweig b ( BBaum a b) ( BBaum a b)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
431
©Arnd Poetzsch-Heffter
TU Kaiserslautern
432
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Unterabschnitt 3.3.2
3.3 Polymorphie und Funktionen höherer Ordnung
Funktionen höherer Ordnung
Überblick:
Funktionen höherer Ordnung
• Einführung in Funktionen höherer Ordnung
• Wichtige Funktionen höherer Ordnung
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
433
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Einführung
434
3.3 Polymorphie und Funktionen höherer Ordnung
Sprachliche Aspekte:
Funktionen höherer Ordnung sind Funktionen, die
• Funktionen als Argumente nehmen und/oder
Alle wesentlichen Sprachmittel zum Arbeiten mit Funktionen höherer
Ordnung sind bereits bekannt:
• Funktionen als Ergebnis haben.
Selbstverständlich sind auch Listen oder Tupel von Funktionen als
Argumente oder Ergebnisse möglich.
• Funktionsabstraktion
• Funktionsdeklaration
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
• Funktionsanwendung
• Funktionstypen
• Differenzial
• unbestimmtes Integral
©Arnd Poetzsch-Heffter
TU Kaiserslautern
435
©Arnd Poetzsch-Heffter
TU Kaiserslautern
436
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Konzeptionelle Aspekte:
Beispiel: (Abstraktion, Wiederverwendung)
Zwei konzeptionelle Aspekte liegen der Anwendung von Funktionen
höherer Ordnung in der Software- Entwicklung zugrunde:
Aufgabe:
Sortiere eine Liste xl von Zahlen durch Einfügen (insertion sort)
Rekursionsidee:
1. Abstraktion und Wiederverwendung
• Sortiere zunächst den Rest der Liste.
2. Metaprogrammierung, d.h. das Entwickeln von Programmen, die
Programme als Argumente und Ergebnisse haben.
• Das ergibt eine sortierte Liste xs.
• Sortiere das erste Element von xl in xs ein.
Wir betrachten im Folgenden den ersten Aspekt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
437
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiel: (Abstraktion, Wiederverwendung) (2)
438
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?
einsortieren :: Int -> [Int] -> [Int]
Antwort:
Abstraktion des Algorithmus: Führe die Vergleichsoperation als
weiteren Parameter ein.
einsortieren p1 []
= [p1]
einsortieren p1 (x:xs)
| p1 <= x
= p1:x:xs
| otherwise = x : ( einsortieren p1 xs)
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 []
= []
sort (x:xr) = einsortieren x (sort xr)
sort vop []
= []
sort vop (x:xr) = einsortieren vop x (sort vop xr)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
439
©Arnd Poetzsch-Heffter
TU Kaiserslautern
440
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Anwendung:
Bemerkung:
l1
l2
l3
l4
Polymorphe Funktionen können häufig auch Funktionen als Parameter
nehmen.
=
=
=
=
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]
Beispiele:
1. Funktion cons auf Listen : abs :fac :[]
2. Identitätsfunktion: id = (x-> x)(x->x)
strcmp :: String -> String -> Bool
strcmp = (<=)
l5 = sort strcmp [" Abbay ","Abba","Ara","ab"]
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
441
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Wichtige Funktionen höherer Ordnung
Wichtige Funktionen höherer Ordnung (2)
Dieser Abschnitt betrachtet einige Beispiele für Funktionen höherer
Ordnung und diskutiert das Arbeiten mit solchen Funktionen.
Map:
Anwendung einer Funktion auf die Elemente einer Liste:
map f [x1,x2,x3] == [f x1, f x2, f x3]
Applikationsoperator:
map :: (a -> b) -> [a] -> [b]
($) :: (a -> b) -> a -> b
map f []
= []
map f (x:xs) = (f x): map f xs
rechtsassoziativ; erlaubt andere Schreibweise/Klammersetzung:
fac $ fac $ n+1 statt fac (fac (n+1))
Beispiele:
Funktionskomposition:
map length [" Schwerter ","zu"," Pflugscharen "]
(.) :: (b -> c) -> (a -> b) -> a -> c
(f.g) x = f (g x)
©Arnd Poetzsch-Heffter
442
TU Kaiserslautern
double n = 2*n
map (map double )
443
©Arnd Poetzsch-Heffter
[ [1,2], [34829] ]
TU Kaiserslautern
444
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Currying und Schönfinkeln:
Zwei Varianten von map im Vergleich:
1. Die beiden Argumente als Paar:
Funktionen mit einem Argumenttupel kann man die Argumente auch
sukzessive geben. Dabei entstehen Funktionen höherer Ordnung.
mapp :: (b -> a, [b]) -> [a]
mapp (f ,[])
= []
mapp (f,x:xs) = (f x): mapp (f,xs)
Beispiele:
times :: Integer -> Integer -> Integer
times m n = m * n
2. Die Argumente nacheinander („gecurryt“):
double :: Integer -> Integer
double = times 2
map :: (a -> b) -> [a] -> [b]
map f []
= []
map f (x:xs) = (f x): map f xs
double 5
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
445
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkung:
446
3.3 Polymorphie und Funktionen höherer Ordnung
Curryen von Funktionen:
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:
Die Funktion curry liefert zu einer Funktion auf Paaren die zugehörige
gecurryte Funktion:
curry :: ((a, b) -> c) -> a -> b -> c
double :: Integer -> Integer
double = times 2
curry f x y = f (x,y)
intListSort :: [Int] -> [Int]
intListSort = sort ((<=)::Int ->Int ->Bool)
doublelist :: [Int] -> [Int]
doublelist = map double
©Arnd Poetzsch-Heffter
TU Kaiserslautern
447
©Arnd Poetzsch-Heffter
TU Kaiserslautern
448
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Prüfen und Filtern von Listen:
Prüfen und Filtern von Listen: (2)
filter :: (a -> Bool) -> [a] -> [a]
-- Liste alle Elemente , die das gegebene Praedikat
-- erfuellen
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
||
filter pred [ ]
= []
filter pred (x:xs)
| pred x
= x : ( filter pred xs)
| otherwise
= ( filter pred xs)
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
&&
3. Funktionales Programmieren
Beispiele:
ismember x xs = any (\y-> x==y) xs
split p xs =
( filter (\y-> y<p) xs , filter (\y-> p<=y) xs)
all pred xs
TU Kaiserslautern
449
TU Kaiserslautern
3. Funktionales Programmieren
450
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:
Die "Veränderung" einer Funktion an einem Punkt des
Argumentbereichs:
foldr ⊗ n [e1, e2, . . . ,en] = e1 ⊗ (e2 ⊗ (...(en ⊗ n) . . . ))
update :: (Eq a) => (a -> b) -> a -> b -> a -> b
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
Punktweise Veränderung von Funktionen:
update f x v y
| x == y
=
| otherwise =
3.3 Polymorphie und Funktionen höherer Ordnung
Deklaration von foldr:
foldr :: (a -> b -> b) -> b -> [a] -> b
v
f y
foldr f n [ ]
= n
foldr f n (x:xs) = f x (foldr f n xs)
TU Kaiserslautern
451
©Arnd Poetzsch-Heffter
TU Kaiserslautern
452
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Falten von Listen: (2)
Falten von Listen: (3)
Üblicherweise steht auch eine Funktion für das Falten von links zur
Verfügung:
Mittels der Faltungsfunktionen foldr und foldl lassen sich viele
Listenfunktionen direkt, d.h. ohne Rekursion definieren:
foldl ⊗ n [e1, e2, . . . ,en] = (. . . ((n ⊗ e1) ⊗ e2) ... ⊗ en)
sum , product :: (Num a) => [a] -> a
sum
= foldr (+) 0
product = foldr (*) 1
Deklaration von foldl:
foldl :: (b -> a -> b) -> b -> [a] -> b
(++) :: [a] -> [a] -> [a]
(++) l1 l2 = foldr (:) l2 l1
foldl f n [ ]
= n
foldl f n (x:xs) = foldl f (f n x) xs
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
453
©Arnd Poetzsch-Heffter
3.3 Polymorphie und Funktionen höherer Ordnung
TU Kaiserslautern
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
Falten von Listen: (4)
Bemerkungen: (Funktionen höherer Ordnung)
Bäume mit variabler Kinderzahl lassen sich allgemeiner und
kompakter behandeln (vgl. Folien 298f):
Die Programmentwicklung mittels Funktionen höherer Ordnung
(funktionale Programmierung) ist ein erstes Beispiel für:
data VBaum = Kn Int [ VBaum ]
• das Zusammensetzen komplexerer Bausteine,
deriving (Eq ,Show)
• programmiertechnische Variationsmöglichkeiten,
zaehleKnVBaum :: VBaum -> Int
zaehleknVBaum (Kn (_,xs)) =
foldr (+) 1 (map zaehleknVBaum xs)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
454
• die Problematik der Wiederverwendung:
I die Bausteine müssen bekannt sein,
I die Bausteine müssen ausreichend generisch sein.
455
©Arnd Poetzsch-Heffter
TU Kaiserslautern
456
3. Funktionales Programmieren
3.3 Polymorphie und Funktionen höherer Ordnung
3. Funktionales Programmieren
Bemerkungen: (zur Haskell-Einführung)
3.4 Semantik, Testen und Verifikation
Abschnitt 3.4
• 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.
Semantik, Testen und Verifikation
• 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
3. Funktionales Programmieren
457
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Übersicht
458
3.4 Semantik, Testen und Verifikation
Unterabschnitt 3.4.1
• Einführung in Semantik von Programmiersprachen
Zur Semantik funktionaler Programme
• Testen und Verifikation
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
TU Kaiserslautern
459
©Arnd Poetzsch-Heffter
TU Kaiserslautern
460
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Zur Semantik funktionaler Programme
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
Lernziele in diesem Unterabschnitt:
• Was bedeutet Auswertungssemantik?
=
dd `div` dr
2. Behandlung rekursiver Deklarationen:
• Wie sieht sie im Falle von Haskell aus?
a. Insgesamt unbestimmt:
• Welche Bedeutung haben Bezeichnerumgebungen dabei?
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
3. Funktionales Programmieren
461
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Ziel:
462
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (denotationelle Semantik)
Ordne jeder syntaktisch korrekten Funktionsdeklaration eine partielle
Funktion zu. Die Semantik beschreibt diese Zuordnung.
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.
Wir unterscheiden hier denotationelle und operationelle Semantik.
Statt operationeller Semantik spricht man häufig von
Auswertungssemantik.
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
TU Kaiserslautern
463
©Arnd Poetzsch-Heffter
TU Kaiserslautern
464
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (denotationelle Semantik)
Beispiel: (denotationelle Semantik) (2)
Eine denotationelle Semantik würde der obigen Funktionsdeklaration
von fac eine Funktion f
f : Z⊥ → Z⊥
Zwei mögliche Lösungen f1 und f2 :
(
f1 (k ) =
zuordnen, wobei
⊥ , falls k =⊥ oder k < 0
k ! , sonst


⊥ , falls k =⊥



0
,k < 0
f2 (k ) = 


 k ! , sonst
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
3. Funktionales Programmieren
465
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiel: (denotationelle Semantik) (3)
Beispiel: (denotationelle Semantik) (4)
Wir zeigen, dass f2 eine Lösung der Gleichung ist:
n =⊥: links:
f2 (⊥) =⊥
rechts: if ⊥= 0 then 1 else ⊥ ∗f2 (⊥ −1) = ⊥
Die denotationelle Semantik muss sicherstellen,
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:
• 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:
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
466
467
©Arnd Poetzsch-Heffter
TU Kaiserslautern
468
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Begriffsklärung: (strikte Funktionen)
3.4 Semantik, Testen und Verifikation
Bemerkungen:
Eine n-stellige Funktion oder Operation heißt strikt, wenn sie ⊥ als
Ergebnis liefert, sobald eines der Argumente ⊥ ist.
• Denotationelle Semantik basiert auf einer Theorie partieller
Beispiele: (nicht-strikte Funktionen)
strikter Funktionen und Fixpunkttheorie.
• Die dreistellige “Funktion” if-then-else und die boolschen
Operatoren && und || sind in fast allen Programmiersprachen
I
I
nicht strikt.
• In Haskell deklarierte Funktionen sind im Allg. nicht strikt:
• ⊥ steht für undefiniert, unabhängig davon, welcher der Gründe für
Partialität vorliegt.
ite :: Bool -> a -> a -> a
ite b x y = if b then x else y
Prelude >
45
©Arnd Poetzsch-Heffter
Vorteil: Für Beweise besser geeignet.
Nachteil: Theoretisch aufwendiger zu handhaben.
ite False (4 `div` 0) 45
TU Kaiserslautern
3. Funktionales Programmieren
469
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Begriffsklärung: (operationelle Semantik)
470
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (formaler/aktueller Parameter)
Eine Semantik, die erklärt, wie eine Funktion oder ein Programm
auszuwerten ist, nennen wir operationell oder
Auswertungssemantik .
Ein Bezeichner, der in einer Funktionsdeklaration einen Parameter
bezeichnet, wird formaler Parameter genannt.
Wir erläutern
Der Ausdruck oder Wert, der einer Funktion bei einer Anwendung
übergeben wird, wird aktueller Parameter genannt.
• eine Auswertungsstrategie für funktionale Programme,
• welche Rolle Bezeichnerumgebungen dabei spielen, und
• führen wichtige Begriffe ein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
471
©Arnd Poetzsch-Heffter
TU Kaiserslautern
472
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Begriffsklärung: (Auswertungsstrategie)
3.4 Semantik, Testen und Verifikation
Beispiele: (Parameterübergabeverfahren)
Parameterübergabe:
1. Call-by-Value:
I
Die Auswertungsstrategie legt fest,
I
• in welchen Schritten die Ausdrücke ausgewertet werden und
I
• wie die Parameterübergabe geregelt ist.
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
3. Funktionales Programmieren
473
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
474
3.4 Semantik, Testen und Verifikation
Beispiel: (Auswertungsstrategien
Beispiel: (Auswertungsstrategien (2)
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
if False then 1
else
else
=
f(1-1,f(1 -0 ,0))
=
f(1-1,f(1 -0 ,0))
TU Kaiserslautern
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.
f (1-1, f(1 -0 ,0) )
©Arnd Poetzsch-Heffter
f (0, f(1 -0 ,0) )
475
©Arnd Poetzsch-Heffter
TU Kaiserslautern
476
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Beispiel: (Auswertungsstrategien (3)
Beispiel: (Auswertungsstrategien (4)
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
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
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).
477
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Begriffsklärung: (Normalform)
478
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.
Der Ergebnisausdruck einer terminierenden Auswertung wird
Normalform genannt.
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
479
©Arnd Poetzsch-Heffter
TU Kaiserslautern
480
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Eine Teilsprache von Haskell
3.4 Semantik, Testen und Verifikation
Beispielprogramme
data Exp =
Cond
Exp Exp Exp
| Ident
String
| Binary Op Exp Exp
| Lambda String Exp
| Appl
Exp Exp
| Let
String Exp Exp
-- 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")))
| BConst Bool
| IConst Integer
| Closure String Exp Env
deriving (Eq , Show)
data Op = Plus | Mult | Eq
deriving (Eq , Show)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
481
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispielprogramme (2)
Beispielprogramme (3)
-- 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))
-- 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
483
©Arnd Poetzsch-Heffter
482
TU Kaiserslautern
484
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Auswertungsbeispiel
=
=
=
=
=
=
=
Datentyp für Bezeicherumgebung
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)
[]
data Env = Ec [ (String ,(Exp ,Env)) ]
[ a=(5 ,[]) ]
[ a=(0 ,[.. .]) , b=(a+7 ,[a=(5 ,[]) ]), a=(5 ,[]) ]
eval (a+7)
(eval a
insert :: String -> (Exp ,Env) -> Env -> Env
-- ( insert bez xe e) traegt die Bindung (bez ,xe)
-- in die Umgebung e ein
[ a=(5 ,[]) ]
[ a=(5 ,[]) ]) + (eval 7
[ a=(5 ,[]) ])
lookUp :: String -> Env -> (Exp ,Env)
-- ( lookUp bez e) liefert das Paar xe der ersten
-- gefundenen Bindung (bez ,xe) mit Bezeichner bez
(eval 5 []) + 7
5 + 7 =
deriving (Eq , Show)
emptyEnv :: Env
-- leere Bezeichnerumbegung
eval (let a=0 in b) [ b=(a+7 ,[a=(5 ,[]) ]), a=(5 ,[]) ]
eval b
3.4 Semantik, Testen und Verifikation
12
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
485
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Funktionsabschlüsse:
486
3.4 Semantik, Testen und Verifikation
Funktionsabschlüsse: (2)
Vorgehen:
Das Ergebnis eines funktionswertigen Ausdrucks wird als Triple
Fragen:
Wie wird das Ergebnis eines funktionswertigen Ausdrucks dargestellt?
Wie wird eine benutzerdeklarierte Funktion in der
Bezeichnerumgebung dargestellt?
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
487
©Arnd Poetzsch-Heffter
TU Kaiserslautern
488
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Beispiel:
=
=
=
=
=
=
3.4 Semantik, Testen und Verifikation
Auswertungssemantik für die Haskell-Teilsprache:
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
3. Funktionales Programmieren
489
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
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Auswertungssemantik für die Haskell-Teilsprache: (2)
490
3.4 Semantik, Testen und Verifikation
Bemerkungen:
• Wir haben die Auswertungssemantik von Haskells Teilsprache in
Haskell selbst definiert, weil wir keine andere
Beschreibungstechnik kennen.
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)
• Ü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
491
©Arnd Poetzsch-Heffter
TU Kaiserslautern
492
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Spezifikation der zulässigen Parameter
3.4 Semantik, Testen und Verifikation
Bemerkungen:
Es ist eine Entwurfsentscheidung,
Bei jeder (partiellen) Funktion muss man sich überlegen und
dokumentieren, welche aktuellen Parameter bei einer Anwendung
zulässig sein sollen.
• welche Parameter zulässig und welche unzulässig sind;
• wie man mit unzulässigen Paramtern umgeht.
Beispiel:
Varianten einer Funktion foo:
Der Anwender der Funktion hat dann die Verantwortung, dass die
Komponente nie mit unzulässigen Parametern angewendet wird.
1. foo(m,n) = if m<n then m `div` n else foo(m-n,n)
Üblicherweise sollte die Komponente für zulässige Parameter normal
terminieren.
Zulässigkeitsbereich
Ggf. sind möglicherweise auftretende Ausnahmen zu dokumentieren.
(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)
Entsprechendes gilt für andere parametrisierte Softwarekomponenten.
Zulässkeitsbereich (m >=0 && n > 0), vervollständigt durch -1.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
493
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen: (2)
494
3.4 Semantik, Testen und Verifikation
Design by Contract:
Entwerfe Programmteile zusammen mit genauen Spezifikationen ihrer
Schnittstellen.
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)
Insbesondere spezifiziere bei Funktionen, welche Parameter zulässig
sind und was das Ergebnis im Zulässigkeitsbereich ist.
Zulässigkeitsbereich (m < n && n /=0)|| n > 0;
Fehlermeldung, wenn Zulässigkeitsbereich verlassen wird.
Die Spezifikation kann als Vertrag zwischen
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
• dem Anwender der Funktion/Komponente (client),
• demjenigen, der die Funktion/Komponente realisiert (provider)
verstanden werden.
495
©Arnd Poetzsch-Heffter
TU Kaiserslautern
496
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Beispiel: (Spezifikation)
3.4 Semantik, Testen und Verifikation
Unterabschnitt 3.4.2
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
Testen und Verifikation
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
3. Funktionales Programmieren
497
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Testen und Verifikation
498
3.4 Semantik, Testen und Verifikation
Qualitätssicherung: Kleine Einführung
Ein zentraler Teil der Software-Entwicklung besteht darin zu prüfen, ob
die entwickelte Software auch den gestellten Anforderungen
entspricht.
Überblick:
Bei der Qualitätssicherung in der Softwareentwicklung spielen zwei
Fragen eine zentrale Rolle:
• Wird das richtige System entwickelt?
• Einführende Bemerkungen zur Qualitätssicherung
• Wird das System richtig entwickelt?
• Testen
• Verifikation von Programmeigenschaften
©Arnd Poetzsch-Heffter
TU Kaiserslautern
499
©Arnd Poetzsch-Heffter
TU Kaiserslautern
500
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Qualitätssicherung: Kleine Einführung (2)
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.
Validation hat die Beantwortung der ersten Frage zum Ziel.
Beispielsweise ist zu klären, ob
• die benutzten Anforderungen die Vorstellungen des Auftraggebers
richtig wiedergeben,
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
• die Anforderungen von allen Beteiligten gleich interpretiert
Eingaben die erwarteten Ausgaben liefert (Beschreibung:
Testfälle).
werden,
• Unterspezifizierte Aspekte richtig konkretisiert wurden.
• 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
3. Funktionales Programmieren
501
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen:
502
3.4 Semantik, Testen und Verifikation
Begriffsklärung: (Testen)
• Liegen die beschriebenen Eigenschaften in einer formalen
Sprache vor, kann die Verifikation automatisiert werden.
Testen bedeutet die Ausführung eines Programms oder
Programmteils mit bestimmten Eingabedaten.
• Zu prüfende Eigenschaften bei Funktionen:
I Terminierung für zulässige Parameter
I Verhalten wie im Entwurf festgelegt
Testen kann sowohl zur Validation als auch zur Verifikation dienen.
Bei funktionalen Programmen bezieht sich Testen überwiegend auf
Eingabe- und Ausgabeverhalten von Funktionen.
• Grundsätzlich können sich Validation und Verifikation auf alle
Phasen der Softwareentwicklung beziehen.
Wir betrachten:
• Testen mit Testfällen
• Wir betrachten im Folgenden Testen und Verifikation durch
• Testen durch dynamisches Prüfen
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
503
©Arnd Poetzsch-Heffter
TU Kaiserslautern
504
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Testen mit Testfällen
3.4 Semantik, Testen und Verifikation
Beispiel:
Testen der folgenden Funktionsdeklaration:
• Beschreibe das “Soll-Verhalten” der Software durch eine
fac :: Int -> Int
-- Berechnet fuer n in [0.. 12] die Fakultaet
• Prüfe, ob die Software zu den Eingaben der Testfälle die
fac 0 = 1
fac n = if 0<n || n <= 12
then n * fac (n -1)
else undefined
(endliche) Menge von Eingabe-Ausgabe-Paaren.
entsprechenden Ausgaben liefert.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
505
3.4 Semantik, Testen und Verifikation
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
Testfälle:
0
12
13
-1
-1073741824
©Arnd Poetzsch-Heffter
506
3.4 Semantik, Testen und Verifikation
Beobachtetes Verhalten:
→
→
→
→
→
1
479001600
Fehler: undefiniert/unzulaessiger Parameter
Fehler: undefiniert/unzulaessiger Parameter
Fehler: undefiniert/unzulaessiger Parameter
TU Kaiserslautern
0
12
13
-1
-1073741824
507
©Arnd Poetzsch-Heffter
→
→
→
→
→
1
479001600
1932053504
*** Exception: stack overflow
*** Exception: stack overflow
TU Kaiserslautern
508
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Bemerkung:
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.
• Das Verhalten von Funktionen mit unendlichem Argumentbereich
kann durch Testen nur teilweise verifiziert werden. Testen kann im
Allg. nicht die Abwesenheit von Fehlern zeigen.
• 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
• Wichtig ist die Auswahl der Testfälle. Sie sollten die “relevanten”
Programms auswerten.
Argumentbereiche abdecken.
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
3. Funktionales Programmieren
509
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Prüfungen:
510
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.
1. Prüfung der Zulässigkeit von Parametern beim Aufruf
2. Prüfung durch Ergebniskontrolle
Im Gegensatz zum Testen erlaubt Verifikation (durch Beweis) die
Korrektheit zu zeigen, d.h. insbesondere die Abwesenheit von Fehlern.
Bemerkung:
Viele moderne Programmiersprachen bieten spezielle
Sprachkonstrukte für das Testen durch dynamische Prüfung an.
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
TU Kaiserslautern
Wir betrachten hier nur Programmverifikation, d.h. den Nachweis, dass
ein Programm eine spezifizierte Eigenschaft besitzt.
511
©Arnd Poetzsch-Heffter
TU Kaiserslautern
512
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Verifikation durch Beweis (2)
3.4 Semantik, Testen und Verifikation
Beispiele: (Spezifikation)
Die Spezifikation kommt üblicherweise aus dem Entwurf bzw. den
Anforderungen.
Spezifikation:
Eine Funktion ggt soll implementiert werden.
Für m, n mit m ≥ 0, n ≥ 0, m, n nicht beide null, soll gelten:
Zwei zentrale Eigenschaften:
• Programm liefert die richtigen Ergebnisse, wenn es terminiert
(partielle Korrektheit).
ggt m n = max { k | k teilt m und n }
• Programm terminiert für die zulässigen Eingaben.
Beide Eigenschaften zusammen ergeben totale Korrektheit.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
513
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Beispiel: (Implementierung)
514
3.4 Semantik, Testen und Verifikation
Beweisverfahren:
Bei funktionalen Programmen spielen zwei Beweisverfahren eine
zentrale Rolle:
Euklidscher Algorithmus:
1. Strukturelle Induktion oder Parameterinduktion
ggt :: Integer -> Integer -> Integer
-- m, n >= 0, nicht beide gleich 0
2. Berechnungsinduktion (computational induction)
Wir stellen nur die Paramterinduktion/strukturelle Induktion vor.
ggt m n = if m==0 then n else ggt (n `mod` m) m
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
TU Kaiserslautern
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.
515
©Arnd Poetzsch-Heffter
TU Kaiserslautern
516
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Beispiel: (Verifikation von ggt)
Beispiel: (Verifikation von ggt) (2)
Vorüberlegung:
Für n ≥ 0, m > 0 gilt:
k teilt m und n
⇔
3.4 Semantik, Testen und Verifikation
• Ad (a) – Induktionsanfang:
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
3. Funktionales Programmieren
517
=
ggt 0 n
n
max{ k | k teilt n}
max{ k | k teilt 0 und n}
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Beispiel: (Verifikation von ggt) (3)
518
3.4 Semantik, Testen und Verifikation
Bemerkung:
• 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)!
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.
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
520
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Äquivalente Funktionsdeklarationen:
Bemerkung:
Wir haben gesehen, dass unterschiedliche Funktionsdeklarationen das
gleiche Ein- und Ausgabeverhalten haben können.
3. Funktionales Programmieren
bei der Effizienz aufweisen.
Programmoptimierung und dem Refactoring von Software eine
wichtige Rolle.
Transformiert man die eine in die andere Form ist es wichtig, die
Äquivalenz zu zeigen.
TU Kaiserslautern
• Semantisch äquivalente Programme können große Unterschiede
• Bedeutungserhaltende Transformationen spielen in der
Zum Beispiel kann eine Deklaration in einer “aufwendigeren”
Rekursionsformen einfacher zu lesen sein, aber eine entsprechende
lineare oder repetitive Funktionsdeklaration performanter sein.
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
Korrektheit von Transformationen:
Wir transformieren die rekursive Funktionsdeklarationen in einfachere
Deklarationen und zeigen Äquivalenz.
521
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Beispiel: (linear → repetitiv)
522
3.4 Semantik, Testen und Verifikation
Lemma:
Wir betten die Fakultätsfunktion
fac :: Integer -> Integer
fac n = if n == 0 then 1 else n * fac (n -1)
Für die obigen Deklarationen von fac und facrep gilt:
in eine Funktion mit einem weiteren Argument ein, das
Zwischenergebnisse aufsammelt:
∀n mit n ≥ 0, r mit r ≥ 0 :
facrep :: Integer -> Integer -> Integer
facrep n res = if n==0 then res
else facrep (n -1) (res*n)
insbesondere: ∀n mit n ≥ 0 :
(fac n) ∗ r = facrep n r
fac n = fac1 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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
524
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Beweis: mittels Parameterinduktion nach n
3.4 Semantik, Testen und Verifikation
Beweis: (Forts.)
Induktionsschritt: k → k + 1
Induktionsanfang:
Zu zeigen: ∀r mit r ≥ 0 : (fac 0) ∗ r = facrep 0 r
=
=
=
=
Induktionsvoraussetzung:
Für k ≥ 0 : ∀r mit r ≥ 0 : (fac k)*r =facrep k r
(fac 0) * r
Zu zeigen: fac(k+1)* r = facrep (k+1)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
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
3. Funktionales Programmieren
525
3. Funktionales Programmieren
Idee:
Führe zwei zusätzliche Parameter ein, in denen ausgehend von 0 und
1 die Zwischenergebnisse berechnet werden.
(* Induktionsvoraussetzung *)
Es soll also gelten:
( facrep k r) * (k+1))
=
fib1 n
k+1==0
fibemb n 0 1
fibemb 0 letzt res = 0
fibemb 1 letzt res = res
fibemb n letzt res = fibemb (n -1) res (letzt +res)
then r else facrep k (r*(k+1))
(* Deklaration von facrep *)
Beweis: mittels Parameterinduktion nach n.
facrep (k+1) r
©Arnd Poetzsch-Heffter
=
Wir definieren fibemb zu:
(* Ausdrucksumformung *)
if
3.4 Semantik, Testen und Verifikation
Tranformation der Fibonacci-Funktion fib in eine lineare Form.
(( fac k)* r) * (k+1)
=
526
Beispiel: (kaskadenartig → linear)
(k+1) * (fac k) *r
(* Kommutativitaet + Assoziativitaet der Multipl . *)
=
TU Kaiserslautern
3.4 Semantik, Testen und Verifikation
Beweis: (Forts.)
=
©Arnd Poetzsch-Heffter
TU Kaiserslautern
527
©Arnd Poetzsch-Heffter
TU Kaiserslautern
528
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Terminierung
3.4 Semantik, Testen und Verifikation
Beispiele: ("kleiner werdende" Parameter)
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.
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
Idee:
Die Parameter sollten bei jedem rekursiven Aufruf “kleiner” werden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
529
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
530
3.4 Semantik, Testen und Verifikation
Definition: (Ordnung)
Definition: (Ordnung) (2)
Eine Teilmenge R von M × N heißt eine (binäre) Relation.
Eine reflexive, antisymmetrische und transitive homogene Relation auf
M × M heißt eine (partielle) Ordnungsrelation.
Gilt M = N, dann nennt man R homogen.
Eine Menge M mit einer Ordnungsrelation R heißt eine (partielle)
Ordnung.
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:
Meist benutzt man Infixoperatoren wie ≤ (oder ⊆) zur Darstellung der
Relation und schreibt
wenn (x, y ) ∈ R und (y , x) ∈ R, dann x = y
• transitiv, wenn für alle x, y , z ∈ M gilt:
x ≤ y statt (x, y ) ∈ R
wenn (x, y ) ∈ R und (y , z) ∈ R, dann (x, z) ∈ R
©Arnd Poetzsch-Heffter
TU Kaiserslautern
531
©Arnd Poetzsch-Heffter
TU Kaiserslautern
532
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Definition: (Kette, noethersche Ordnung)
Definition: (Kette, noethersche Ordnung) (2)
Sei N eine Teilmenge von M. x ∈ N heißt:
Sei (M, ≤) eine Ordnung. Eine Folge ϕ : N → M heißt eine (abzählbar
unendliche) aufsteigende Kette, wenn für alle i ∈ N gilt:
• größtes Element von N, wenn ∀y ∈ N gilt: y ≤ x.
• kleinstes Element von N, wenn ∀y ∈ N gilt: x ≤ y .
ϕ(i) ≤ ϕ(i + 1)
• maximales Element von N, wenn ∀y ∈ N gilt:
(absteigende Kette: entsprechend).
x ≤ y impliziert x = y .
• minimales Element von N, wenn ∀y ∈ N gilt:
Eine Kette ϕ wird stationär, falls es ein j ∈ N gibt, so dass
y ≤ x impliziert x = y .
ϕ(j) = ϕ(j + k ) für alle k ∈ N
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Eine Ordnung (M, ≤) heißt noethersch, wenn jede nicht-leere
Teilmenge von M ein minimales Element besitzt.
533
©Arnd Poetzsch-Heffter
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Lemma:
534
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, ≤)
Eine Ordnung ist genau dann noethersch, wenn jede absteigende
Kette stationär wird.
• und eine Abb. δ : P → M gibt,
Beweis: (siehe Theorievorlesung)
• 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
535
©Arnd Poetzsch-Heffter
TU Kaiserslautern
536
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
Bemerkung:
3.4 Semantik, Testen und Verifikation
Beispiele: (Terminierungsbeweis)
1. foldrplus xs =
if null xs then 0
else (headd xs) + foldrplus (tail xs)
• Da die aktuellen Parameter nur endlich oft echt kleiner werden
können (und dann stationär werden), garantiert das obige
Kriterium die Terminierung.
P ist die Menge aller endlichen Listen über Integer.
• Um die Terminierung nachzuweisen, muss man also eine
Noethersche Ordnung (N, ≤).
geeignete noethersche Ordnung und eine geeignete Abbildung δ
finden.
Als δ wähle die Funktion länge (Länge einer Liste).
Zu zeigen:
• Ist der Argumentbereich bereits noethersch geordnet, kann δ
i) tail xs ist ein zulässiger Parameter: ok.
selbstverständlich auch die Identität sein.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
ii) länge (tail xs) < länge xs: ok.
537
3.4 Semantik, Testen und Verifikation
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: (Terminierungsbeweis) (2)
2. einfuegen ([] ,z,ix)
=
einfuegen (xs ,z ,0)
=
einfuegen (x:xs ,z,ix) =
©Arnd Poetzsch-Heffter
538
3.4 Semantik, Testen und Verifikation
Beispiele: (Terminierungsbeweis) (3)
[z]
z:xs
einfuegen (xs ,z,ix -1)
Bemerkung:
P ist die Menge aller Tripel aus (a,[a],Int).
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.
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
540
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
3. Funktionales Programmieren
3.4 Semantik, Testen und Verifikation
Beispiele: (Terminierungsbeweis) (4)
Beispiele: (Terminierungsbeweis) (5)
3. foo(m,n) =
if m<n then m `div` n else foo (m-n,n)
Zu zeigen:
i) (m − n, n) ist ein zulässiger Parameter: ok.
P ist die Menge aller Paare (m, n) aus (Integer,Integer)
mit (m < n und n , 0) oder n > 0.
ii) Unter der Voraussetzung m ≥ n und n > 0:
Noethersche Ordnung (N, ≤).
1. Fall: m − n ≥ n:
δ(m − n, n) = m − n − n + 1 < m − n + 1 = δ(m, n)
δ : Z × Z → N mit
(
δ(m, n) =
©Arnd Poetzsch-Heffter
2. Fall: m − n < n:
0
, falls m < n
m − n + 1 , falls m ≥ n
TU Kaiserslautern
3. Funktionales Programmieren
δ(m − n, n) = 0 < m − n + 1 = δ(m, n)
541
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
©Arnd Poetzsch-Heffter
TU Kaiserslautern
542
Herunterladen