Praxis der Funktionalen Programmierung (27. 10.)


Wiederholung Listen

Wir betrachten nochmal das Programm für Append (++) und beweisen eine Eigenschaft durch Induktion. Das gleiche für das Umdrehen von Listen (reverse). Natürlich wünschen wir uns, daß diese Beweise werden. Wieweit geht das? Es ist Equational Reasoning, siehe Vorlesung Term-Ersetzung (Dr. Hartwig).

Eine typische Aktion auf Listen ist das Sortieren. Wir können Quicksort sehr direkt hinschreiben. Es hat aber die bekannten Nachteile, falls wir ungünstige Pivot-Elemente benutzen, und die Laufzeit leidet zusätzlich durch die naive Implementierung.

Sicherer ist Mergesort. Wir schreiben zuerst die top-down, dann die bottom-up-Variante, und verbessern diese schließlich (Hausaufgabe).


Typkonstruktoren, Arten

Listen: [a]
kennen wir schon. Die eckigen Klammern (der List-Konstruktor) macht aus einem Typ a einen Typ [a]. Das heißt, List hat die Art (Kind) * -> *. Solche Kind-Deklarationen benutzen wir beim Beschreiben von Programmen, sie sind aber selbst nicht Teil der Sprache.
Tupel: (a,b), (a,b,c), ...
Das Komma (die Kommas) sind der Konstruktor. Das zweistellige Komma (der Paar-Konstruktor) hat die Art * -> * -> *.
Funktionen: a -> b
alle (stetigen) Funktionen von Typ a nach Typ b.

Typ-Ausdrücke

Einfacher Typ-Ausdruck
Typname (vordefinierter oder nutzerdefinierter)
Zusammengesetzter Typ-Ausdruck:
Typkonstruktor mit Argumenten (Anzahl entsprechend der Art des Konstruktors)

Algebraische Datentypen

data Typname (Parameter)* = (Konstruktor (Komponente)*)*
Beispiel (SOE, Seite 24)
data Shape = Rectangle Side Side
           | Ellipse Radius Radius
	   | Polygon [ Vertex ]
Hier ist Shape der neu definierte algebraische Datentyp, er hat keine Parameter, Rectangle, Ellipse und Polygon sind die Konstruktoren, die ersten beiden sind zweistellig, der letzte ist einstellig.

Die Konstruktoren sind Funktionen, haben also Typen, Beispiel:

Polygon :: [ Vertex ] -> Shape
Die Konstruktoren verwenden wir beim Mustersuchen (SOE, Seite 25 f)
area :: Shape -> Float
area (Rectangle x y) = x * y
area (Ellipse a b) = pi * a * b
area (Polygon ps) = ...
In einem Sichtbarkeitsbereich (Modul) darf es jeden Konstruktor nur einmal geben. Das folgende geht also nicht:
data Shape = ... (wie oben)
data Round = Circle Radius
           | Ellipse Radius Radius
Dann wüßte man nämlich nicht, welchen Typ der Ausdruck Ellipse 1.0 2.0 haben soll (Shape oder Round). Man könnte das durch Nachschlagen im Kontext herausbekommen, falls der Programmierer eine umgebende Typdeklaration geschrieben hat. Das muß er aber in Haskell (leider) nicht, deswegen geht das nicht.

Zwei wichtige Typen aus der Prelude sind

data Maybe a = Nothing | Just a
data Either a b = Left a | Right b

Typ-Synonyme

erklären keinen neuen Typ, sondern einer neuen Namen für einen bereits bestehenden Typ. Sie werden durch den Compiler (wie Macros) expandiert.
type Typname (Parameter)* = Typ-Ausdruck
Sie haben die gleichen Vor- und Nachteile wie Macros in C: bei sorgsamer Benutzung erhöhen sie die Übersichtlichkeit, aber der Compiler kann das nicht vom Programmierer erzwingen. Beispiel (SOE Seite 24)
type Radius = Float
type Side = Float
Die Aussage Rectangle :: Side -> Side -> Shape sieht schön aus, aber Rectangle :: Float -> Radius -> Shape wird vom Compiler genauso akzeptiert.

Mit Typ-Synonymen können wir also Abstraktionen nicht wirklich durchsetzen. Das richtige Mittel dazu sind Algebraischen Datentypen, deren Konstruktoren nicht exportiert werden (siehe später mehr über Module).


(Später:) Algebraische Datentypen mit benannten Komponenten

data Typname (Parameter)* = ( Konstruktor { name :: Type, ... } )*
Das erklärt wie oben eine Menge von Konstruktoren mit entsprechenden Typen und Stelligkeiten und zusätzlich eine Menge von Zugriffsfunktionen.

Beispiel (kompletter Quelltext)

type Winkel = Rational
type Winkels = [ Winkel ]

data Ecke   = Ecke { sichtbar :: Winkel
                   , alle :: Winkels
                   }

data Streckenzug = Streckenzug  [ Ecke ]
Ausdrücke vom Typ Ecke bauen wir so (Beispiel: reguläres Polygon?)
e :: Ecke
e = Ecke { sichtbar = 1 % 2, alle = [ 1 % 2 ] }
und wir können benannte Muster benutzen:
länge (Ecke { alle = ws }) = length ws
Die Namen "sichtbar", "alle" sind auch Zugriffsfunktionen:
sichtbar :: Ecke -> Winkel
alle     :: Ecke -> Winkels
Wir können (leider?) die Konstruktoren mit benannten Komponenten auch in der unbenannten Version benutzen.

Anwendung von Datentypen

Wir interessieren uns für Listen (Warteschlangen) mit Abständen (Wartezeiten) zwischen den Elementen. Das brauchen wir später bei der Musik. Wir können Warteschlangen nacheinander oder nebeneinander ausführen. (Quelltext.) Das Nebeneinander ist sehr ähnlich zum merge bei mergesort.

best viewed with any browser


http://www.informatik.uni-leipzig.de/~joe/ mailto:joe@informatik.uni-leipzig.de