published
tags: Haskell Programmieren funktional

Ein Semester funktional Programmieren mit Haskell - Fazit

Im Rahmen einer Vorlesung im vergangenen Wintersemester stand funktionale Programmierung mit Haskell auf dem Plan. Ich selbst und viele meiner Kommilitonen hatten nie den Kontakt zu einer rein funktionalen Programmiersprache und ich hatte zuvor durchaus meine Zweifel am funktionalen Konzept - konnte mir ehrlich gesagt aber auch nicht viel Konkretes darunter vorstellen, schon gar nicht warum ich das haben wollte. Im Nachhinein muss ich gestehen, dass mich das funktionale Denken durchaus in gewisser Hinsicht geprägt hat. Ich wünsche mir seitdem oft funktionale Konzepte in nicht-funktionalen Sprachen und denke auch teilweise öfter in funktionalen Lösungen - was auch in nicht-funktionalen Sprachen oft möglich ist und dem Code eine gewisse Eleganz verleiht.

Das hier soll kein Loblied auf Haskell oder funktionales Programmieren werden. Wenn ich ehrlich sein soll habe ich vor allem in der Anfangszeit, oft aber auch später extrem geflucht und mir einfach eine Schleife gewünscht. Der Code wird auch nicht automatisch schöner, weil er in einer funktionalen Sprache verfasst ist, man hat aber irgendwie oft mehr Raum für Eleganz.

Das hier soll auch keine Einführung in Haskell werden. Das können andere viel besser als ich. Das Haskell Wiki bietet eine umfangreiche Übersicht über Lehrmaterialien, Learn you a Haskell for Great Good! ist auch desöfteren bei der Recherche über diverse Probleme positiv aufgefallen. Es soll auch recht gute Offline-Literatur (Bücher) geben, da kann ich jedoch keine Empfehlung aussprechen. Die Vorlesung war an das Buch Haskell - The Craft of Functional Programming angelehnt, welches uns auch empfohlen wurde. Ohne geht es jedenfalls auch.

Ich will in diesem Post hauptsächlich auf ein paar Features eingehen, die man sonst vielleicht noch nicht gesehen hat und ich sehr zu schätzen gelernt habe. Einfach ein wenig Code, was er macht und warum das unter Umständen ziemlich cool ist. Vielleicht bekommt ja der eine oder andere Lust auf Haskell/FP und schaut sich die Sache mal genauer an, was ich nur wärmstens empfehlen kann.

Hinweis: Falls nicht anders angegeben sind alle Codebeispiele im interaktiven Haskell-Interpreter ghci ausgeführt worden (erkennbar an Zeilen, die mit Prelude> beginnen). Kommentare werden mit -- eingeleitet.

Listen

Naja, auf den ersten Blick nicht so cool. Kennt man und hat man sicherlich schon oft genug selbst geschrieben. Das schöne daran ist die syntaktische Integration in die Programmiersprache sowie die vielen vorhandenen Funktionen, um mit Listen zu arbeiten. Vor allem im Zusammenhang mit Funktionen höherer Ordnung, List Comprehensions und Pattern Matching entfalten sie erst wirklich ihre Wirkung. In Haskell kann man zudem prinzipiell mit unendlich großen Listen hantieren, was manchmal extrem praktisch ist.

Prelude> [1,2,3] -- Eine Liste mit den Zahlen 1,2,3
[1,2,3]
Prelude> sum [1,2,3] -- Die Summe der Zahlen
6
Prelude> [1..10] -- Alle Zahlen von 1 bis 10
[1,2,3,4,5,6,7,8,9,10]
Prelude> length [1..10] -- Länge der Liste
10
Prelude> head [1..10] -- Erstes Element der Liste
1
Prelude> tail [1..10] -- Alle Elemente bis auf das Erste
[2,3,4,5,6,7,8,9,10]
Prelude> head [1..] -- Erstes Element der Liste von 1 bis unendlich
1
Prelude> take 3 [1..10] -- Die ersten drei Elemente der unendlichen Liste
[1,2,3]

Wichtig ist im Zusammenhang mit Listen vor allem die Überlegung, wie eine Liste aufgebaut ist:

Prelude> []
[]
Prelude> 1:[]
[1]
Prelude> 1:2:[]
[1,2]

Der cons Operator (:) beschreibt dabei das Zusammenspiel zwischen head (erstes Element) und tail (Rest) der Liste. Im ersten Beispiel haben wir lediglich eine leere Liste. Im zweiten Beispiel ist der head die 1 und tail die leere Liste. Im dritten Beispiel ist die 1 head der Liste, die als tail wiederum eine Liste mit 2 als head und der leeren Liste als tail besitzt. Warum das relevant ist, wird später beim Pattern Matching klar.

