II Grundlagen funktionaler Sprachen

Werbung
Prinzipien funktionaler Programmieren
Am Beispiel der Sprache Haskell
HfT Stuttgart
Seminar: Wissenschaftliches Schreiben
Seminar Leiterin: Frau Prof. Dr. Burger
Student: Yves Alexander
Matrikelnr.: 371275
Inhaltsverzeichnis
I Einleitung...................................................................................................................................... 2
II Grundlagen funktionaler Sprachen.......................................................................................... 3
1 Funktionale Sprachen und „Rapid-Prototyping“ ................................................................ 3
2 Einordnung in das Sprachspektrum ..................................................................................... 4
3 Das Lambda-Kalkül ................................................................................................................ 5
4 Das Programm als Menge von Funktionen........................................................................... 5
5 Datenstrukturen ...................................................................................................................... 7
III Merkmale Funktionaler Sprachen .......................................................................................... 8
1 Referentielle Transparenz ...................................................................................................... 8
2 Programmverifikation ............................................................................................................ 9
3 Ein-/ Ausgabe ......................................................................................................................... 10
4 Auswertung ............................................................................................................................ 11
IV Fazit .......................................................................................................................................... 13
Literaturverzeichnis ..................................................................................................................... 14
Bücher ....................................................................................................................................... 14
Internet ...................................................................................................................................... 14
1
I Einleitung
Angesichts hoher Kosten in der Softwareentwicklung, wird die Projektplanung immer wichtiger. Dabei
bekommt das so genannte „Rapid Prototyping“ eine immer bedeutendere Rolle, da mit einem Prototyp
eine sehr konkrete Spezifikation erreicht wird. Auf diesem Gebiet des „Rapid Prototyping“ haben
funktionale Sprachen gegenüber imperativen Sprachen einen enormen Vorteil. Auf Grund der hohen
Abstraktion ist es möglich, kurze und dadurch übersichtliche und meist auch fehlerfreie Quelltexte zu
entwerfen. Funktionale Programme orientieren sich fast ausschließlich an der mathematischen Lösung des
Problems, so dass sich der Programmierer nicht mit der Speicherplatzverwaltung beschäftigen muss, wie
es beispielsweise bei C der Fall ist.
Ein weiterer Vorteil funktionaler Sprachen ist die Möglichkeit Programme zu verifizieren, d.h. einen
mathematischen Beweis für die Semantik zu führen.
Trotz dieser Vorteile sind funktionale Sprachen im Gegensatz zu den modernen objektorientierten
Sprachen nur wenig verbreitet. In dieser Seminararbeit soll daher eine Einführung in die Prinzipien
funktionaler Sprachen vorgenommen werden. Diese Arbeit hat nicht das Ziel, dem Leser das funktionale
Programmieren beizubringen, sondern versucht den Leser in die Lage zu versetzen, eine fundierte Antwort
auf die Frage, was eine funktionale Sprache ist zu geben.
Dabei wird beispielhaft die funktionale Sprache Haskell näher betrachtet. Es werden jedoch keine
Kenntnisse in funktionaler Programmierung vorausgesetzt, wobei Grundbegriffe der Informatik und
Mathematik bekannt sein sollten.
In dem ersten Teil der Arbeit, sollen dem Leser die wichtigsten Grundkenntnisse funktionaler
Programmierung in Haskell vermittelt werden. Diese sind auch von Nöten, um sich von den Themen, im
zweiten Teil der Arbeit eine Vorstellung machen zu können.
Wie bereits erwähnt hat diese Seminararbeit einen einführenden Charakter. Die einzelnen Themen können
daher nur kurz dargestellt werden, für die Vertiefung wird auf weiterführende Literatur verwiesen.
An Literatur sollte "The craft of functional programminmg" von Thompson besonders erwähnt werden.
Thompson führt sehr behutsam in die Sprache Haskell ein und zeigt dabei die charakteristischen
Merkmale funktionaler Programmierung auf. Für diese Seminararbeit ist außerdem "Grundlagen
funktionaler Programmierung" von Martin Erwig, so wie das Skript, von Frau Prof. Dr. Loogen, zur
Vorlesung "Deklaratives Programmieren WS 03/04", der Universität Marburg von Bedeutung. Natürlich
muss darüber hinaus auch die Webseite www.haskell.org erwähnt werden, welche sehr umfangreiche
Informationen über Haskell gibt.
2
II Grundlagen funktionaler Sprachen
1 Funktionale Sprachen und „Rapid-Prototyping“
Wie in der Einleitung schon gesagt eignen sich funktionale Sprachen sehr gut für das „Rapid-Prototyping“
im Rahmen der Softwareentwicklung. Der Grund liegt darin, dass es relativ leicht ist die algorithmische
Arbeitsweise eines funktionalen Programms zu verstehen, auch wenn keine Vorkenntnisse in funktionaler
Programmierung bzw. in der Sprache Haskell vorhanden sind. Um dies zu demonstrieren, soll nun gleich
zu beginn ein Haskell Programm und ein C Programm verglichen werden.
Quicksort in Haskell (zum Sortieren einer Liste):
qsort []
= []
qsort (x:xs) = qsort less_then_x ++ [x] ++ qsort greater_then_x
where
less_then_x
= [y | y <- xs, y < x]
greater_then_x = [y | y <- xs, y >= x]
Quicksort in C (zum Sortieren einer Liste)
qsort( a, lo, hi ) int a[], hi, lo;
{
int h, l, p, t;
if (lo < hi) {
l = lo;
h = hi;
p = a[hi];
do {
while ((l < h) && (a[l] <= p))
l = l+1;
while ((h > l) && (a[h] >= p))
h = h-1;
if (l < h) {
t = a[l];
a[l] = a[h];
a[h] = t;
}
} while (l < h);
t = a[l];
a[l] = a[hi];
a[hi] = t;
qsort( a, lo, l-1 );
qsort( a, l+1, hi );
}
}
3
Vergleicht man das Haskell Programm und das C Programm, fällt sofort auf, dass das Haskell Programm
mit viel weniger Zeilen auskommt als das C Programm. Das C Programm ist ohne Kenntnisse der Sprache
C nur schwer zu verstehen. Dagegen erinnert die Syntax des Haskell Programms, zum teil stark an
mathematische Notationen, was enorm zum Verständnis beiträgt.
In der ersten Zeile des Haskell Programms wird der Fall der leeren Liste „[]“ behandelt, diese ist schon
sortiert und wird als Resultat zurückgegeben. In der zweiten Zeile wird die nicht leere Liste „(x:xs)“
behandelt. Dabei ist x das erste Listenelement und xs die Restliste. Auf der rechten Seite der
Definitionsgleichung kann man genau die Idee des Quicksortalgorithmus erkennen. Das erste
Listenelement x wird als Pivo-Element genommen. Links davon wird eine Liste „less_then_x“ angehängt
in der alle Elemente kleiner als x sind, jedoch unsortiert. Auf diese Liste wird qsort rekusriv angewendet.
Analog wird rechts vom Pivo-Element eine Liste „greater_then_x“ angehängt. Die Listen „less_then_x“
und „greater_then_x“ werden in den unteren Zeilen erzeugt. Die dazugehörende Syntax entspricht der
Mengendefinition in der Mathematik.
Bevor nun genauer der Aufbau und die Arbeitweise eines Funktionalen Programms erläutert wird, soll
erst eine Einordnung in das Sprachspektrum vorgenommen werden, um einen Überblick über die
verschiedenen Konzepte von Programmiersprachen zu bekommen.
2 Einordnung in das Sprachspektrum
Das weite Spektrum der Programmiersprachen, lässt sich grob in zwei Kategorien aufteilen. Zum einen
gibt es die imperativen Sprachen und zum anderen die deklarativen Sprachen.
Die imperativen Sprachen werden nochmals in konventionelle Sprachen, wie Pascal oder Fortran und in
die populären objektorientierten Sprachen wie, C++ oder Java aufgeteilt.
Die Basis imperativer Sprachen bildet das Prinzip des „von Neumann“, bei dem Programme aus Folgen
von Befehlen bestehen, die konsequent nacheinander ausgeführt werden. Damit werden Berechnungen als
Abfolge lokaler Wertzuweisungen beschrieben. Die Hauptkontrollstruktur ist dabei die While-Schleife.
Auch die deklarativen Sprachen werden nochmals aufgeteilt. Zum einem in die logik Sprachen, deren
bekanntester Vertreter PROLOG ist und zum anderen in die funktionalen Sprachen wie ML, oder die im
folgenden beschriebene Sprache Haskell. Außerdem gab es Versuche, die funktionalen Sprachen und
logik Sprachen in eine Sprache zu integrieren. Zu diesen so genannten funktional-logischen Sprachen
gehören Ideal oder Curry.
Die Merkmahle deklarativer Sprachen beruhen auf einer rechnerunabhängigen Fundierung. Im
Unterschied zu den imperativen Sprachen, ist die Basis eine mathematische Theorie. Berechnungen
werden daher nicht als Abfolge von Wertzuweisungen, sondern als Manipulation von Werten beschrieben.
Da es keine Zuweisungen gibt, sind Kontrollstrukturen wie while, repeat, usw. sinnlos. Die
Hauptkontrollstruktur ist die Rekursion.
Wie schon in der Einleitung erwähnt soll im weiteren als Beispiel die funktionale Sprache Haskell benutzt
werden. Die Mathematische Theorie bildet hier das Lambda-Kalkül.
4
3 Das Lambda-Kalkül
Das Lambda-Kalkül ist die Basis funktionaler Programmiersprachen. Alonzo Churche und Stephen
Kleene entwickelten dieses Kalkül in den 30 Jahren zur Formalisierung des Funktionenbegriffs. Mit dem
Lambda-Kalkül definierten sie, was eine Funktion ist. Dabei stellten sie mit den Regeln zur Manipulation
von Werten gleichzeitig ein vollständiges System zur Beschreibung und Ausführung mathematischer
Berechnungen und Verfahren dar, welches im Sinne des Konzepts der Berechenbarkeit ebenso mächtig ist
wie die modernen Programmiersprachen.
Funktionale Programmiersprachen sind daher genau genommen nur syntaktische Verzierungen des
Lambda-Kalküls, welche den Umgang mit den Regeln vereinfachen, an der Mächtigkeit des Kalküls
jedoch nichts ändern.
Eine Betrachtung der Definition des Lambda-Kalkül, würde an dieser Stelle, auf Grund des Umfangs zu
weit gehen und ist für das weitere Verständnis auch nicht nötig. Eine ausführliche Definition findet man
bei Martin Erwig.
4 Das Programm als Menge von Funktionen
„Die funktionale Programmierung bezieht einen Großteil ihrer Attraktivität aus der Tatsache, dass der ihr
zugrunde liegende Funktionenbegriff sehr eng an den aus der Mathematik angelehnt ist.“ [Erwig S. 7].
Auch wenn die Definition des Lambda-Kalkül ausgelassen wurde, soll daher nun eine
umgangssprachliche Definition einer Funktion folgen, wie es für das Verständnis funktionaler
Programmierung ausreichend ist.
Eine Funktion ist eine Abbildungsvorschrift, welche eine oder mehrere Eingaben (Parameter) in eine
eindeutige Ausgabe (Resultat) umwandelt. Die Regeln der Abbildungsvorschrift werden in der
Funktionsdefinition festgelegt. Die Anwendung der Abbildungsvorschrift wird Funktionsapplikation
genannt.
Ein funktionales Programm besteht aus einer Menge, oder besser gesagt einer Folge von
Funktionsdefinitionen und Funktionsapplikationen. Die Funktionen lassen sich wie man es aus der
Mathematik gewohnt ist, wie folgt definieren: f:: A -> B
Eine Funktion zur Addition zweier natürlicher Zahlen hat damit in Haksell folgende Syntax:
add :: Int -> Int -> Int
add x1 x2 = x1 + x2
Die erste Zeile der Definition ist die Typdeklaration, dabei werden Definitionsbereich und Wertrebereich
festgelegt. In der zweiten Zeile werden die beiden Parameter mit dem „+“ Operator addiert, dabei ist die
rechte Seite der Gleichung gleichzeitig das Resultat der Funktion.
Auch Programmverzweigungen erinnern stark an die mathematische Notation. So gibt es kein
Schlüsselwort wie beispielweise „if“ mit dem eine Programmverzweigung realisiert werden kann. Hierfür
wird eine Fallunterscheidung benutzt deren Syntax im folgenden Beispiel zu sehen ist.
maxi :: Int -> Int ->
Int
maxi n
m | n >= m = n
| otherwise = m
5
Diese Funktion liefert das Maximum der beiden Parameter n und m. Trift n >= m zu, ist n das Resultat,
andernfalls ist m das Resultat.
Eine Funktionsapplikation hat in Haskell folgende Syntax.
add 3 5
Hier wird die oben definierte Funktion „add“ auf die Parameter 3 und 5 angewendet. Das Resultat ist 8.
Zu beachten ist, dass eine Applikation mit Ausklammern notiert wird. Man schreibt (f x y) anstelle von
f(x, y).
An dieser Stelle gilt es darauf hinzuweisen, dass es noch eine weitere Art gibt, Funktionen zu definieren,
die des pattern matching. Um diese Art der Funktionsdefinition sinnvoll darzustellen, muss man
Datenstrukturen betrachten. Dies geschieht im folgenden Kapitel.
Zunächst sollen noch zwei Kernkonzepten der funktionalen Programmierung betrachtet werden. Zum
einem die Rekursion und zum anderen Funktionen höherer Ordnung.
Wie bereits erwähnt, ist die Rekursion die Hauptkontrollstruktur. Bei einer Rekursion werden die Werte
von Argumenten, d.h. die Parameter, auf Basisargumente zurückgeführt. Hierzu ein Beispiel zur
Berechnung der Fakultät.
fak :: Int ->
Int
fak x | x == 0 = 1
| otherwise = fak (x-1) * x
Im Falle, dass x ungleich 0 ist, wird fak erneut aufgerufen, allerdings wird zuvor (in der Klammer) der
Parameter x um 1 verringert. Gleichzeitig wird die rekursiv aufgerufene Funktion fak mit dem aktuellen
Parameter x multipliziert. Dieser Vorgang wiederholt sich so lange, bis der Parameter x dem Wert 0
entspricht. Dies ist der Basiswert, welcher als Resultat 1 liefert.
In funktionalen Sprachen werden Funktionen und Werte gleichgestellt, d.h. Funktionen, sind so genannte
„first class citizens“ und werden wie basiswerte oder Datenstrukturen behandelt. Sie können als
Parameter, oder als Resultat anderer Funktionen auftreten. Dies kennt man auch aus der Mathematik, z.B.
bei der Komposition zweier Funktionen. Dadurch wird die Definition allgemeiner Berechnungsschemata
möglich und ein modularer Programmierstil unterstützt. Solche Funktionen werden als Funktionen
höherer Ordnung bezeichnet. Ein Beispiel ist die mehr oder weniger sinnvolle aber dafür leicht
verständliche Funktion twice:
twice :: (a -> a) -> a -> a
twice
f
x = f (f x)
Die Funktion twice nimmt zwei Argumente, eine Funktion f und einen Basiswert x. Die Funktion f wird
zweimal auf x angewendet.
Erwähnt werden sollte noch, das es in Haskell die Möglichkeit gibt Funktionen partiell anzuwenden, d.h.
es wird zugelassen, dass eine Funktion auf weniger Argumente angewendet wird als zur Auswertung
notwendig ist. Weiterführend wird hier auf Thompson verwiesen.
6
5 Datenstrukturen
In funktionalen Sprachen werden Datenstrukturen mit Datenkonstruktoren gebildet. Sie sind Funktionen,
die frei interpretiert werden, d.h. das Ergebnis der Applikation eines n-stelligen Konstruktors K auf
Ausdrücke a1…an ist die Struktur K( a1…an).
In jeder funktionaler Sprache, ist die Liste als Datenstruktur vordefiniert. Listen werden mit Hilfe von
zwei Konstruktoren gebildet. Zum einem mit der Konstruktorkonstante Nil, zur Darstellung der leeren
Liste und des weiteren mit dem Konstruktor Cons, der bei Applikation auf ein Listenelement und einer
Liste, eine neue Liste durch hinzufügen an den Anfang erzeugt.
Die Listenkonstruktoren Nil und Cons haben einen polymorphen Typ, d.h. für den Typ der Listenelemente
werden Typvariablen verwendet. Als Typvariablen können in Haskell beliebige Bezeichner, die mit einem
Kleinbuchstaben beginnen benutzt werden.
Nil hat den Typ [ ] :: [t] für beliebige Typen t. Damit kann die leere Liste einfach mit [ ] dargestellt
werden. Cons hat den Typ (:) :: t -> [t] -> [t] für beliebige Typen t. Eine Liste wird damit mit (x:xs)
bezeichnet, wobei x das vorderste Listenelement (Head) vertritt und xs die Restliste (Tail) darstellt. [vergl.
Loogen S. 25] Mit diesen Konstruktoren ist es nun einfach eine Funktion zu definieren, welche ein
Element an eine Liste anfügt.
-- An der Typdeklaration sieht man, dass der Funktion ein Element a und eine Liste [a]
-- übergeben wird, das Resultat ist eine Liste.
list :: a -> [a] -> [a]
list x [] = [x]
list x (y:ys) = (x:y:ys)
In der zweiten Zeile der Definition, wird falls die übergebene Liste leer ist, eine Liste mit einem Element
erzeugt. In der dritten Zeile wird der Cons Konstrukor zweimal hintereinander angewendet. Erst zwischen
y:ys und dann zwischen x:(y:ys) .Die Klammern entsprechen hier nicht der Haskell Notation, sie sollen
vielmehr die Rechtsassoziativität von Cons verdeutlichen.
An der obigen Funktionendefinition fällt auf, dass die Funktion list durch mehrere Gleichungen, welche
verschiedene Fälle abdecken definiert wird. Diese Art Funktionen zu definieren wird als pattern matching
bezeichnet. Die Reihenfolge der Gleichungen bei pattern matching ist wichtig, da diese in der angegebnen
Reihenfolge getestet werden und das Parametermuster der zweiten Gleichung allgemeiner ist als das erste.
Pattern matching kann oft auch durch Fallunterscheidungen ersetzt werden, doch ist gerade bei
Datenstrukturen das pattern matching der schönere Programmierstil.
Für Funktionen über Listen werden in der Regel zwei Definitionsgleichungen benötigt, eine welche den
Fall der leeren Liste abdeckt und eine welche den Fall der nicht-leeren Liste mit Parametermuster (x:xs)
abdeckt.
Im folgenden Beispiel wird die Länge einer Liste ermittelt. Die Funktion hat den polymorphen Typ a und
bildet auf eine natürliche Zahl ab. Sie ist durch pattern matching definiert und ruft sich selbst rekursiv auf.
length :: [a] -> Int
length
[] -> 0
length (x:xs) -> 1 + length xs
Nach dem gleichen Prinzip wie Listen definiert sind, kann man auch eigene Konstruktoren definieren,
diese werden algebraische Datenstrukturen genannt. Solche Datenstrukturen sollen hier aber nicht vertieft
werden. Die Deklaration und Anwendung wird bei Thompson ausführlich erklärt.
7
III Merkmale Funktionaler Sprachen
1 Referentielle Transparenz
Nach der kurzen Einführung in die Grundlagen der funktionalen Programmierung in Kapitel II, werden
nun die Merkmale funktionaler Sprachen betrachtet. Hier wird der grundlegende Unterschied zu
imperativen Sprachen deutlich und die bedeutendste Antwort, auf die Fragen was eine funktionale
Sprache ist, gegeben.
Diese Frage kann mit dem Schlagwort referentielle Transparenz kurz beantwortet werden. Die
referentielle Transparenz ist die fundamentale Eigenschaft, auf der sich alle anderen Merkmale
funktionaler Sprachen aufbauen. Referentielle Transparenz bedeutet, dass der Wert eines Ausdrucks nur
von seiner Umgebung und nicht vom Zeitpunkt seiner Auswertung abhängt. Das Resultat einer Funktion
ergibt sich damit nur aus den Parametern. Dieses Verhalten entspricht dem, von Funktionen in der
Mathematik.
Um das Verständnis von referentieller Transparenz zu konkretisieren, lohnt es sich ein Gegenbeispiel zu
betrachten.
Bei imperativen Sprachen gilt dieses Prinzip der referenziellen Transparenz nicht. Hier ist der Wert eines
Ausdrucks vom Zeitpunkt seiner Auswertung abhängig. So ist z.B. folgende Wertzuweisungsoperation
erlaubt:
i = i + 1;
Diese Gleichung stellt jedoch mathematisch betrachtet eine Gleichung dar, die nicht erfüllbar ist. Noch
deutlicher wird die zeitliche Abhängigkeit bei imperativen Programmen, wenn Seiteneffekte entstehen,
wie z.B. bei folgendem Java-Programm:
class SEffekt {
int count=0;
public int f ( int x ) {
return ++count;
}
}
Die Funktion f erhöht bei jedem Aufruf die globale Variable um eins. Auch wenn ein imperativer
Programmierer dieses Konstrukt für ganz normal hält, hat es einen entscheidenden Nachteil: Mann kann
das Resultat dieser Funktion für einen beliebigen Zeitpunkt nicht vorhersagen. Da der Wert von f vom
vorhergehenden Aufruf abhängt, gilt hier nicht f(1)==f(1)!
In funktionalen Sprachen bestehen Programme grundsätzlich aus einer Reihe von Werte- und
Funktionsdefinitionen. Es gibt keine Zuweisungen an Variablen. Die durch die referentielle Transparenz
erreichte mathematische Fundierung, bewirkt eine wohldefinierte formale Semantik von Programmen.
Damit ist es möglich Korrektheitsbeweise, sowie Analysen von Programmeigenschaften durchzuführen.
8
2 Programmverifikation
Die referentielle Transparenz ermöglicht, dass Ausdrücke durch „gleichwertige“ andere Ausdrücke ersetzt
werden können, ohne daß dies Auswirkungen auf die Semantik des Programms hat. Dieses, so genannte
Substitutionsprinzip bildet die Basis für die Programmverifikation.
Durch die Verifikation kann die Korrektheit eins Programms mathematisch bewiesen werden. Dies
geschieht mit Hilfe der vollständigen Induktion. So kann z.B. die Korrektheit, der oben beschriebenen
Funktion fak, zur Berechnung der Fakultät leicht nachgewiesen werden.
Behauptung: Die folgende Funktion berechnet die Fakultät von k
-- Fakultätsfunktion
fak :: Int ->
Int
fak x | x == 0 = 1
| otherwise = fak (x-1) * x
Induktion über k:
Induktionsanfang:
k=0 => 0! = 1 und fak 0 = 1
Induktionsannahme: gilt k! so auch fak k, für k > 0
Induktionsschritt:
<=>
<=>
<=>
k = k +1
(k + 1)!
(k + 1) * k!
(k + 1) * fak k
fak (k + 1)
Somit ist die Behauptung bewiesen!
Analog zum Prinzip der vollständigen Induktion für natürliche Zahlen, wird das Prinzip der
Listeninduktion zum Beweis von Aussagen über endliche Listen eingesetzt. Listen sind wie folgt induktiv
über dem Grundtyp t definiert:
1. [] ist Liste über t ([] :: [t])
2. für beliebige Elemente x :: t und xs :: [t] ist auch (x:xs) eine Liste über t
3. für alle x :: t, xs :: [t] gilt: (x:xs) ungleich []
4. nur die mit 1. und 2. erzeugten Elemente sind Listen über t
Ein einfaches Beispiel für die Listeninduktion ist der Nachweis der Assoziativität der Listenverkettung.
Die Listenverkettung ist in Haskell eine vordefinierte infix Funktion, die wie folgt definiert ist:
-- Die Klammern um den Funktionsbezeichner, bedeuten das ++ infix ist
(++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x:xs) ++ ys = x:(xs++ys)
Behauptung: ++ ist assoziativ, d.h. für beliebige endliche Listen xs, ys, zs :: [a] gilt:
xs ++ (ys ++ zs) = (xs ++ ys) ++ zs
9
Listeninduktion über xs:
Induktionsanfang: xs = []
=> [] ++ (ys ++ zs) =
ys ++ zs
= ([] ++ ys) ++ zs
Induktionsannahme: xs ++ (ys ++ zs) = (xs ++ ys) ++ zs
Induktionsschritt: xs = x:xs`
(x:xs`) ++ (ys ++zs) = x:(xs` ++ (ys ++ zs))
= x: ((xs’ ++ ys) ++ zs)
=(x: (xs’ ++ ys)) ++ zs
=((x:xs`) ++ ys) ++ zs
Somit ist die Behauptung Bewiesen!
3 Ein-/ Ausgabe
Wie gerade gesehen wird mit der referentiellen Transparenz ein enormer Vorteil gegenüber imperativen
Sprachen erreicht. Es ist jedoch nicht leicht die referentielle Transparenz für die Ein-/ Ausgabe aufrecht zu
erhalten.
Mit Ein-/ Ausgabeaktionen sind in jeder Programmiersprache immer imperative Anweisungen verbunden.
Umgangsprachig definiert läuft eine interaktive Ein-/ Ausgabe wie folg ab:
1. lese ein Zeichen
2. mache etwas damit
3. gib es wieder aus
Für funktionale Sprachen stellt dies ein schwieriges Problem dar, da diese imperativen Anweisungen die
referentielle Transparenz verletzen würden und Seiteneffekte bewirken könnten.
Haskell ist eine Funktionale Sprache bei der man es geschafft hat diese Problem zu lösen. Dafür wird ein
Mechanismus zur Kapselung von Berechnungen verwendet, die so genannte I/O-Monade. Diese
vermittelt zwischen der funktionalen Welt und der imperativen Welt, wobei der Übergang eine
Einbahnstraße ist. Funktionale Werte können in Monaden eingebettet werden, aber aus einer Monade
können keine Werte in die in Funktionale Welt übertragen werden.
In der Monade selbst, wird eine sequentielle Ausführungsreihenfolge für Ein-/Ausgabeaktionen festgelegt.
Diese muss in jeder Implementierung eingehalten werden.
Die Grundlegenden Deklarationen für I/O-Monaden sind:
und
data IO a
-- I/O-Aktionen, die einen Wert vom Typ a liefern
return :: a -> IO a
-- Um Werte in eine Monade einzubetten, wird diese Funktion verwendet
An den Deklarationen ist zu erkennen, das IO ein Typ ist, mit dem Monaden repräsentiert werden.
10
Ein besseres Bild vom Umgang mit Monaden bekommt man, wenn man die vordefinierten Funktionen
betrachtet.
Elementare vordefinierten Ein-/Ausgabefunktionen sind:
putChar
putStr
getChar
getLine
:: Char -> IO ()
:: String -> IO ()
:: IO Char
:: IO String
Die Funktionen putChar und putStr geben ein Zeichen bzw. eine Zeichenkette auf der Standardausgabe
aus und liefern das leere Tupel () als Resultat. Die Funktionen getChar und getLine lesen ein Zeichen bzw.
eine Zeichenkette von der Standardeingabe und liefern dieses als Resultat.
Die Funktion putStr kann mit Hilfe der putChar Funktion auch leicht selbst definiert werden:
putStr
putStr []
putStr (c:cs)
:: [Char] -> IO ()
= return ()
= do putChar c
putStr cs
Dieses Beispiel zeigt, obwohl es schwierig ist, die referentielle Transparenz bei I/O aufrecht zu erhalten,
das programmieren mit I/O-Aktionen nicht schwer ist. Gerade für die I/O gibt es eine Reihe von StandardBibliotheken für Haskell, aus denen man Module importieren kann. Auf der Webseite
<http://www.haskell.org/libraries/#guis>
existiert eine Übersicht über die bisher existierenden Bibliotheken.
4 Auswertung
Im folgenden Kaptitel macht sich die referentielle Transparenz funktionaler Sprachen wieder vorteilhaft
bemerkbar. Da keine Seiteneffekte auftreten, ist die Auswertungsreihenfolge von Ausdrücken nicht von
Bedeutung. Es kommt daher nur darauf an, ob es überhaupt eine erfolgreiche Auswertung gibt.
Die Auswertungsstrategie bestimmt, in welcher Reihenfolge die einzelnen Teilausdrücke (Redexe)
ausgewertet werden. Es gibt zwei Standartstrategien: Zum einem die „leftmost innermost“ und zum
anderen die „leftmost outermost“ Strategie.
Wie aus dem Namen vielleicht schon hervor geht, wird bei der „leftmost innermost“ zuerst dieses Redex
ausgewertet, welches am weitesten innen vorkommt und unter diesen am weitesten links steht.
Bei der „leftmost outermost“ wird dem entsprechend zuerst das am weitesten außen stehende und davon
das am weitesten links stehende Redex ausgewertet. Im folgenden ein Beispiel zur Veranschaulichung.
(length ((fak 2) : (fac 3))
Bei dem obigen Ausdruck wird mit der „leftmost innermost“ Strategie zuerst (fak 4) ausgewertet, dann
(fak 7) und dann (length [2,6]) .
Mit der „leftmost outermost“ Strategie wird zuerst (length ), dann (fak 2) und dann (fac 3) ausgewertet.
11
Es fällt auf, dass bei der „leftmost outermost“ Stratgie die Länge der Liste schon nach dem ersten
Auswertungsschritt ermittelt wurde, die folgenden Schritte sind dafür nicht nötig und werden daher auch
nicht mehr ausgewertet. Hier handelt es sich um eine so genannte bedarfsgesteuerte Auswertung (Lazy
Evaluation).
Bei funktionalen Sprachen, wird im Gegensatz zu den imperativen Sprachen, die bedarfgesteuerte
Auswertung realisiert. Man muss jedoch klarstellen, dass die bedarfsgesteuerte Auswertung nicht
effizienter ist, da die Verwaltung, der nicht ausgewerteten Ausdrücke, einen hohen Aufwand hat. Es
ergeben sich jedoch andere sehr nützliche Vorteile. So ist z.B. die Verarbeitung partieller Informationen
erlaubt, d.h. es ist möglich mit noch nicht ausgewerteten Ausdrücken zu Rechnen.
Ein anderer sehr interessanter Vorteil, ist das Arbeiten mit unendlichen Datenstrukturen. Folgende
Fibonacci-Funktion ist damit in Haskell möglich:
fib :: Int -> Int -> [Int]
fib n
m = n : fib m (n+m)
12
IV Fazit
„Functional languages take another large step towards a higher-level-programming model. Programs
are easier to design, write and maintain, but the languages offers the programmer less control over the
machine.
For
most
programs
the
result
is
perfectly
acceptable.“
[http://www.haskell.org/aboutHaskell.html]. Zu diesem Fazit kommt der Autor der Haskell.org Webseite
nach einem Vergleich von funktionalen und imperativen Sprachen.
Ob sich der Einsatz funktionaler Sprachen lohnt, hängt also vom Problem welches gelöst werden soll ab.
Ein Studie von Ericsson hat allerdings ergeben, dass mit der Einführung der funktionalen Sprache Erlang
die Programmierproduktivität, bei der Erstellung von Telekommunikationssoftware, um einen Faktor 9 bis
25 gesteigert werden konnte.
Abgesehen vom kommerziellen Nutzen funktionaler Sprachen, kann der Einsatz in Schulen und
Universitäten, didaktisch sinnvoll sein. Denn eine Auseinandersetzung mit deklarativer Programmierung,
trägt auch beim imperativen Programmieren zu einem schöneren Programmierstil bei. So kann z.B. die
Rekursion zu einem selbstverständlichen Werkzeug werden. Ein guter Programmierstil, ist wiederum aus
kommerzieller sicht nicht zu unterschätzen, da somit eine effizientere Wartung der Software garantiert
werden kann.
Ein Aspekt der aktuellen Forschung ist die implizite Parallelität. Die referentielle Transparenz
funktionalen Sprachen ermöglicht, dass unabhängige Programmteile auch unabhängig ausgewertet werden
können. In diesem Bereich arbeitet Frau Prof. Dr. Loogen an der Universität Marburg.
In diese Bereiche der Forschung, konnte in dieser Seminararbeit, auf Grund des Umfangs nicht
eingegangen werden. Viel mehr sollte die Idee der funktionalen Programmierung dargestellt werden.
Diese, ist die referentielle Transparenz, welche nach einem längeren Umgang mit funktionalen Sprachen,
für den Programmierer sehr komfortabel werden kann.
13
Literaturverzeichnis
Bücher
Erwig, Martin. Grundlagen funktionaler Programmierung; München, 1999
Gumm, Heinz-Peter. Einführung in die Informatik; München, 2002
Loogen, Rita. Praktische Informatik III: Deklarative Programmierung; Marburg, WS 2003/2004
Thompson, Simon. Haskell Craft of Functional Programming 2.Edition; Edingburgh, 1999
Internet
<http://www.haskell.org>
<http://www.wikipedia.de>
14
Herunterladen