Funktionale Programmierung
mit Haskell
V 21.05.01

Inhaltsverzeichnis

Vorwort: Warum Haskell?

Haskell gehört zu den wenigen ungewöhnlichen Sprachen, welche schonungslos jedes Konzept ausschließlich funktional umsetzen. Selbst bekannte Vertreter aus dem funktionalen Lager wie F#, Standard ML, Clojure oder Scala gelten nicht als rein funktional. Was aber bedeutet rein funktional? Genau dieser Frage wird man beim Lernen von Haskell nachgehen und dabei feststellen, dass viele Dinge, welche in den allermeisten Sprachen als selbstverständlich gelten, oft zu Problemen führen.

Auf die Frage, was funktionale Programmierung von anderen Paradigmen unterscheidet, antwortete Simon Peyton Jone, britischer Informatiker und einer der Hauptverantwortlichen von Haskell:

Oh, das ist einfach: Kontrolle von Wirkungen.

(original: Oh, that’s easy: control of side effects.)

aus Masterminds of Programming

Damit ein Programm nützlich ist und nicht bloß eine Art Blackbox darstellt, muss dieses irgendeinen äußeren Einfluss ausüben, beispielsweise eine Bildschirmausgabe oder das Verändern von Datensätzen. Folglich ist ohne derartige Programmeffekte, auch Wirkungen (en side effects) genannt, keine Anwendung gegeben. Jedoch führen Wirkungen zu nichtdeterministischem Programmverhalten und sind in imperativen Sprachen ursächlich für eine ganze Reihe von Problemen. So unterliegt die Werteingabe über die Tastatur der völligen Willkür eines Benutzers und steht damit diametral zu mathematischen Funktionen, welche nur von wohldefinierten sowie bekannten Parametern abhängen (Definitionsmenge), folglich stets vorhersagbare Ergebnisse liefern (Funktionsgraph). Haskell hingegen umgeht die Notwendigkeit von änderlichen Zuständen und nutzt mathematische Ansätze, um auch Ein- sowie Ausgaben funktional zu lösen, ohne dabei den vorhersagbaren Charakter seiner Funktionen zu verlieren, also zu einer unreinen Sprache zu verkommen.

Obgleich Haskell ein Akademikerspielzeug darstellt und im Gegensatz zu OCaml weniger in der Industrie verwendet wird, ist die Sprache nach gut 30 Jahren seit der Erstveröffentlichung sichtlich gereift und von einer wachsenden Nutzerschaft praktikabel gemacht worden. Als Beispiele für den Einsatz von Haskell sind die Programmbibliothek Pandoc oder das 3D-Spiel Frag zu nennen.

Überblick

Merkmal
Paradigmadeklarativ: ausdrucksorientiert, funktional, modular
SyntaxPython-artig: distinktive Einrückungen und Zeilenenden
Speicherverwaltungautomatisch
Typsystemstark, statisch, implizit, strukturell
Übersetzungsmodellkompilierend
DateiendungHS, LHS (literate Haskell – Programmtext mit LaTeX)
Ersterscheinungsjahr1990
AusgabenHaskell 1.{0, 1, …, 4}, Haskell {98, 2010, 2020 (in Planung)}
ErfinderKomitee bestehend aus ca 25 Personen; u A John Hughes, Lennart Augustsson, Paul Hudak, Simon Peyton Jones
VorläuferHope, Miranda, Orwell
beeinflusst vonClean, Gofer, Lisp, ML
AbkömmlingeIdris, PureScript, Timber
Standardisierungenkeine – akademisches Gemeinschaftsprojekt
StandardumsetzungGHC (Kompilierer) / GHCi (interaktive Umgebung)
PaketverwalterStack (empfohlen), Cabal
empfohlene IDEEclipse, Geany, IntelliJ, KDevelop, Visual Studio
offizielle Webseitenhaskell.org | wiki.haskell.org
Eckpfeiler funktionaler Programmierung in Haskell
Höhere Funktionen
Funktionen vermögen andere Funktionen als Argument zu empfangen oder als Ergebnis zurückzuliefern. Dies setzt voraus, dass Funktionen wie gewöhnliche Datenobjekte handhabbar und miteinander verknüpfbar sind.
Referenzielle Transparenz
Funktionen hängen ausschließlich von ihren Parametern ab und üben keinen äußeren Einfluss aus – wie beispielsweise das Verändern globaler Variablen.
Kapselung von Programmeffekten
Wirkungen liegen als abstrakte Gebilde bestimmten Typs vor und sind nicht rückumwandelbar in ungekapselte Werte.

Einrichten & Übersetzen

Haskell-Quelldateien enden auf hs und werden von der Standardimplementierung, dem Glasgow Haskell Compiler (GHC), in eine nativ ausführbare Binärform kompiliert. Zusätzlich besteht eine Sprachshell GHC interactive environment (GHCi), in welcher sich Module laden und testen lassen.

Empfohlen ist das Installieren von Haskell über das spracheigene Build-Tool Stack, folgend den Empfehlungen auf der offiziellen Webseite:

https://www.haskell.org/downloads/#stack

Windows Installer oder mittels chocolatey (empfohlen)
Linux hiesige Paketverwaltung

Stack vereint nicht nur GHC und GHCi in sich, sondern ermöglicht auch das Erstellen, Bereitstellen und Nutzen von Bibliotheken.

IDE

IntelliJ IDEA

Das IntelliJ plugin for Haskell bietet neben Syntaxhervorhebung viele weitere Funktionen, um das Programmieren mit Haskell äußerst komfortabel zu gestalten.

Visual Studio Code

Die offizielle Erweiterung Haskell beruht auf dem Haskell Language Server.

Stack

 

 

REPL

Über die Systembefehlszeile wird mit ghci bzw stack ghci oder stack repl die interaktive Umgebung (en read–eval–print loop) von Haskell gestartet:

Die interaktive Umgebung bietet den Vorteil, Terme sofort auswerten zu lassen und einzelne Funktionen zu testen, aber auch Angaben zur Signatur abzufragen. Damit stellt GHCi ein äußerst nützliches Werkzeug im Umgang mit Haskell dar.

Befehlsübersicht
AktionKürzel
Hilfe / Übersicht:h :?
GHCi beenden:q
Datentyp abfragen:t VALUE
Typart abfragen:k TYPE
Erweiterung einschalten:set -X…
Modul laden:l NAME

Grundsyntax

Nachfolgend einige Variablen sowie Funktionen für den Einstieg in Haskell:

2 – 4 Variablen in Haskell sind eher im mathematischen Sinne als bloße Platzhalter zu verstehen und unveränderlicher Art, sodass sofort ein Wert anzugeben ist. Der Typ muss hierbei nicht notiert werden, da Haskell die Datenart anhand des Ausdrucks durch Ableitungsregeln eigenständig ermittelt. Entsprechen schlicht fällt auch die Variablendefinition aus: NAME = AUSDRUCK
5 – 6 Funktionsdefinitionen geschehen in gleicher Weise, wobei die Parameter unmittelbar nach dem Namen anzugeben sind. Beim Aufruf der Funktion werden alle Argumente ebenfalls ohne Klammerung und Beistriche nacheinander aufgelistet. Diese Schreibweise mag zu Beginn etwas ungewöhnlich erscheinen, ist jedoch deutlich handlicher und besser zu lesen als das Klammerwirrwarr in den meisten C-Sprachen. Der genaue Hintergrund ist jedoch der, dass Funktionen in Haskell tatsächlich einstellig sind, also nur einen Parameter vorweisen. Bei scheinbar mehreren Parametern wird nach Anwendung des ersten eine neue Funktion zurückgeliefert, wobei die Klammern (mult a) 9 entfallen.
8 Mit der Funktion putStrLn wird eine Zeichenkette unmittelbar ausgegeben.
10 Haskell verfügt über die drei logischen Operatoren und &&, oder ||, sowie nicht not.
Kommentare