List Comprehension (LC)

Wem Listen zu langweilig und vielleicht auch bisher ein wenig zu unflexibel waren, der wird vielleicht an List Comprehensions mehr Freude finden. Da es so gesehen keine Schleifen gibt, sind LCs eine Möglichkeit, über eine Liste zu iterieren. Natürlich ist das nicht der einzige Eisatzzweck, vielmehr kann man mit LCs Listen sehr flexibel konstruieren und modifizieren.

Prelude> [x * x | x <- [1..10]]
[1,4,9,16,25,36,49,64,81,100]
Prelude> import Data.Char
Prelude Data.Char> [toUpper x | x <- "Hallo Welt"] -- String ist in Haskell lediglich eine Liste von Chars
"HALLO WELT"
Prelude Data.Char> 

Natürlich kann man auch auf mehreren Listen arbeiten. Dabei verhält sich die LC wie mehrere geschachtelte Schleifen:

Prelude Data.Char> [x + y | x <- [0..5], y <- [10..15]]
[10,11,12,13,14,15,11,12,13,14,15,16,12,13,14,15,16,17,13,14,15,16,17,18,14,15,16,17,18,19,15,16,17,18,19,20]
Prelude Data.Char> ["x: " ++ (show x) ++ " y: " ++ (show y) | x <- [0..2], y <- [3..7]]
["x: 0 y: 3","x: 0 y: 4","x: 0 y: 5","x: 0 y: 6","x: 0 y: 7","x: 1 y: 3","x: 1 y: 4","x: 1 y: 5","x: 1 y: 6","x: 1 y: 7","x: 2 y: 3","x: 2 y: 4","x: 2 y: 5","x: 2 y: 6","x: 2 y: 7"]

Man sieht auch, dass die beiden Listen nicht gleich groß sein müssen. In List Comprehensions kann man auch Bedingungen anführen, die Elemente erfüllen müssen:

Prelude Data.Char> [x | x <- [1..10], x > 5, x < 8]
[6,7]
Prelude Data.Char> [x | x <- "Ein Text mit ganz(!!1!) vielen Sonderzeichen??!.", isLetter x]
"EinTextmitganzvielenSonderzeichen"

Ich denke das Prinzip ist klar. Da man in Haskell recht viel mit Listen arbeitet ist die List Comprehension ein sehr elegantes und mächtiges Werkzeug für viele Aufgaben.

Pattern Matching

Pattern Matching ist eines meiner Lieblingsfeatures. Im Grunde genommen ist die Idee dahinter, den Programmfluss anhand der Struktur der gegebenen Daten statt der Daten selbst zu steuern. In dieser kurzen Übersicht werde ich das bloß mit Listen demonstrieren, Pattern Matching ist aber auch wunderbar mit anderen in Haskell vorhandenen und eigenen Typen möglich und oft ein sehr eleganter Weg zum Ziel. Da sich Pattern Matching eher schlecht im interaktiven GHCI zeigen lässt, habe ich im Folgenden eine Funktionsdefinition lokal in der Datei pm.hs abgespeichert und rufe diese Funktion dann im GHCI mit diversen Paramtern auf.

foo :: [Integer] -> Integer -- Signatur, nicht unbedingt benötigt
foo [] = 0 -- bei einer leeren Liste wird 0 zurück gegeben
foo (x:xs) = x + (foo xs) -- ansonsten werden alle Elemente rekursiv aufaddiert

bar :: [Integer] -> [Integer]
bar [] = [] -- bei einer leeren Liste als Parameter wird wieder eine leere Liste zurück gegeben
bar [x] = [] -- bei genau einem Parameter wird auch eine leere Liste zurück gegeben
bar (x:y:xs) = (x*2):(y*3):(bar xs) -- alle Elemente an einer geraden Indexposition in der Liste werden verdoppelt, die anderen verdreifacht
Prelude> :l pm.hs
[1 of 1] Compiling Main             ( pm.hs, interpreted )
Ok, modules loaded: Main.
*Main> foo [1..10]
55
*Main> foo [1..100]
5050
*Main> sum [1..10] 
55
*Main> sum [1..100]
5050
*Main> bar [1..10]
[2,6,6,12,10,18,14,24,18,30]

Mit Pattern Matching lassen sich vor allem sehr elegant Rekursionen formulieren. In diesem Beispiel habe ich nur auf Listen gearbeitet, Pattern Matching gibt es aber auch auf vielen anderen Datentypen, auch auf eigenen. Auf einem selbst definierten Baum kann man nativ und elegant mit Pattern Matching arbeiten, während man in anderen Programmiersprachen recht viele Fallunterscheidungen braucht oder mit speziellen Entwurfsmustern und Polymorphie arbeiten muss. Dinge, um die man sich meistens nicht kümmern will und die natürlich auch Code produzieren, der Fehler enthalten kann und gewartet werden muss.

