Haskell ist eine rein funktionale Programmiersprache, benannt nach dem US-amerikanischen Mathematiker Haskell Brooks Curry, dessen Arbeiten zur mathematischen Logik eine Grundlage funktionaler Programmiersprachen bilden. Haskell basiert auf dem Lambda-Kalkül, weshalb auch der griechische Buchstabe Lambda als Logo verwendet wird. Die wichtigste Implementierung ist der Glasgow Haskell Compiler (GHC).
Entwicklung
Gegen Ende der 1980er Jahre gab es bereits einige funktionale Programmiersprachen. Um der Wissenschaft eine einheitliche Forschungs- und Entwicklungsbasis bereitzustellen, sollte eine standardisierte und moderne Sprache die funktionale Programmierung vereinheitlichen. Zunächst wollte man dazu Miranda als Ausgangspunkt benutzen, doch deren Entwickler waren nicht daran interessiert. So wurde 1990 Haskell 1.0 veröffentlicht.
Die Sprachderivate von Haskell sind zahlreich; dazu zählen Parallel Haskell, Distributed Haskell (ehemals Goffin), Eager Haskell, Eden mit einem neuen Ansatz zum parallelen Programmieren und Bedarfsauswertung, DNA-Haskell und sogar objektorientierte Varianten (Haskell++, O’Haskell, Mondrian). Des Weiteren diente Haskell beim Entwurf neuer Programmiersprachen als Vorlage. So wurde beispielsweise im Falle von Python die Lambda-Notation sowie Listenverarbeitungssyntax übernommen.
Eigenschaften Programmfluss Haskell ist eine rein funktionale Programmiersprache. Funktionen geben nur Werte zurück, ändern aber nicht den Zustand eines Programms (d. h. Funktionen haben keine Nebeneffekte). Das Ergebnis einer Funktion hängt deshalb nur von den Eingangsparametern ab, und nicht vom Kontext, in dem die Funktion aufgerufen wird (z. B. Zeitpunkt des Aufrufs). Daraus folgt, dass bei wiederholtem Aufrufen einer Funktion mit denselben Parametern auch jedes Mal dasselbe Ergebnis folgt. Dies können Haskell-Implementationen nutzen, um Funktionen zu cachen.
Es gibt keine nativen imperativen Sprachkonstrukte. Durch die IO-Monade ist es möglich, Ein- und Ausgabeoperationen und zustandsabhängige Berechnungen, wie Zufallszahlengeneratoren, rein funktional zu behandeln. Haskell stellt jedoch die sogenannte do-Notation bereit, welche es erlaubt, die Verkettung von Monaden in Stil von imperativen Sprachen darzustellen-- Die Definition einer IO aktion, welche einen Namen von der Konsole einliest -- und dann den String "Hallo <name>! Willkommen" ausgibt aktion = getLine >>= (\eingabe -> putStr ("Hallo, " ++ eingabe)) >> putStrLn "! Willkommen"
-- Eine Identische Definition mit do-Notation aktion = do eingabe <- getLine putStr ("Hallo, " ++ eingabe) putStrLn "! Willkommen"Es gibt keine Unterscheidung zwischen Variablen und Konstanten, da der Wert einer Variable in Haskell nicht verändert werden kann. Man spricht von unveränderlichen oder nicht veränderbaren Variablen. Dies ist auch der Grund, weshalb es keine Unterscheidung von Identität und Gleichwertigkeit von Variablen gibt. Durch diese Eigenschaften ist Haskell der Mathematik sehr nahe und es ist möglich, die Richtigkeit gewisser Programme zu beweisen, beispielsweise durch Vollständige Induktion.
Haskell ist nicht-strikt. Es werden nur Ausdrücke ausgewertet, die für die Berechnung des Ergebnisses gebraucht werden. first x y = x quadrat x = x * x
Die Funktion first liefert bei Eingabe zweier Parameter den ersten als Ergebnis zurück. Bei der Eingabe von first x (3+7) ist die Auswertung der Summe (3+7) zur Ergebnisbestimmung nicht notwendig, sollte also unberücksichtigt bleiben. Die Funktion quadrat berechnet bei Eingabe eines Parameters dessen Quadrat. Bei Eingabe von quadrat(3+5), was im Laufe des Auswertungsprozesses zu (3+5)*(3+5) wird, wäre eine doppelte Berechnung der Summe (3+5) ineffizient, sollte also vermieden werden. Die Auswertungsstrategie, welche die beiden eben geschilderten Probleme umgeht, wird Bedarfsauswertung () genannt und kommt in Haskell meist zum Einsatz. Die Bedarfsauswertung ist vor allem wegen der strengen Einhaltung des funktionalen Konzepts möglich. Umgekehrt macht die Bedarfsauswertung die funktionale Programmierung angenehmer, denn sie erlaubt es besser, Funktionen, die reine Berechnungen durchführen, von Ein-/Ausgabefunktionen zu trennen. Die Bedarfsauswertung erlaubt das Arbeiten mit undefinierten Werten und potentiell unendlich großen Datenmengen. So kann man elegant mit Potenzreihen, Zeitreihen (etwa Audiosignalströmen), Kettenbruchzerlegungen, Entscheidungsbäumen und ähnlichem umgehen. Aber auch bei endlichen, aber großen, oder endlichen und noch nicht vollständig bekannten Daten erlaubt diese Art der Ausführung elegante Programme. So kann man etwa eine Transformation eines XML-Dokumentes als Folge von Transformationen des gesamten XML-Baumes beschreiben. Ausgeführt wird die Gesamttransformation aber von Beginn zum Ende des XML-Dokumentes, auch wenn das Ende noch gar nicht verfügbar ist. Man beachte allerdings, dass Haskell nach Sprachdefinition lediglich nicht-strikt ist; die Bedarfsauswertung ist nur eine mögliche Implementierung der Nicht-Striktheit - die allerdings von allen gängigen Haskell-Übersetzern angewandt wird. Andere Implementierungen sind möglich (z. B. optimistic evaluation, Ennals & Peyton Jones, ICFP’03).
Typsystem Haskell ist stark typisiert. Es wird also zum Beispiel streng zwischen Wahrheitswerten, Zeichen, ganzen Zahlen, Gleitkommazahlen und Funktionen von und zu verschiedenen Typen unterschieden.
Haskell erlaubt Typvariablen. Damit können Funktionen sehr allgemein formuliert werden. Wird eine allgemeingehaltene Funktion für bestimmte Typen verwendet, werden automatisch die Typen abgeglichen (Typinferenz). Die Funktion map wendet eine beliebige Funktion auf die Elemente einer Liste an. Ihr Typ wird so angegeben: map :: (a -> b) -> [a] -> [b]
Wird map etwa mit der speziellen Funktion toUpper vom Typ Char -> Char aufgerufen, ergibt der Typabgleich map toUpper :: [Char] -> [Char]
Haskell ist von der Grundidee her statisch typisiert, obwohl es auch Erweiterungen für dynamische Typen gibt. Das bedeutet, dass für die meisten Berechnungen die Typen bereits zum Zeitpunkt der Programmübersetzung feststehen. Dies deckt viele „offensichtliche“ Fehler noch vor Ausführung des Programms auf. Haskell unterstützt Funktionen höherer Ordnung (Funktionale). Das sind Funktionen, die Funktionen als Eingabeparameter bzw. Funktionen als Ergebnis haben. Ein Beispiel ist die map-Funktion, die eine Funktion f auf jedes Element eines Datentyps (hier Liste) anwendet. map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs
map quadrat [1,2,3] = [quadrat 1, quadrat 2, quadrat 3] = [1,4,9]
Funktionen erlauben Currying. Während man in anderen Sprachen Tupel als Argumente an Funktionen übergibt, also Funktionstypen der Form (a, b) -> c verwendet, ist in Haskell die Curry-Form a -> b -> c üblicher. Damit wird die partielle Auswertung von Funktionen bequem möglich. Der Ausdruck map toUpper ist beispielsweise eine teilweise Auswertung von map, denn er beschreibt eine Funktion, nämlich die Funktion, welche alle Kleinbuchstaben einer Liste in Großbuchstaben verwandelt. Haskell erlaubt benutzerdefinierte Datentypen. Diese algebraischen Datentypen werden mit Hilfe von Datenkonstruktoren definiert. data Tree = Leaf Int | Branch Int Tree Tree
Das Beispiel zeigt die Datenstruktur eines mit ganzen Zahlen beschrifteten binären Baumes. Solch ein Baum Tree besteht entweder aus einem Blatt (Leaf Int) oder einer Verzweigung (Branch Int t1 t2), wobei t1 und t2 die Teilbäume darstellen, die wiederum die Struktur Tree haben. Zur Definition dieser Datenstruktur wurde sowohl der einstellige Konstruktor Leaf als auch der dreistellige Konstruktor Branch verwendet. Datentypen mit mehreren ausschließlich parameterlosen Konstruktoren können als Aufzählungen eingesetzt werden. data Tag = Montag | Dienstag | Mittwoch | Donnerstag | Freitag | Samstag | Sonntag deriving (Show, Eq, Ord, Ix, Enum) Haskell unterstützt Typenklassen. Mit Typenklassen lassen sich Typen zusammenfassen, welche eine bestimmte Menge an Operationen unterstützen. In Signaturen von Funktionen dürfen als Abstufung zwischen festen Typen wie Char und freien Typvariablen auch noch Typvariablen mit Einschränkung auf bestimmte Klassen verwendet werden. Alle Ausprägungen einer Methode der Typklasse tragen den gleichen Namen. In gewisser Weise entsprechen Typklassen also dem Überladen von Funktionen. Der gleiche Funktionsname steht also abhängig vom Typ für verschiedene Funktionen. Zum Beispiel ist mit der ==-Methode der Klasse Eq der Vergleich sowohl zweier Zahlen als auch zweier Texte möglich. Trotzdem arbeitet der Gleichheitstest je nach Argumenttyp anders. In Haskell haben Ein- und Ausgabefunktionen einen speziellen Typkonstruktor namens IO. putStrLn :: String -> IO () getLine :: IO String
putStrLn gibt einen Text und einen Zeilenumbruch auf der Standardausgabe aus. Da es kein informationstragendes Ergebnis gibt, wird der Einheitstyp () als Rückgabetyp verwendet. getLine liest eine Textzeile von der Standardeingabe. Der IO-Typkonstruktor stellt sicher, dass man den Nutzern der Funktion offenlegen muss, dass die Ergebnisse durch Ein-/Ausgabe gewonnen wurden. Diese strenge Handhabung ermuntert Haskell-Programmierer zur klaren Trennung von Ein- und Ausgabe und anderen Teilen eines Programms. Der größte Teil eines Haskell-Programms besteht in der Regel aus Funktionen ohne Ein- und Ausgabe. Man kann IO-Typen natürlich auch in andere Typen einbetten und so zum Beispiel einen speziellen IO-Typ definieren, der nur Eingaben erlaubt.
Syntax Haskell unterscheidet Groß- und Kleinschreibung. Bezeichner, die mit einem Großbuchstaben beginnen, stehen für Typ- und Wertkonstruktoren. Bezeichner, die mit einem Kleinbuchstaben beginnen, stehen für Typvariablen, Funktionen und Parameter.
Der Umgang mit Leerzeichen und Zeilenumbrüchen geschieht in Anlehnung an das intuitive Verständnis von mathematischer Notation, bei Zeilenumbrüchen muss lediglich eine Einrückung beliebiger Tiefe geschehen, damit der Zusammenhang nicht verlorengeht. So ist der Ausdruck fun a b = a*b völlig gleichwertig zu fun a b= a * b
Haskell unterstützt einzeilige und mehrzeilige Kommentare, erstere ab den Zeichen -- bis zum Ende der Zeile und letztere im Einschluss von {- und -}. f x = x**2 -- f y = y*5 diese Zeile ist auskommentiert {- Alles, was hier drin steht, wird auch nicht beachtet. f z = z*2
Haskell bietet eine Reihe von syntaktischen Besonderheiten. Diese sollen nicht darüber hinwegtäuschen, dass alles rein funktional erklärt ist.
Die do-Notation verleiht Berechnungen mit Monaden das Aussehen von imperativen Programmen.Statt readFile "eingabe.txt" >>= writeFile "ausgabe.txt" oder readFile "eingabe.txt" >>= (\inhalt -> writeFile "ausgabe.txt" inhalt) kann man auch do inhalt <- readFile "eingabe.txt" writeFile "ausgabe.txt" inhalt schreiben.
Sowohl symbolische Bezeichner (bestehend etwa aus +, -, *, /, >, <) als auch alphanumerische Bezeichner (Buchstaben, Ziffern und Apostroph) können für Funktionsnamen verwendet werden und sowohl als Infix-Operatoren als auch in Präfixschreibweise eingesetzt werden. Es gilt beispielsweise a + b = (+) a b a `div` b = div a b
Haskell erlaubt spezielle Notationen bei der Listenverarbeitung. So können unter anderem Zahlenfolgen mit zwei Punkten (..) angedeutet werden: [0..5] = [0,1,2,3,4,5] ['a'..'e'] = ['a','b','c','d','e'] = "abcde" [0,2..10] = [0,2,4,6,8,10] Wird kein Endwert angegeben, dann wird eine unendliche Liste erzeugt [1..] = [1,2,3 usw.] [10,20..] = [10,20,30 usw.]
Des Weiteren ist eine Notation erlaubt, genannt „list comprehension“, die an die mathematische Schreibweise für Mengendefinitionen angelehnt ist. In folgendem Beispiel wird aus der Folge der positiven natürlichen Zahlen die Folge der geraden Zahlen extrahiert. [ x | x <- [1..], even x] als Umschreibung für do x <- [1..] guard $ even x return x