Blockkommentare sind nicht verschachtelbar.

Einsprungpunkt

Genauso wie in Python werden Blöcke durch Einrücken kenntlich gemacht. Eine weitere Besonderheit von Haskell ist die getrennte Betrachtung von Deklaration (3) und Definition (5). Typangaben erfolgen mittels des Operators ::, welcher soviel bedeutet wie ist vom Typ. Deklarationen sind wahlfrei, jedoch aus Gründen der Dokumentation empfohlen und bei komplexen Signaturen unabdingbar.

Längere Ausdrücke können über mehrere Zeilen verteilt werden, solange alle Teilausdrücke tiefer eingerückt sind:

Erlaubt sind die meisten Unicode-Zeichen in Namen; so auch die deutschen Buchstaben (ä, ö, ü, ß); jedoch sollte man sich bei öffentlichen Projekten auf den ASCII-Zeichensatz als größten gemeinsamen Nenner beschränken.

Ein- und Ausgaben finden ausschließlich innerhalb einer mit main bezeichneten Funktion statt und weisen eine eigene Syntax auf, welche do-Notation genannt wird.

Beispiele
Programmierstile
Declaration style Expression style
lokale Bindungen in Klauseln mit where … als Ausdrücke mit let … in
Funktionen als Gleichungssysteme: als Lambda-Ausdrücke:
f x = … f = \x -> …
Muster f 1 = … f x = case x of 1 -> …
Wächter f x | x > 0 = … f x = if (x > 0) then … else …

 

Im deklarativen Stil wird ein Algorithmus aus mehreren zu erfüllenden Gleichungen beschrieben.

Funktionen

Funktionen in Haskell sind für gewöhnlich geschönfinkelt (→ en currying); benannt nach dem Logiker Moses Schönfinkel, im Englischen hingegen curried nach Haskell Brooks Curry. Im Nachfolgenden wird einfachheitshalber von geschönten Funktionen gesprochen:

Tatsächlich sind Funktionen in Haskell nur einstellig (f:xy). Der Eindruck der Mehrstelligkeit ist einzig und allein der Syntax geschuldet . So ließe sich das Eingangsbeispiel auch umschrieben als:

Die Argumente einer derart geschönten Funktion werden von links nach rechts umgesetzt, wobei als Zwischenschritt jeweils eine neue Funktion entsteht, bis alle Parameter bedient worden:

Solche partiellen Funktionsanwendungen erscheinen auf den ersten Blick nicht sonderlich nützlich, entfalten aber ihre wahre Stärke im Zusammenspiel mit anderen Funktionen. Durch die Teilanwendbarkeit lassen sich bestehende Funktionen flexibler verarbeiten und passgenau als Argument an andere Funktionen übergeben:

Die Funktion map gehört zur Standardbibliothek und bildet eine beliebige Funktion f(x) auf alle Werte einer Liste als zweiten Parameter ab. In imperativen Sprachen wären für gleichartige Aufgaben aufwendige Schleifenkonstrukte notwendig.

Innerhalb der Funktionssignatur fassen Klammern um mehrere Parameter diese zu einer Funktion xf(x) zusammen. Dabei bedeuten Kleinbuchstaben, dass verschiedene Datentypen erlaubt sind (generisch). Als zweiten Parameter erwartet map eine beliebige Liste [x] – in manchen Sprachen auch Feld (en array) genannt – vom Typ des Parameters der übergebenen Funktion (Datentyp -> Datentyp). Im Ergebnis liefert map eine neue Liste [f(x)] an Funktionswerten zurück. Da map selbst keine Abbildung vornimmt, sondern diese gleichermaßen als Parameter ausgelagert hat, beschränkt sich map auf die bloße Anwendung der übergebenen Funktion auf eine Liste von Argumenten. Derart höherere Funktionen, welche andere Funktionen zum Parameter haben oder als Ergebnis zurückgeben, bilden das Rückgrat funktionaler Programmierung.

Ungeschönte Funktionen

Parameter können auch als Tupel zusammengefasst sein, was den Anschein von herkömmlichen Funktionen aus imperativen Sprachen weckt:

Solche ungeschönten Funktionen haben den Nachteil, dass diese nicht partiell anwendbar sind. Jedoch bietet Haskell mit den Funktionen curry sowie uncurry die Möglichkeit, zumindest Dupeln zu zerlegen oder zwei Parameter zu einer Dupel zusammenzufassen:

Für das Umwandeln von Tripeln bestehen curry3 und uncurry3 als Bibliotheksfunktionen.

Gängiger hingegen ist die Praxis, mehrere zusammengehörige Angaben, welche für sich allein genommen auch nicht sinnvoll erscheinen, in einem Typen zu vereinen; beispielsweise Farben nach dem RGB-Modell, oder die Beschreibung eines geometrischen Objekts:

Lambda-Ausdrücke

Neben der deklarativen Funktionsbeschreibung f x = … bietet Haskell auch eine Syntax für sog namenlose Funktionen, fachlich λ-Ausdrücke genannt:

Lambda-Ausdrücke treten zumeist als "Einwegfunktionen" in Erscheinung, wenn diese nur einmal als Argument benötigt werden:

Genauso wie bei benannten Funktionen, lassen sich die Parameter literaler Funktionen als Tupel zusammenfassen:

Lambda-Ausdrücke stellen gegenwärtig die naheliegendste Lösung für partielle Anwendungen dar, wenn nicht die ersten Parameter, sondern nur ganz bestimmte versorgt werden sollen:

Freie und gebundene Variablen

Globale Namensbindungen werden auch als freie Variablen im mathematischen Sinne bezeichnet. Hingegen gelten Variablen als gebunden, wenn diese innerhalb eines betrachteten Ausdrucks definiert sind. So liegt in der Funktion \ x -> a + x das x als lokal gebundene Variable vor, und ist nur innerhalb des Lambda-Ausdrucks sichtbar; a hingegen referenziert einen äußeren Wert und wird demnach im Bezug auf diesen Ausdruck als frei angesehen.

Der Umstand, ob eine Variable gebunden oder frei ist, bestimmt maßgeblich die Auswertung eines Ausdrucks: Beispielsweise kann eine gebundene Variablen sicher umbenannt werden, ohne den resultierenden Wert zu ändern: \ x -> a + x, ist das gleiche wie \ y -> a + y; jedoch bezeichnet \ x -> b + x etwas völlig anderes, da b nicht zwangsläufig gleich a sein muss.

Funktionsabschluss

Parameter im deklarativen Stil werden als freie Variablen angesehen, da diese außerhalb des Lambda-Ausdrucks gebunden vorliegen:

Mit einem Funktionsabschluss (en closure) ist ein Lambda-Ausdruck gemeint, welcher freie Variablen enthält; sozusagen umschließt der resultierende Ausdruck einen Teil seiner Umgebung. Nach dieser Definition stellen alle deklarativ definierten Funktionen in Haskell zugleich Funktionsabschlüsse dar.

Operatoren

Jede zweistellige Funktion in Haskell kann als Infix-Operator verwendet werden, wenn der Name zwischen zwei schrägen Hochstrichen `` steht:

Umgekehrt sind die bestehenden Operatoren wie Funktionsnamen verwendbar, wenn diese in Rundklammern () erfolgen:

Auch lassen sich Operatoren partiell anwenden, wobei je nach fehlendem Operanden entweder ein Linksstück (en left section) oder Rechtsstück (en right section) vorliegt:

Weitere nützliche Stückelungen (en sections) sind u A:

Funktionskomposition

Entspricht das Ergebnis einer Funktion f dem Typ des einzigen Parameters einer Funktion g, so können beide Funktionen miteinander verknüpft werden: y=g(f(x))=(fg)(x)

Der Operator . verkettet zwei Funktionen zu einer Gesamtabbildung, indem der Rückgabewert der rechten Funktion den Parameter der linken Funktion bedient:

Zu unterscheiden von der Komposition ist die bloße Funktionsanwendung:

Der Zweck des Operators $ besteht lediglich darin, durch eine extrem niedrige Operatorpriorität Rundklammern um Argumente einzusparen.

Punktfreier Stil

Unter impliziter bzw punktfreier Programmierung (en tacit programming, oder auch point-free style) ist das Definieren von Funktionen gemeint, ohne die Parameter explizit zu benennen. Stattdessen setzen sich Definitionen aus bestehenden Funktionen zusammen, darunter auch Kombinatoren, welche die Argumente manipulieren:

Der Begriff punktfreier Stil rührt aus der Topologie her, einem Zweig der Mathematik, welcher mit aus Punkten bestehenden Räumen arbeitet und den Funktionen zwischen diesen Räumen. Eine punktefreie Funktionsdefinition ist also eine Definition, bei der die Punkte (Werte) des Raums, auf welchen die Funktion einwirkt, nicht ausdrücklich erwähnt werden. In Haskell bezeichnet Raum einen Datentyp, und Punkte sind die zugehörigen Werte. In der Deklarierung f x wird also eine Funktion f als Wirkung auf einen beliebigen Punkt x definiert.

Beispiel für Kombinatoren in Haskell

Kombinatoren sind das Gegenteil von Funktionsabschlüssen.

Rekursion

Selbstaufrufe von Funktionen stellen eine Spezialform der Funktionskomposition dar:

Über einen Fixpunkt-Kombinator lassen sich auch namenlose Funktionen rekursiv definieren:

Statt eines Selbstaufrufs erwartet der Lambda-Ausdruck eine andere Funktion als erstes Argument. Der Fixpunkt-Kombinator nimmt den Lambda-Ausdruck entgegen und ruft sich selbst solange auf, bis der Fixpunkt der literalen Funktion gefunden wurde.

Hinweis

In der interaktiven Umgebung rufen sich nicht-terminierende Funktionen solange selbst auf, bis der Arbeitsspeicher vollständig belegt ist. Aus diesem Grund sollte sofort über den Taskmanager der Prozess von GHCi getötet werden!

Typsystem

Typen bieten ein mächtiges Mittel, um Daten möglichst fehlerfrei sowie effizient zu verarbeiten, indem eine Menge von zulässigen Werten spezifiziert wird. Dies gestattet dem Programmierer, genau anzugeben, auf welche Menge von Datenobjekten eine Funktion anwendbar ist. Derartige Typangaben werden als statisch bezeichnet, während Sprachen, in denen der genaue Typ eines Objekts erst zur Laufzeit bekannt ist, als dynamisch typisiert gelten. Dynamische Typisierung birgt den Vorteil, dass Programme wesentlich uneingeschränkter (generischer) und folglich auch schneller entwickelt werden können. Jedoch sind als Preis für diese Flexibilität gewisse Performanzeinbußen durch zur Laufzeit stattfindende Typprüfungen hinzunehmen, welche auch nicht in jedem Fall den fehlerfreien Betrieb des Programms gewährleisten, beispielsweise wenn eine Zeichenkette in eine Zahl automatisch umgewandelt wird.

Haskell gilt als eine äußerst stark sowie ausschließlich statisch typisierte Sprache, und unterscheidet streng zwischen verschiedenen Datenarten, worunter sogar Ein- und Ausgaben fallen. Demnach muss die Signatur eines jeden Objekts – ob Einzelwert, Datenstruktur oder Funktion – dem Übersetzer bereits bekannt sein, was zwar den Umgang mit Datenobjekten einschränkt, jedoch den Programmierer mit einer deutlich höheren Ausführgeschwindigkeit des Kompilats belohnt, bedingt durch den Wegfall jeglicher Typprüfungen zur Programmlaufzeit. Haskell perfektioniert das Ganze sogar noch, indem der Übersetzer mittels eines internen Regelwerks eigenmächtig den genauen Typ von literal vorliegenden sowie erzeugten Datenobjekten schlussfolgert, sodass der Programmierer nur in seltensten Fällen die konkrete Datenart mittels :: vorgeben muss:

Hinweis: Mit :t kann in der interaktiven Umgebung die genaue Signatur des nachfolgenden Ausdrucks erfragt werden.

Neben dem Wegfall von Typangaben zu Einzelwerten und Funktionen gestattet diese Fähigkeit, fachlich Typableitung oder Typinferenz genannt, dass sämtliche Typverletzungen bereits zur Übersetzungszeit erkannt werden. Neben der Bedarfsauswertung ist die Typableitung eines der markantesten Merkmale von Haskell schlechthin.

Grundtypen

DatenartTypkonstruktorBspWertebereich
WahrheitstypBoolTrue{ False, True }
Ganzzahl – 32 / 64 BitInt2[-231; 231 – 1] / …
Ganzzahl – beliebigInteger-137842ℕ (unendlich)
Gleitkommazahl – 32 BitFloat1.66
Gleitkommazahl – 64 BitDouble3,14159
BruchzahlRational1 / 9
EinzelzeichenChar'þ'Unicode
ZeichenketteString = [Char]"Tach!"Unicode

Algebraische Datentypen

Ein algebraischer Datentyp – abgekürzt ADT – ist eine zusammengesetzte Datenart, beruhend auf zwei gegensätzlichen Konstruktionsprinzipien:

Ein Produkttyp stellt mathematisch gesehen nichts anderes als das kartesische Produkt zweier oder mehr Mengen dar. Folglich beschreibt ein Produkttyp eine Menge von möglichen Tupeln:

A×B:={(a,b)aAbB}

Jede vollwertige Programmiersprache weist zumindest ein gleichartiges Konzept auf:

Ein Summentyp hingegen lässt sich als Vereinigung – bzw Summe – von bestehenden Mengen auffassen:

A+B:={xxAxB}=AB

Nur funktionale Sprachen sowie moderne Neuentwicklungen wie Swift oder Rust weisen echte Summentypen vor:

Varianten in C sind eher als primitive (maschinennahe) Vorläufer ohne jegliche Typsicherheit zu sehen.

Vereinfacht gesagt sind mit Produkttypen zusammengesetzte Typen – Verbünde / Kompositionen – gemeint, während Summentypen als eine Auswahl von möglichen Typen begreifbar sind.

In Haskell werden sowohl Produkt- als auch Summentypen mit dem Schlüsselwort data festgelegt:

Anmerkung: Mit deriving wird automatisch eine Instanz der Typklasse Show erzeugt, sodass Objekte der betreffenden Datenart unmittelbar ausgegebenen werden können. Genaue Erläuterungen hierzu erfolgen unter dem Abschnitt Typklassen.