Anonyme Funktionen

Auch Lambda-Funktionen genannt. Viele Sprachen - inzwischen sogar Java 8 - bieten dieses Feature. Man kann eine Funktion ohne Namen definieren. Meiner Meinung nach ist das in anderen Sprachen häufig lediglich ein Zugeständnis an die Leute, die sich mehr funktionale Features wünschen. Wirklich einen Gewinn abseits von Schreibarbeit gibt es selten, da man in Haskell ziemlich gut mit Funktionen als Parametern arbeiten kann führen anonyme Funktionen aber dennoch häufig zu ästhetischerem Code.

Prelude> (\x -> x + 3) 5
8
Prelude> (\x -> (x * 7) + 3) 6
45
Prelude> (\x -> \y -> \z-> (x + 4) * y - z) 1 2 3 -- ((1 + 4) * 2) - 3
7

Der einführende Backslash soll an das griechische Lambda erinnern, der darf bei Lambda-Funktionen natürlich nicht fehlen. Das sieht jetzt vielleicht nicht nach besserem Code hinsichtlich Lesbarkeit oder praktischem Nutzen aus, bei den Funktionen höherer Ordnung werden wir dann sehen, was uns anonyme Funktionen bringen.

Currying

Currying ist auch eines meiner liebsten Fähigkeiten von Haskell. Eine Funktion, die n Parameter erwartet, bekommt k Parameter (k < n) übergeben. Der Rückgabewert ist eine neue Funktion, die anschließend lediglich (n-k) Parameter verlangt. Mag auf den ersten Blick auch nicht sonderlich nützlich aussehen, ist aber oftmals recht praktisch:

Prelude> (+1) 2 -- Addition mit Plus-Zeichen ist in Haskell auch nicht mehr als eine Funktion, die mit Infix-Notation aufgerufen wird
3
Prelude> (|| False) True -- logisches Oder
True
Prelude> (&& False) False -- logisches Und
False

Macht wie gesagt erst Sinn, wenn man Funktionen als Parameter in andere Funktionen steckt. Nun ist es aber auch endlich so weit:

Funktionen höherer Ordnung

Funktionen höherer Ordnung wollen andere Funktionen als Parameter. Wenn man noch nie in Berührung mit funktionaler Programmierung gekommen ist hört sich das vielleicht etwas komisch an, ist aber richtig praktisch und man will es nie wieder abgeben!

Prelude> [x + 5 | x <- [1..10]] -- LC, kennen wir schon
[6,7,8,9,10,11,12,13,14,15]
Prelude> map (+5) [1..10] -- map wendet eine Funktion auf jedes Element einer Liste an. Die Funktion in diesem Beispiel ist durch Currying erzeugt worden
[6,7,8,9,10,11,12,13,14,15]
Prelude> filter (\x -> x > 5 && x < 8) [1..10] -- filter liefert eine Liste von Elementen, für die die Funktion als Parameter True zurück gibt. Die Funktion ist hier eine anonyme Funktion
[6,7]

Solche Funktionen kann man sich auch selber bauen. Und sie können auch anonym sein:

Prelude> (\x -> \y -> x y) (\z -> z + 3) 5 -- äquivalent zu '(+3) 5' oder '3 + 5'
8
Prelude> import Data.Char
Prelude Data.Char> reverse (map chr (filter even (map ord ['a'..'z']))) -- Alle Kleinbuchstaben, deren ASCII-Code eine gerade Zahl ist, in umgekehrter Reihenfolge
"zxvtrpnljhfdb"

Das sind meiner Meinung nach die wichtigsten Spracheigenschaften. Es gibt noch viel mehr praktische Dinge (Lazy Evaluation, Guards, Freiheit von Seiteneffekten, …) auf die ich nicht weiter eingehen will. Wenn man in Haskell einsteigt wird man schnell damit in Berührung kommen. Natürlich gibt es in Haskell auch einige Dinge die mich stören. Sobald man Funktionen mit Seiteneffekten braucht (Dateisystem, Netzwerk, Zufallszahlen, …) wird es mir ein wenig zu hässlich. Außerdem hat Haskell doch schon recht viel Syntax, was es vor allem Einsteigern extrem erschwert in fortgeschrittene Beispiele ohne explizite Einführung einzusteigen (ist vielleicht aufgefallen ;). Und wenn man das überwunden hat nervt einen auch später oft noch der Compiler und das im Vergleich zu nicht-funktionalen Sprachen etwas eigentümliche Typsystem. Ich kann trotzdem jeden nur Empfehlen, mal wenigstens rein zu schnuppern - es gibt nichts zu verlieren und viel zu entdecken! :)