Mit Ausnahme von Grundtypen – deren Wertemengen aus Literalen wie Zahlen bestehen –, besitzt jeder Datentyp einen oder mehrere Datenkonstruktoren. In ähnlicher Weise wird der Name eines algebraischen Datentyps auch als Typkonstruktor bezeichnet und ist zu unterscheiden von den Datenkonstruktoren – auch Wertkonstruktoren genannt. Sowohl Typ- als auch Datenkonstruktoren müssen mit einem Großbuchstaben beginnen, wobei Typkonstruktoren nur in Signaturen vorkommen, während Datenkonstruktoren ausschließlich auf Termebene verwendet werden, sodass diese jeweils einen eigenen Namensraum bilden (Bsp: Zeile 4).

Anhand der Signaturen [18, 19, 20] ist erkennbar, dass Datenkonstruktoren echte Funktionen darstellen, welche aber im Gegensatz zu gewöhnlichen Funktionen nicht vom Programmierer zu definieren sind, sondern über die Typdeklarierung – mittels data – automatisch abgeleitet werden.

Typparameter

Typkonstruktoren sind Funktionen auf Typebene, welche einen konkreten Typen erzeugen:

Genauso wie Funktionen der Termebene können Datentypen parametrisch sein (Typ parameter), und durch Übergabe eines Datentypen als Argument einen neuen konkreten Datentypen Type Arg konstruieren. Auch nimmt Haskell an dieser Stelle dem Programmierer die Entscheidung ab und erwartet, dass Typparameter im Gegensatz zu Bezeichnern von Datentypen stets mit einem Kleinbuchstaben beginnen.

Typparemeter gestatten, Wertkonstruktoren für noch unbekannte Datentypen zugänglich zu halten:

In diesem Beispiel könnte anstelle einer viergliedrigen Nummer in Form eines Produkttypen Int × Int × Int × Int genauso gut eine alphanumerische Variante genutzt werden:

Mit Typparametern erlaubt Haskell das Zusammenfassen von ähnlichen Datenstrukturen zu einer allgemeinen Beschreibung, welche für den jeweiligen Anwendungsfall anpassbar ist.

Ext: Lokale Typvariablen

Beim Deklarieren von Funktionen werden etwaige Typvariablen vom Compiler automatisch gebunden:

map :: (a -> b) -> [a] -> [b]

ist gleichwertig zur

map :: forall a b. (a -> b) -> [a] -> [b]

Die Signatur kann ähnlich wie in der Logik als für alle a und b gilt … gelesen werden, formalisiert durch den Allquantor ∀ (en for all).

Die Erweiterung ScopedTypeVariables geht einen Schritt weiter und führt die semantische Bedeutung ein, dass ausdrücklich gebundene Typvariablen auch für lokale Deklarierungen gelten. Das ist vor allem dann sinnvoll, wenn Typvariablen lokaler Namensbindungen den gleichen Beschränkungen unterliegen müssen:

Die erste Version wird vom Übersetzer abgelehnt, da der Parameter von g nicht gleichermaßen auf die Typklasse Read beschränkt wurde.

Symbole

Nullstellige Datenkonstruktoren beinhalten nur ihren Namen als symbolische Information:

  1. Typkonstruktor der Datenart Bool.
  2. Datenkonstruktor für den Wert Falsch.
  3. | ist als ausschließendes oder (XOR) zu werten: entweder … oder …
  4. Datenkonstruktor für den Wert Wahr.

Ext: Verallgemeinerte ADT (GADT)

Verallgemeinerte algebraische Datentypen (en Generalised Algebraic Data Types – GADT) erweitern gewöhnliche algebraische Datentypen, indem die Signatur der Konstruktoren genau aufgeschlüsselt wird:

In diesem Beispiel wird eine domänenspezifische Sprache definiert, in welcher der Typparameter die Auswahl möglicher Wertkonstruktoren beschränkt, was den Vorteil bietet, dass Musterabgleiche nicht alle Varianten berücksichtigen müssen.

Ext: Existenzielle Typen

Beim Definieren eines Typen muss jede Typvariable ausdrücklich als Typparameter auf der rechten Seite vom Gleichheitszeichen eingeführt werden:

Für t ist jeder beliebige Typ einsetzbar:

Da aber jede dieser Konstruktionen zu Objekten unterschieden Typs führt, lassen sich diese bspw nicht in einer Liste zusammen:

Mit der Erweiterung von Existenzialaussagen sind Typen definierbar, dessen Typvariaben nicht zur Übersetzungszeit aufgelöst werden, sondern ihre Generizität beibehalten:

Existentielle Typen sind nichts anderes als Haskells Ansatz, etwas dynamische Typisierung in der sonst ausschließlich statisch typisierten Sprache zu simulieren.

Typumschreibungen

Obgleich Haskells algebraische Datentypen äußerst mächtig sind, wie noch im Nachfolgenden zu sehen, wird bereits an dieser Stelle ein Nachteil der bestehenden Syntax deutlich: Da Datenkonstruktoren immer mitzuschleppen sind, neigen verschachtelte Typkonstrukte nicht nur zur Unübersichtlichkeit, sondern werden mit zunehmender Tiefe auch äußerst unhandlich, sodass Typsynonyme gegenüber inneren Produkttypen eine Alternative darstellen, zumal echte Feldnamen in Haskell nicht bestehen.

Ferner wird anstelle eines Produkttypen, welcher ein oder mehr Summentypen enthält, der umgekehrte Weg gegangen; bezogen auf das Einführungsbeispiel:

Alternative Typnamen sollten auch nur dann eingesetzt werden, wenn anhand des Konstruktornamens die Bedeutung der nachfolgenden Argumente nicht ersichtlich ist. Im Idealfall sind Typsynonyme gar nicht erst erforderlich, indem der Produkttyp tatsächlich nur die wesentlichen Daten umfasst, sodass ein aussagekräftiger Konstruktorname bereits genügt:

Neben Synonymen für bestehende Datenarten gestattet Haskell auch das Festlegen neuer Datentypen, welche über einen eigenen Wertkonstruktor verfügen und ihre Kompatibilität zum ursprünglichen Datentyp aufgeben:

Neue Typen weisen im Gegensatz zu algebraischen Datentype mit data stets nur einen Konstruktor vor.

Derartige Umdefinierungen mittels newtype wirken generischer Programmierung entgegen, und sollten nur in Ausnahmefällen verwendet werden, bspw um die Typsicherheit durch eine gewisse Spezialisierung zu erhöhen (siehe smart constructors).

Unverpackte Typen

Fast alle Werte in Haskell sind als Verweis auf ein Objekt im Freispeicher umgesetzt:

Tatsächlich bezeichnet t während der Laufzeit nur einen Zeiger, welcher bei Bedarf automatisch dereferenziert wird. Das verwiesene Objekt umfasst neben etwaigen primitiven Daten auch eine Tabelle mit allen wichtigen Informationen, bspw für die automatische Speicherverwaltung oder um die Polymorphie zu regeln. Datentypen derartiger Objekte gelten als boxed (verpackt). Hingegen repräsentieren unverpackte Typen rohe Werte, welche zumeist im Stapelspeicher liegen und direkt von der Maschine verarbeitbar sind. Aus Gründen der Effizienz ersetzt GHC beim Optimieren möglichst viele Werte durch ihr unverpacktes Gegenstück, sodass durchaus mit einer durchschnittlichen Geschwindigkeit zwischen Java und C liegend gerechnet werden kann.

GHC kennzeichnet unverpackte Datentypen und deren Werte mit einer angefügten Raute #:

Unverpackte Werte unterliegt gewissen Einschränkungen. Insbesondere können diese nicht an polymorphe Funktionen – wie show oder ($) – übergeben werden, da keine Infotabelle besteht. Folglich wird im obigen Beispiel die unverpackte Ganzzahl mittels des Konstruktors I# in die verpackte Form überführt.

Bodentyp

Ein bottom type ist ein spezieller Typ, welcher keinen Wert beinhaltet (en zero / empty) und aus diesem Grund gelegentlich mit als logischer Widerspruch (Kontradiktion / Absurdum) dargestellt wird. In Haskell bezieht sich der Bodentyp auf Auswertungen, welche niemals erfolgreich sein können, beispielsweise eine Fehlermeldung error oder ein undefinierter Wert undefined.

Fast alle verpackten Typen, welche bottom – also die polymorphen Werte error und undefined – umfassen, werden auch als lifted bezeichnet. Der Begriff Bodentyp rührt daher, dass alle anderen Werte eine bestimmte Bedeutung vorweisen, wohingegen bottom auf unterster Ebene liegt.

Normalerweise erwartet Haskell, dass sowohl der bedingte Wert then als auch seine Alternative else vom gleichen Typ sind; da jedoch die Fehlerfunktion error :: [Char] -> a polymorph ist, wird dessen Ergebnis a als Double gewertet.

GHC kennt nur wenige verpackte Werte, welche zugleich unlifted (ungehoben) sind, wie auf verpackte Objekte verweisende Zeiger:

Programmierparadigmen

 

Um Abstraktionen wie Polymorphie und Bedarfsauswertung zu ermöglichen, sind die allermeisten Datentypen in Strukturen mit zusätzlichen Daten eingebettet (en boxed).

Typarten

Jeder Typkonstruktor in Haskell weist eine bestimmte Typart (en kind) vor, welche seine Arität genau spezifiziert:

Das Sternchen * ist als Typ zu lesen und steht für einen nullstellige Typkonstruktor, worunter alle Grundtypen (Basistypen) fallen.

Genau genommen steht * für einen verpackten Typen (en boxed type); unverpackte Datenarten hingegen sind von der Art #. Durch diese Unterscheidung weiß Haskell bereits statisch zur Übersetzungszeit, wie mit den betreffenden Werten umzugehen ist und kann entsprechende Optimierungen vornehmen.

Spracherweiterungen wie PolyKinds und DataKinds bauen das kind-System weiter aus, um dem Programmierer eine höhere Generizität zu bieten.

Ext: Polymorphe Typarten

Mit der Freischaltung PolyKinds sind auch Typparameter variabler Stelligkeit definierbar:

Ausdrücke und Bindungen

Vereinfacht gesagt stellt ein Ausdruck nichts anderes als ein Datenobjekt dar oder eine Verknüpfung von Datenobjekten, was Zahlen, Symbole, Text oder auch komplexere Strukturen sein können. Dabei gibt der jeweilige Datentyp eines Objekts die erlaubten Operationen vor. Der Begriff Objekt ist allgemein zu verstehen und nicht im Sinne objektorientierter Programmierung.

Abgesehen von Deklarationen sowie Variablenbindungen ist ein Programm in Haskell ausschließlich durch eine Reihe von Ausdrücken gekennzeichnet, welche bei Bedarf nacheinander ausgewertet werden. Beispielsweise erlauben imperative Sprachen keine Bedingungen oder andere Kontrollstrukturen innerhalb von Zuweisungen:

Dies ist in Haskell statthaft, da die Kontrollstruktur if … then … else einen Ausdruck darstellt, und genauso wie eine Funktion über einen Rückgabewert verfügt. Hingegen wäre vergleichbares in C / Cpp nur mittels des ternären Operators möglich:

Tatsächlich lassen sich in streng funktionalen Sprachen die allermeisten Konstrukte als Funktion auffassen und miteinander zu Ausdrücken verknüpfen, währenddem imperative Sprache vor allem auf Anweisungen beruhen, welche über keinen Rückgabewert verfügen und folglich nicht kombinierbar sind, sondern einfach im Raum herumstehen und nacheinander ausgeführt werden.

Variablenbindungen

Jeder Ausdruck in Haskell führt wie ein mathematischer Term zu einem neuen Wert:

Charaktertisch für funktionale Sprachen ist, dass Variablen nicht nachträglich verändert werden können, sondern lediglich als Platzhalter zu verstehen sind. Demnach wird nicht von Initialisierungen sowie Zuweisungen gesprochen, sondern von Variablenbindungen. In diesem Sinne bestehen auch keine Schlüsselwörter wie var und const, stattdessen greifen funktionale Sprachen auf Begriffe wie let zurück.

Um innerhalb von Ausdrücken andere Ausdrücke an Namen zu binden, bietet Haskell die Sprachmittel let bzw let … in sowie where.

Bezeichner

Die Groß- und Kleinschreibung ist streng geregelt: Konstruktoren müssen mit einem Großbuchstaben beginnen, gebundene Ausdrücke hingegen mit einem Kleinbuchstaben. Danach dürfen beliebig Buchstaben, Zahlen sowie Unterstriche _ folgen. Sogar Hochkommas ' sind erlaubt, um wie in der Mathematik abgewandelte Variablen zu bezeichnen:

Zu beachten ist, dass Haskell den Unterstrich _ als Kleinbuchstabe behandelt, sodass die folgenden Namen statthaft sind:

Derartige Bezeichner sind jedoch nicht zu empfehlen!

Als gängige Konvention gilt, die Bestandteile zusammengesetzter Namen durch Binnengroßschreibung (en camel case) hervorzuheben, bspw: useDateFromInputStream. Jedoch sollten Bezeichner weder zu lang noch zu kurz bzw kryptisch gewählt sein, und schon gar nicht einen ganzen Satz nachbilden, sondern sich hinreichend aus dem Kontext heraus selbst erklären.

let als Ausdruck

Das Schlüsselwort let erlaubt auch mehrere Bindungen gleichzeitig:

Man beachte, dass alle Ausdrücke unter dem Namen bzw let eingerückt sind.

let als Anweisung

Variablenbindungen innerhalb eines do-Blocks werden wie Anweisungen gehandhabt, sodass let – ohne Rückbezug auf einen Ausdruck durch in – zu verwenden ist:

Der zweite Anwendungsfall von let-Anweisungen sind Listenbeschreibungen:

where-Klausel

Im Gegensatz zu let, welches entweder als Ausdruck oder Anweisung für sich alleine steht, sind Bindungen über where auf ein bestehendes Konstrukt beschränkt:

Mit where wird eine Umgebung geboten, um Variablen zwischen verschiedenen Abschnitten einer Funktion zu teilen, welche jeweils eigenständige Ausdrücke bilden:

Anmerkung zum Beispiel: Die Collatz‐Funktion macht nichts anderes als jede natürliche Zahl solange zu verarbeiten, bis am Ende eine 1 herauskommt. Mit dem Datentyp Integer erlaubt Haskell beliebig lange Zahlen.

Die senkrechten Striche | nach dem Parameter werden Wächter genannt und kündigen eine Fallunterscheidung an, lassen sich aber genauso gut durch andere Konstrukte wie if oder case ausdrücken. Eine genauere Betrachtung erfolgt im Nachfolgenden zu den Kontrollstrukturen.

Ext: Implizite Parameter

en implicit parameters

Implizite – bzw indirekte – Parameter erlauben das spätere Definieren von lokalen Bindungen innerhalb eines Ausdrucks, womit sich in Haskell dynamische Gültigkeitsbereiche (en dynamic scoping) in statisch typisierter Manier nachstellen lassen:

Das Fragezeichen ? fungiert als Präfix, um implizite Parameter von gewöhnlichen Namensbindungen zu unterscheiden.

Kontrollstrukturen

In Haskell bestehen nur Verzweigungen, während Schleifen durch Rekursion zu lösen sind. Zudem liegen Kontrollstrukturen als Ausdrücke vor:

Neben dem althergebrachten wenn … dann … sonst …-Konstrukt bietet Haskell auch mehrere Arten der Fallunterscheidung.

Bedingter Ausdruck

Bedingte Ausdrücke erfordern stets die Angabe eines Alternativwertes.

Fallunterscheidungen

Fallunterscheidungen werden auch Musterabgleiche (en pattern matching) genannt und vergleichen – im Gegensatz zu Bedingungen mit if – einen gegebenen Ausdruck mit einer Reihe von Literalen ab:

Der erste zutreffende Fall wird evaluiert. Darüber hinaus müssen Fallunterscheidungen vollständig sein, sodass ggf mit einem Unterstrich _ – sozusagen als allgemeines Muster – alle übrigen Möglichkeiten abzudecken sind.

Gängiger ist jedoch die deklarative Syntax, Funktionen als Einzelgleichungen zu definieren:

Zu beachten ist, dass der Übersetzer den ersten zutreffenden Fall, beginnend vom Dateianfang, auswählt.

Wächter

Parameter lassen sich auch unmittelbar prüfen:

Als Wächter wird der boolesche Ausdruck zwischen dem senkrechten Strich | und Gleichheitszeichen = bezeichnet.

Statt eines Alternativwertes else erfordern Wächtern ähnlich wie Musterabgleiche das vollständige Erfassen aller erdenklichen Fälle, jedoch mit dem Schlüsselwort otherwise.

Die Notation von Wächtern ist unmittelbar aus der Mathematik übernommen:

entspricht:

f(x)={x2xxif x10otherwise
Ext: Musterabgleich

Mit Haskell 2010 wurde unter der Bezeichnung pattern guards die Syntax für Wächter erweitert, um auch das Abgleichen von Mustern zu ermöglichen:

Muster und Bedingungen sind beliebig miteinander kombinierbar, solange diese durch ein Komma voneinander getrennt sind.

Ext: Bedingter Ausdruck

Die Spracherweiterung MultiWayIf schaltet das Verwenden von Wächtern auch für bedingte Ausdrücke frei:

Entstrukturierungen

Summentypen erfordern, dass alle Varianten abgedeckt werden:

Das einzelne Ansprechen von Komponenten innerhalb eines Musters KONSTRUKTOR x y … wird auch Dekonstruktion oder Entstrukturierungen genannt.

Rekord-Syntax

Die Record-Syntax erlaubt das automatische Erstellen von Funktionen für den Zugriff auf die einzelnen Daten (Argumente) eines Wertkonstruktors.

Da Haskell keine Verbundstrukturen (wie struct in C) mit eigenem Namensraum vorweist, besteht neben dem Verwenden von sprechenden Typsynonymen wie Name oder IstGast nur noch eine zweite Lösung, über sog Zugriffsfunktionen gezielt einzelne Werte abzugreifen:

Glücklicherweise bietet Haskell hierfür Syntaxzucker, woraus die erforderlichen Zugriffsfunktionen automatisch generiert werden:

Die Record-Schreibweise darf aber nicht darüber hinwegtäuschen, dass die Feldnamen Teil des globalen Namensraums sind!

Zwischen den geschweiften Klammern und Beistrichen kann beliebig viel eingerückt und formatiert werden. Jedoch hat sich bei Haskell-Programmierern eingebürgert, das Komma am Anfang einer Zeile zu setzen.

Lösungen zum Namensraumproblem

Module

Durch Auslagern von Datentypen in jeweils eigene Module lassen sich Namenskollisionen vermeiden.

Präfixe

Die Feldnamen werden durch eindeutige Anfügungen kenntlich gemacht:

Überladung

Die GHC-Erweiterung DuplicateRecordFields erlaubt das mehrfache Verwenden eines Feldnamens für unterschiedliche Datentypen:

Die Auswahl trifft der Übersetzer anhand des Typen, was im Grunde nichts anderes als Ad-Hoc-Polymorphie ist und sich folglich auch – umständlicher – mit Typklassen realisieren ließe.

Punktnottation

Anfang April 2020 wurde eine neue Erweiterung RecordDotSyntax beschlossen, welche gestattet, auf die Felder in gewohnter Punktnottation zuzugreifen, womit Haskell endlich echte Records bietet.

Bedarfsauswertung

Als Normalform wird ein nicht weiter evaluierbarer Ausdruck bezeichnet. Dies ist dann der Fall, wenn nach Auflösen aller Operationen nur noch ein Literal übrig bleibt:

Zeile 3 stellt einen verknüpften Ausdruck dar, bestehenden aus zwei Variablen sowie einem Literal. Der Vorgang des Auswertens wird auch Reduzierung oder Normalisierung genannt, und erfolgt in Haskell bei Bedarf. So werden nur jene Argumente ausgewertet, welche für ein Funktionsergebnis tatsächlich nötig sind:

Das Gegenteil der Bedarfsauswertung (en lazy evaluation, auch call-by-need) ist die strikte Auswertung (en eager evaluation, auch strict evaluation) klassischer prozeduraler Programmiersprachen.

Die Bedarfsauswertung ist eine der größten Stärken von Haskell, denn diese erlaubt beispielsweise das Beschreiben von unendlichen Datenstrukturen (→ unendliche Listen) oder Definieren eigener Steuerkonstrukte:

Anmerkung: Dank der Bedarfsauswertung wird der alternative Ausdruck (das letzte Argument) nur normalisiert, wenn die Funktion unless dessen Ergebnis zurückgibt.

Auch führt der Wegfall unnötiger Auswertungen zu einem Geschwindigkeitsgewinn, gestaltet jedoch das händische Optimieren ungleich schwieriger, da für den Entwickler der tatsächliche Ressourcenverbrauch nicht mehr offensichtlich ist. Ebenso steigt der Umfang an Verwaltungsdaten (en overhead), was soweit führen kann, dass der Performanzugewinn durch Bedarfsauswertung wieder zunichte gemacht wird, sodass nicht selten die Nachteile überwiegen. Aus diesem Grund resümierte sogar Simon Peyton Jones nach Jahren der Arbeit an der Sprache, dass das neue Haskell strikt sein wird (original: The next Haskell will be strict) und faul nur auf ausdrücklichem Wunsch des Programmierers. Jedoch bleibt die Frage noch ungeklärt, wie strikte und bedarfsweise Auswertung in einer Sprache bestmöglichen zu vereinen sind. Haskell bieten zumindest einige Strategien, eine strikte Evaluierung zu erzwingen.

Strikte Auswertung

 

 

Undefinierte Werte

Falls eine Funktion zu einem späteren Zeitpunkt vervollständigt oder implementiert werden soll, bietet Haskell hierfür den generischen Wert undefined:

Das Programm kompiliert dennoch, wirft jedoch bei Ausgabe von x einen Laufzeitfehler.

Datenstrukturen

Haskell kennt zwei grundlegende Datenstrukturen, um mehrere Werte zu verwalten:

 TupelListe
 (EXPR, EXPR, …)[EXPR, EXPR, …]
StrukturElemente beliebigen Typsgleichtypige Elemente
Typ(A, B, …)[A]
Erweiterbarkeitfeste Größeergänzbar um neue Elemente

Tupeln

 

 

Listen

 

 

 

Unendliche Listen

Die Bedarfsauswertung ermöglicht das Beschreiben von nicht terminierenden Datenstrukturen, welche erst bei Anwendung berechnet werden:

Der Operator (!!) nimmt eine beliebige Liste entgegen und gibt das n-te Element zurück. Die Liste selbst wird als Teilausdruck nur bis zur n-ten Stelle ermittelt.

Bei einer unmittelbaren Ausgabe der Liste besteht hingegen kein konkreter Bedarf, sodass erst durch Abbruch mit Strg + c die Auswertung der Liste gestoppt wird.

Mengen

Haskell unterstützt typisierte geordnete Mengen mit schnellen Einfüge-, Lösch- und Suchoperationen. Aus diesem Grund wird auch empfohlen, die Set-Implementierung aus dem Base-Paket zu verwenden, anstatt Listen zweckzuentfremden:

Das Module Data.Set enthält zahlreiche Funktionen zum Verarbeiten von Mengen.

Typklassen

Typenklassen und Typen verhalten sich gewissermaßen gegensätzlich: Typdeklarationen geben an, wie ein Typ zu erstellen ist (Konstruktion). Eine Typklasse hingegen beschreibt eine Menge von Konstanten und Funktionen, welche für einen bestimmten Datentyp implementierbar sind. In diesem Sinne entsprechen Typklassen einer allgemeinen Schnittstellenbeschreibung (en interface oder trait in OOP-Sprachen) und fassen mehrere Datentypen zu einer Klasse mit gemeinsamen Eigenschaften zusammen.

Ursprünglich wurden Typklassen entwickelt, um Operatoren in Haskell systematisch zu überladen:

In diesem Beispiel wird für die Typklasse Num eine neue und unvollständige Instanz erstellt, um mit dem Plus-Operator zwei Objekte vom Typ Quadrupel miteinander zu addieren.

Die Typklasse Num enthält neben dem + noch weitere Operatoren, sowie wenige Funktionen:

Mit :info kann interaktiv die Deklaration einer Typklasse erfragt werden sowie die jeweiligen dem Übersetzer (GHC) bekannten Instanzen.

Die Typklasse Num umfasst die mathematischen Operatoren +, - und * sowie grundlegenden Funktionen, welche gleichermaßen vollständig auf Ganzzahlen anwendbar sind. Für das Arbeiten mit Gleitkommazahlen wird die Typklasse Num um eine weitere Klasse erweitert, welche Operationen im Umgang mit gebrochen Zahlen beschreibt:

Der Ausdruck Num a => Fractional a besagt, dass jede Bruchzahl wie 3.4 oder (2 / 3)::Rational zugleich von der Typklasse Num ist, womit Haskell sämtliche grundlegende mathematische Operationen für jeglichen Zahlentypen verallgemeinert hat. Alle komplexeren Funktionen, welche die in den Typklassen Num sowie Fractional beschriebenen Operationen verwenden, sind gleichermaßen generisch anwendbar!

Ableiten von Instanzen

Haskell bietet einen internen Mechanismus, um vollständige Instanzen für die Typklassen Eq, Ord, Enum, Bounded, Show sowie Read automatisch zu erstellen:

Ext: Multiparametrische Typklassen

Die Spracherweiterung MultiParamTypeClasses gestattet mehrere Typparameter:

Funktionale Abhängigkeiten

Multiparametrische Typklassen weisen den Nachteil auf, dass die Eindeutigkeit verloren geht und manuelle Typfixierungen vorgenommen werden müssen, wenn kein Kontext vorliegt:

 

Mit funktionalen Abhängigkeiten – oder kurz Fundeps – lassen sich Beziehungen zwischen den Typklassenparametern ausdrücken:

Als Preis für diese zurückgewonnene Eindeutigkeit muss in Kauf genommen werden, dass nur noch bestimmte Varianten implementierbar sind.

Eine alternative zu funktionalen Abhängigkeiten stellen Typfamilien, genauer assoziierte Datentypen, dar.

Ext: Nullstellige Typklassen

Mittels Erweiterung erlaubt Haskell auch parameterlose Typklassen:

Einen wirklichen Vorteil bieten nullstellige Typklassen nicht und wurden nur eingeführt, um unnötige Beschränkungen der Sprache abzubauen. Tatsächlich ist die Erweiterung implementiert, indem lediglich eine Zeile Code in GHC gestrichen wurde.

Variadische Funktionen

Über trickreiches Verwenden von Typklassen lassen sich typsichere Funktionen mit beliebig vielen Argumenten nachbilden:

Der Trick besteht darin, dass die variadische Funktion entweder einen konkreten Wert zurückliefert (erste Instanz) oder aber eine neue Funktion (zweite Instanz), welche den zuvorigen Wert mit dem eigenen Parameter verarbeitet. Demzufolge sind zur Umsetzung immer zwei generische Instanzen erforderlich, wobei die eine Instanz die betreffende variadische Methode als Rekursion definiert, während die andere Instanz den zugehörigen Rekursionsanker darstellt, indem die Methode das Endergebnis zurückgibt statt einer weiteren selbstaufrufenden Funktion.

Monaden

Eine Monade ist ein Monoid in der Kategorie der Endofunktoren. ;D

Oder auf gut Deutsch: Monaden stellen nichts anderes als ein Behältnis dar, um beliebige Ausdrücke miteinander zu einer Abarbeitungsfolge zu verknüpfen, wobei der Programmierer in jedem einzelnen Schritt die Verarbeitung genau spezifizieren kann. Bezugnehmend auf imperative Sprachen und deren Syntax lässt sich eine Monade auch bildhaft als ein programmierbares Semikolon auffassen und ermöglicht das hintereinander Ausführen als eine Sequenz.

Um eine Monade zu formulieren, sind drei Funktionen erforderlich:

  1. Typkonstruktor m t

    • definiert den für einen Typen t korrespondierenden Monadentyp m t
  2. Einheitsfunktion t -> m t

    • bildet einen Wert vom Typ t auf den korrespondierenden monadischen Wert ab
    • in Haskell return / pure
  3. Bindungsfunktion (bind-Operator) m a -> (a -> m b) -> m b

    • Erlaubt die Übergabe eines monadischen Wertes an eine Funktion mit korrespondierendem nichtmonadischen Parameter.

Im Gegensatz zu imperativen Sprachen kennt Haskell keine Anweisungen oder Funktionen ohne Rückgabewert (Prozeduren). Folglich müssen auch Ein- / Ausgaben sowie ganze Sequenzen irgendwie als Ausdrücke händelbar und miteinander verknüpfbar sein. Genau für diesen Zweck bestehen Monaden.

Bewahren der referenziellen Transparenz

Monaden gewährleisten außerdem, dass unreine Werte, beispielsweise Zahlen aus einer Tastatureingabe herrührend, nicht vom Typ Integer sind, sondern IO Integer. Da jedoch die Wertkonstruktoren des Typen IO nicht zur Verfügung stehen (→ abstrakter Datentyp), kann der als Monade bzw Aktion abgekapselte (en encapsulated) Wert auch nicht durch Dekonstruktion zurückgewonnen werden. Somit lassen sich Werte vom Typ IO t nur innerhalb bestehender IO t Ausdrücke verarbeiten. Auf diese Weise ist bereits anhand der Signatur ersichtlich, ob sich eine Funktion deterministisch oder unvorhergesehen (→ unreine Funktion bzw en impure function) verhält.

Monadische Verknüpfungen

In C-Sprachen dient der Strichpunkt (Semikolon) als Abschlusszeichen von Anweisungen, während in Python und Pascal bereits das Zeilenende genügt. In ähnlicher Weise erwartet Haskells ausdrucksorientierter Sprachbau, dass alle Teilausdrücke zu einem Ganzen verknüpft sind:

Anstelle eines Strichpunkts ; oder des Zeilenendes bedürfen Ausdrücke je nach Datentyp bestimmter Operatoren; denn ohne eine Verknüpfung zu einem Gesamtausdruck wäre nicht ersichtlich, für was der Name steht:

Folglich kann auch nicht von einem Anweisungsblock wie in imperativen Sprachen gesprochen werden:

Da in Haskell sogar Ein- und Ausgaben nichts anderes als Ausdrücke – vom Typ IO t – sind, bedürfen diese gleichfalls bestimmter Operatoren, um miteinander zu einer Kette verknüpft zu werden:

Datentypen gelten als Monaden, wenn diese eine Instanz für die Typklasse Monad bieten:

Neben IO sind gelten noch weitere Typen wie Maybe als Monaden.

Ein- und Ausgabe

Über den abstrakten Datentyp IO werden Aktionen beschrieben, welche allgemein mit dem Betriebssystem oder der Umgebung wechselwirken. Ein Objekt von der Datenart IO T stellt eine IO-Wirkung (In- / Output) dar, welche einen Wert vom Typ T zurückgibt.

Zum Verständnis lässt sich IO folgendermaßen definiert vorstellen:

Tatsächlich sind die Konstruktoren von IO hinter Funktionen wie putStrLn und getLine versteckt, bzw ist die konkrete Implementierung von IO pure Zauberei seitens GHC.

EA-Aktionen sind nicht über gewöhnliche Operatoren miteinander verknüpfbar:

Hier wäre nicht klar, in welcher Reihenfolge die beiden getLine-Funktionen verarbeitet werden würden.

Beispiel

 

Fehlerumgang

 

 

 

Unsichere Programmierung

 

unsafePerformIO

 

Module

Module bezeichnen Quelldateien mit einem jeweils eigenen Namensraum. Der Bezeichner eines Moduls entspricht dessen Dateinamen:

 

 

Dokumentation

Haddock ist das Tool der Wahl, um die API einer Bibliothek oder Anwendung zu dokumentieren:

 

 

Das Schreiben spezieller Kommentare neben Ihrem Code hilft dabei, Code und Dokumentation synchronisiert und auf dem neuesten Stand zu halten (da veraltete Dokumentation ein schmerzhaftes Problem darstellt). Außerdem sind die Dokumente sowohl für Betreuer als auch für Benutzer bequem. Und genau das bietet Haddock. Haskell ist nicht immer anfängerfreundlich, daher wird es sehr geschätzt, Benutzern Ihres Codes auf bequeme Weise zu helfen.

 

Parallele Programmierung

 

 

Reaktive Programmierung

 

 

Anhang

Lambda-Kalkül als Grundlage

Die theoretische Grundlage der funktionalen Programmierung bildet der Lambda-Kalkül von Alonzo Church. Der Lambda-Kalkül erlaubt es, vollständige Teilausdrücke separat auszuwerten. Dies ist der wichtigste Vorteil gegenüber der imperativen Herangehensweise, und vereinfacht die Programmverifikation sowie -optimierung erheblich.

 

Sprachebenen und Namensräume

In Haskell bildet Datentypen und Werte jeweils eigene Teilsprachen. Je nach Sichtweise ließen sich auch Module als eine dritte eigenständige Sprachebene auffassen.

Jede Sprachebene bildet einen eigenen unabhängigen Namensraum, sodass Funktionen nicht mit Typparametern und Wertkonstruktoren nicht mit Datentypen kollidieren:

Teilsprache Kleinschreibung Großschreibung
Term- / Wertebene Konstanten / Funktionen / Parameter Datenkonstruktoren
Typebene Typvariablen / -parameter Typkonstruktoren
Dateiebene Module als Namensräume

Ferner erleichtert die verbindliche Reglung von Groß- und Kleinschreibung ganz erheblich das Programmverständnis und ist sogar syntaktisch ausgenutzt:

Schlüsselwörter

Originale Übersicht: wiki.haskell.org/Keywords

SymbolBedeutung
--Zeilenkommentar
{- … -}Blockkommentar
(…) (…, …)Tupelkonstruktor
[…] […, …]Listenkonstruktor
!!Indexoperator
..Fortsetzungsoperator
'X'Einzelzeichen
"…"Zeichenkette
``Infix-Notation
::Typspezifikator: ist vom Typ
=>Vererbung
|Wächter; Alternative in Datendeklarationen
_Platzhalter in Muster
!Auswertung erzwingen
@Musterabgleiche: lesen als
[|…|]Vorlage (Metaprogrammierung mit Template Haskell)
\mehrzeilige Zeichenketten
~bedarfsweiser Musterabgleich (en lazy pattern match)
asUmbenennen von Modulimporten
case ofFallunterscheidung
classDeklaration: Typklasse
dataDeklaration: Konstruktor / algebraischer Datentyp
data familyGHC-Erweiterung; Deklaration: Typfamilie
data instanceGHC-Erweiterung; Deklaration: Instanz einer Typfamilie
defaultStandardimplementierung festlegen
derivingAbleiten von Instanzen
deriving instanceGHC-Erweiterung; alleinstehende Ableitung
doSyntaxzucker; Monaden
foreign importImport von fremdsprachigen Bibliotheken
foreign exporterlaubt Funktionsaufruf aus anderen Sprachen
hidingNamen vom unmittelbaren Import ausschließen
if then elsebedingter Ausdruck mit Alternative
importModulimport (Namen werden unmittelbar übernommen)
import qualifiedqualifizierter Import (Namen werden nicht übernommen)
infixDeklaration: nicht assoziativer Operator
infixlDeklaration: linksassoziativer Operator
infixrDeklaration: rechtsassoziativer Operator
instanceDeklaration: Umsetzung einer Typklasse für einen DT
let inlokale Namensbindungen eines Ausdrucks
moduleDeklaration: Namensraum zugehöriger Deklarationen
newtypeErzeugen eines neuen Datentyps mit eigenem Konstruktor
procVerallgemeinerung von Monaden zu Pfeilen (en arrows)
rec (-XDoRec)Erlauben von rekursiven Bindungen in do-Blöcken
typeDeklaration: Typalias (Typsynonym)
type familyGHC-Erweiterung; Deklaration: Typsynonymenfamilie
type instance—: Instanz einer Typsynonymenfamilie
whereEinführung: Modul, Instanz, Typklasse, GADT

Operatoren

+, -, *, /
Addition, Subtraktion, Multiplikation, Division (nur Gleitkommazahlen)
^, ^^, **
Potenzierung je nach Exponent: ganzzahlig, ganzzahlig negativ, gleitkommazahlig
==, /=, <, <= >, >=
Gleichheit, Ungleichheit, Kleiner, Kleinergleich, Größer, Größergleich
Not, &&, ||
Negation, Konjunktion, Disjunktion
Anmerkungen

Ein- und Ausgabefunktionen

Konsolenausgabe

Konsoleneingabe

Dateioperationen

Polymorphie in Haskell

 

 

Überladen und Autoumwandlungen (en coercion)