Vorwort: Warum Haskell?ÜberblickEinrichten & ÜbersetzenIDEStackGrundsyntaxFunktionenUngeschönte FunktionenLambda-AusdrückeOperatorenFunktionskompositionPunktfreier Stil Rekursion TypsystemGrundtypenAlgebraische DatentypenTypparameterExt: Lokale TypvariablenSymboleExt: Verallgemeinerte ADT (GADT)Ext: Existenzielle TypenTypumschreibungenUnverpackte TypenTypartenAusdrücke und BindungenVariablenbindungenBezeichnerlet als Ausdrucklet als Anweisungwhere-KlauselExt: Implizite ParameterKontrollstrukturenBedingter AusdruckFallunterscheidungenWächterEntstrukturierungenRekord-SyntaxBedarfsauswertungStrikte AuswertungUndefinierte WerteDatenstrukturenTupelnListenMengenTypklassenAbleiten von InstanzenExt: Multiparametrische TypklassenExt: Nullstellige TypklassenVariadische FunktionenMonadenMonadische VerknüpfungenEin- und AusgabeFehlerumgangUnsichere ProgrammierungModuleDokumentationParallele ProgrammierungReaktive ProgrammierungAnhangLambda-Kalkül als GrundlageSprachebenen und NamensräumeSchlüsselwörterOperatorenEin- und AusgabefunktionenPolymorphie in 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.
Merkmal | — |
---|---|
Paradigma | deklarativ: ausdrucksorientiert, funktional, modular |
Syntax | Python-artig: distinktive Einrückungen und Zeilenenden |
Speicherverwaltung | automatisch |
Typsystem | stark, statisch, implizit, strukturell |
Übersetzungsmodell | kompilierend |
Dateiendung | HS, LHS (literate Haskell– Programmtext mit LaTeX) |
Ersterscheinungsjahr | 1990 |
Ausgaben | Haskell 1.{0, 1, …, 4}, Haskell {98, 2010, 2020 (in Planung)} |
Erfinder | Komitee bestehend aus ca 25 Personen; u A John Hughes, Lennart Augustsson, Paul Hudak, Simon Peyton Jones |
Vorläufer | Hope, Miranda, Orwell |
beeinflusst von | Clean, Gofer, Lisp, ML |
Abkömmlinge | Idris, PureScript, Timber |
Standardisierungen | keine – akademisches Gemeinschaftsprojekt |
Standardumsetzung | GHC (Kompilierer) / GHCi (interaktive Umgebung) |
Paketverwalter | Stack (empfohlen), Cabal |
empfohlene IDE | Eclipse, Geany, IntelliJ, KDevelop, Visual Studio |
offizielle Webseiten | haskell.org | wiki.haskell.org |
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.
Das IntelliJ plugin for Haskell
bietet neben Syntaxhervorhebung viele weitere Funktionen, um das Programmieren mit Haskell äußerst komfortabel zu gestalten.
Die offizielle Erweiterung Haskell
beruht auf dem Haskell Language Server
.
xstack ghc DATEI
stack build DATEI
Über die Systembefehlszeile wird mit ghci
bzw stack ghci
oder stack repl
die interaktive Umgebung (en read–eval–print loop
) von Haskell gestartet:
xxxxxxxxxx
USER@SYSTEM:~$ stack ghci
GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help
Prelude> (\a b -> (a^2) + (2*a*b) + (b^2)) 2 3
25
Prelude>
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.
Aktion | Kürzel |
---|---|
Hilfe / Übersicht | :h :? |
GHCi beenden | :q |
Datentyp abfragen | :t VALUE |
Typart abfragen | :k TYPE |
Erweiterung einschalten | :set -X… |
Modul laden | :l NAME |
Nachfolgend einige Variablen sowie Funktionen für den Einstieg in Haskell:
xxxxxxxxxx
X@X:~$ stack ghci
Prelude> a = 9
Prelude> b = "Tach!"
Prelude> c = False
Prelude> mult a b = a * b
Prelude> mult a 9
81
Prelude> putStrLn b
Tach!
Prelude> c || not c
True
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 . |
xxxxxxxxxx
-- Zeilenkommentar
{-
Blockkommentar
-}
Blockkommentare sind nicht verschachtelbar.
xxxxxxxxxx
überschrift = "Papiergröße nach DIN\n"
papierfDIN :: Double -> Int -- Funktionsdeklaration (wahlfrei)
papierfDIN breite = round (breite * (2 ** 0.5)) -- Definition
-- möglich ist auch »… = floor (…)«
main = do
putStrLn überschrift
putStrLn "Höhe in mm:"
eingabe <- getLine
let breite = (read eingabe :: Double)
print (papierfDIN breite)
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:
xxxxxxxxxx
f a b c d e = a + b - c * d / e
wert = f 4
5 (2 / 6)
(5 * 4)
9
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.
xxxxxxxxxx
plus :: Int -> Int -> Int
-- Deklarationen von Einzelwerten sind unüblich
plus a b = a + b
main = do
print (plus 9223372036854775807 1) -- Höchstgrenze + 1
print (div 5 3) -- Ganzzahldivision
print (mod 5 3) -- Teilungsrest
print (-3.7e2) -- Wissenschaftliche Notation
print 'þ' -- Zeichenkodierung
print (1 / 2 :: Rational) -- Typangabe verhindert Auswertung
putStrLn ['H', 'a', 'l', 'l', 'o'] -- Zeichenkette
putStrLn "Hallo" -- altern Syntax für Zeichenketten
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 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:
xxxxxxxxxx
binom :: Int -> Int -> Int -> Int
binom m n x = (m * x) + n
y = binom 1 5 9 -- ≙ ((binom 1) 5) 9
Tatsächlich sind Funktionen in Haskell nur einstellig (
xxxxxxxxxx
binom m = let b1 n = (let b2 x = (m * x) + n in b2) in b1
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:
xxxxxxxxxx
a = binom
b = a 1 -- b :: Int -> Int -> Int
c = b 2 -- c :: Int -> Int
d = c 3 -- d :: Int
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:
xxxxxxxxxx
Prelude> bsp = map (binom 2 4) [1, 2, 3] -- Anwenden von »binom« auf mehrere x-Werte
Prelude> bsp
[6,8,10] -- Liste von y-Werten
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.
xxxxxxxxxx
Prelude> :t map
map :: (a -> b) -> [a] -> [b]
Innerhalb der Funktionssignatur fassen Klammern um mehrere Parameter diese zu einer Funktion 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.
Parameter können auch als Tupel zusammengefasst sein, was den Anschein von herkömmlichen Funktionen aus imperativen Sprachen weckt:
xxxxxxxxxx
Prelude> binom(m, n, x) = (m * x) + n
Prelude> binom(6, 4, 4)
28
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:
xxxxxxxxxx
Prelude> i (a, b) = a + b
Prelude> j = curry i
Prelude> i (8, 6)
14
Prelude> j 8 6
14
Prelude> k = uncurry j
Prelude> k 3 4
7
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:
xxxxxxxxxx
Prelude> data Rechteck a = Rechteck a a
Prelude> rechteckFl (Rechteck a b) = a * b
Prelude> rechteckFl (Rechteck 6.7 4.8)
32.16
Neben der deklarativen Funktionsbeschreibung f x = …
bietet Haskell auch eine Syntax für sog namenlose Funktionen
, fachlich λ-Ausdrücke genannt:
xxxxxxxxxx
Prelude> (\m x n -> (m * x) + n) 2 8 4
20
Lambda-Ausdrücke treten zumeist als "Einwegfunktionen" in Erscheinung, wenn diese nur einmal als Argument benötigt werden:
xxxxxxxxxx
Prelude> filter (\ x -> even x && x > 10) [1..20]
[12,14,16,18,20]
Genauso wie bei benannten Funktionen, lassen sich die Parameter literaler Funktionen als Tupel zusammenfassen:
xxxxxxxxxx
Prelude> (\(m, x, n) -> (m * x) + n) (6, 8, 2)
50
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:
xxxxxxxxxx
Prelude> f m n x = (m * x) + n
Prelude> g = (\m x -> f m 3 x)
Prelude> g 3 1
6
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.
Parameter im deklarativen Stil werden als freie Variablen angesehen, da diese außerhalb des Lambda-Ausdrucks gebunden vorliegen:
xxxxxxxxxx
f x = (\y -> x + y)
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.
Jede zweistellige Funktion in Haskell kann als Infix-Operator verwendet werden, wenn der Name zwischen zwei schrägen Hochstrichen ``
steht:
xxxxxxxxxx
Prelude> quadr a b = a * b
Prelude> 2 `quadr` 3
6
Umgekehrt sind die bestehenden Operatoren wie Funktionsnamen verwendbar, wenn diese in Rundklammern ()
erfolgen:
xxxxxxxxxx
Prelude> (^) 7 8
5764801
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:
xxxxxxxxxx
Prelude> (2 ^) 6 -- Linksstück
64
Prelude> (^ 2) 6 -- Rechtsstück
36
Prelude> (\x -> x ^ 2) 6 -- oder als literale Funktion verpackt
36
Weitere nützliche Stückelungen (en sections
) sind u A:
xxxxxxxxxx
Prelude> (1+) 9 -- Inkrement
10
Prelude> (2*) 3 -- Verdopplungsfunktion
6
Prelude> putStrLn (('\t':) "eingerückt!") -- »('\t':)«
eingerückt!
Entspricht das Ergebnis einer Funktion f dem Typ des einzigen Parameters einer Funktion g, so können beide Funktionen miteinander verknüpft werden:
xxxxxxxxxx
f :: Num a => a -> String
g :: Num c => String -> c
h :: Num c => c -> (String, c)
f a = "Hallo"
g b = 1
h c = ("Welt!", c)
main = print ((h . g . f) 5) -- ≙ »h (g (f x))«
Der Operator .
verkettet zwei Funktionen zu einer Gesamtabbildung, indem der Rückgabewert der rechten Funktion den Parameter der linken Funktion bedient:
xxxxxxxxxx
Prelude> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
Prelude> :t (h . g . f)
(h . g . f) :: Num t => t1 -> ([Char], t)
Zu unterscheiden von der Komposition ist die bloße Funktionsanwendung:
xxxxxxxxxx
Prelude> :t ($)
($) :: (a -> b) -> a -> b
Prelude> putStrLn (show (1 + 1))
Prelude> putStrLn (show $ 1 + 1)
Prelude> putStrLn $ show (1 + 1)
Prelude> putStrLn $ show $ 1 + 1
Der Zweck des Operators $
besteht lediglich darin, durch eine extrem niedrige Operatorpriorität Rundklammern um Argumente einzusparen.
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:
xxxxxxxxxx
Prelude> f x = x + 1 -- ausdrückliches Erwähnen des Parameters
Prelude> g = (+ 1) -- impliziter Parameter durch Teilanwendung von (+)
Prelude> :t f
f :: Num a => a -> a
Prelude> :t g -- 'f' und 'g' sind völlig gleichwertig
g :: Num a => a -> a
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.
xxxxxxxxxx
Prelude> owl = (.)$(.) -- ≙ 'owl a b c d = a b (c d)'
Prelude> :t owl
owl :: (a1 -> b -> c) -> a1 -> (a2 -> b) -> a2 -> c
Prelude> owl (==) 1 (1+) 0
True
Kombinatoren sind das Gegenteil von Funktionsabschlüssen.
Selbstaufrufe von Funktionen stellen eine Spezialform der Funktionskomposition dar:
xxxxxxxxxx
Prelude> f x = 2 * x
Prelude> g = f . f -- ≙ 'g x = (x * 2) * 2'
Prelude> h = f . f . f -- ≙ 'h x = ((x * 2) * 2) * 2'
Prelude> h 7 -- ≙ 'dup 7 3'
56
xxxxxxxxxx
dup x n
| 0 <- n = x
| otherwise = dup (2 * x) (n - 1)
Über einen Fixpunkt-Kombinator lassen sich auch namenlose Funktionen rekursiv definieren:
xxxxxxxxxx
Prelude> fix f = f (fix f)
Prelude> fix (\f n -> if n == 0 then 1 else n * f (n - 1)) 6
720
Prelude> dup = fix (\f x n -> if n == 0 then x else f (2 * x) (n - 1))
Prelude> dup 3 5
96
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.
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!
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:
xxxxxxxxxx
Prelude> :t 334
334 :: Num t => t
Prelude> :t 'h'
'h' :: Char
Prelude> :t True
True :: Bool
Prelude> :t (\x -> print x)
(\x -> print x) :: Show a => a -> IO ()
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.
Datenart | Typkonstruktor | Bsp | Wertebereich |
---|---|---|---|
Wahrheitstyp | Bool | True | { False , True } |
Ganzzahl – 32 / 64 Bit | Int | 2 | [-231; 231 – 1] / … |
Ganzzahl – beliebig | Integer | -137842 | ℕ (unendlich) |
Gleitkommazahl – 32 Bit | Float | 1.66 | … |
Gleitkommazahl – 64 Bit | Double | 3,14159 | … |
Bruchzahl | Rational | 1 / 9 | ℚ |
Einzelzeichen | Char | 'þ' | Unicode |
Zeichenkette | String = [Char] | "Tach!" | Unicode |
Der Typ Int
entspricht einer gewöhnlichen 32- oder 64-bit großen Zahl, während Integer
beliebig groß sein kann und lediglich vom vorhandenen Speicher begrenzt wird, sodass arithmetische Überläufe nicht zu befürchteten sind, jedoch mit dem Nachteil, dass Operationen langsamer ablaufen als mit Int
.
Gegenüber Double
weist Float
eine geringere Genauigkeit auf, bietet aber auf einem 64-bit Rechner für gewöhnlich keinen Mehrgewinn, weshalb dringend empfohlen wird, auf Float
zu verzichten; auch um weniger numerische Fehler zu provozieren.
Für speicherschonende Berechnungen von beliebiger Genauigkeit besteht noch ein weiterer Typ Scientific
im Modul Data.Scientific
.
Der ursprüngliche Zeichenkettentyp in Haskell stellt lediglich eine Liste von Einzelzeichen dar.
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:
Jede vollwertige Programmiersprache weist zumindest ein gleichartiges Konzept auf:
struct) und Klasse in objektorientierter Sicht
record)
namedtupleoder literal)
Ein Summentyp hingegen lässt sich als Vereinigung – bzw Summe – von bestehenden Mengen auffassen:
Nur funktionale Sprachen sowie moderne Neuentwicklungen wie Swift oder Rust weisen echte Summentypen vor:
union)
enum)
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:
xxxxxxxxxx
-- allg: »data Typkonstruktor = Datenkonstruktor DATENTYPEN«
-- Produkttyp: String × String
data Name = Name String String deriving (Show)
-- Summentyp: Straße + Postfach
data Str_Pf = Straße String Int | Postfach Int deriving (Show)
-- Typsynonyme:
type PLZ = Int
type Ort = String
-- Produkttyp: Name × Str_Pf × PLZ × Ort
data Adr = Anschrift Name Str_Pf PLZ Ort deriving (Show)
anschrift = Anschrift (Name "Hans" "Wurst") (Postfach 4532) 34645 "Fleischhausen"
-- Name :: String -> String -> Name
-- Straße :: String -> Int -> Str_Pf
-- Postfach :: Int -> Str_Pf
main = do
print anschrift
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.
Typkonstruktoren sind Funktionen auf Typebene, welche einen konkreten Typen erzeugen:
xxxxxxxxxx
Prelude> data Person id = Personalien id String String
Prelude> :t PersonalienPersonalien
Personalien :: id -> String -> String -> Person id
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:
xxxxxxxxxx
Prelude> data AuswNr = Ausweisnummer Int Int Int Int
Prelude> :t Ausweisnummer
Ausweisnummer :: Int -> Int -> Int -> Int -> AuswNr
Prelude> person = Personalien (Ausweisnummer 1932 23 255 832) "Olaf" "Schubert"
*Main> :t person
person :: Person AuswNr
In diesem Beispiel könnte anstelle einer viergliedrigen Nummer in Form eines Produkttypen Int × Int × Int × Int
genauso gut eine alphanumerische Variante genutzt werden:
xxxxxxxxxx
Prelude> data AuswNr = Ausweisnummer [Char] [Char] [Char] [Char]
Prelude> person = Personalien (Ausweisnummer "A17" "38FG" "HR5" "89ZT") "Olaf" "Schubert"
Mit Typparametern erlaubt Haskell das Zusammenfassen von ähnlichen Datenstrukturen zu einer allgemeinen Beschreibung, welche für den jeweiligen Anwendungsfall anpassbar ist.
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
gelesen werden, formalisiert durch den Allquantor ∀ (en a
und b
gilt …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:
xxxxxxxxxx
-- nicht kompilierend:
f :: Read a => String -> [a]
f s = g ("[" ++ s ++ "]") -- Fehler!
where
g :: String -> a -- Typvariable 'a' von 'g' ist zu allg für 'f'
g a = read a
-- kompilierend: gleiche Typbeschränkung
f :: Read a => String -> [a]
f s = g ("[" ++ s ++ "]")
where
g :: Read a => String -> a -- gleichwertige Typvariable zu 'f'
g a = read a
-- kompilierend: implizite Typbeschränkung
f :: forall a. Read a => String -> [a]
f s = g ("[" ++ s ++ "]")
where
g :: String -> [a]
g a = read a
Die erste Version wird vom Übersetzer abgelehnt, da der Parameter von g
nicht gleichermaßen auf die Typklasse Read
beschränkt wurde.
Nullstellige Datenkonstruktoren beinhalten nur ihren Namen als symbolische Information:
xxxxxxxxxx
data Bool = False | True -- data #1 = #2 #3 #4
Bool.
Falsch.
|
ist als ausschließendes oder(XOR) zu werten: entweder … oder …
Wahr.
Verallgemeinerte algebraische Datentypen (en Generalised Algebraic Data Types
– GADT) erweitern gewöhnliche algebraische Datentypen, indem die Signatur der Konstruktoren genau aufgeschlüsselt wird:
xxxxxxxxxx
data Expr a where
I :: Int -> Expr Int
B :: Bool -> Expr Bool
Add :: Expr Int -> Expr Int -> Expr Int
Mul :: Expr Int -> Expr Int -> Expr Int
Eq :: Eq a => Expr a -> Expr a -> Expr Bool
eval :: Expr a -> a
eval (I n) = n
eval (B b) = b
eval (Add e1 e2) = eval e1 + eval e2
eval (Mul e1 e2) = eval e1 * eval e2
eval (Eq e1 e2) = eval e1 == eval e2
main = print (eval (Add (I 4) (I 5)))
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.
Beim Definieren eines Typen muss jede Typvariable ausdrücklich als Typparameter auf der rechten Seite vom Gleichheitszeichen eingeführt werden:
xxxxxxxxxx
data Verschieden t = Verschieden t
Für t
ist jeder beliebige Typ einsetzbar:
xxxxxxxxxx
Prelude> :t Verschieden 23
Verschieden 23 :: Num t => Verschieden t
Prelude> :t Verschieden "Hallo"
Verschieden "Hallo" :: Verschieden [Char]
Prelude> :t Verschieden True
Verschieden True :: Verschieden Bool
Da aber jede dieser Konstruktionen zu Objekten unterschieden Typs führt, lassen sich diese bspw nicht in einer Liste zusammen:
xxxxxxxxxx
Prelude> l = [Verschieden 23, Verschieden "Hallo", Verschieden True]
<interactive>:2:43: error:
• Couldn't match type ‘Bool’ with ‘[Char]’
Expected type: Verschieden [Char]
Actual type: Verschieden Bool
• In the expression: Verschieden True
In the expression:
[Verschieden 23, Verschieden "Hallo", Verschieden True]
In an equation for ‘l’:
l = [Verschieden 23, Verschieden "Hallo", Verschieden True]
Prelude> l = [Verschieden False, Verschieden False, Verschieden True]
Prelude> :t l
l :: [Verschieden Bool]
Mit der Erweiterung von Existenzialaussagen sind Typen definierbar, dessen Typvariaben nicht zur Übersetzungszeit aufgelöst werden, sondern ihre Generizität beibehalten:
xxxxxxxxxx
data Beliebig = forall x. Show x => Beliebig x
instance Show Beliebig where show (Beliebig x) = show x
bsp = [Beliebig 42, Beliebig "Hallo!", Beliebig True]
-- bsp :: [Beliebig]
Existentielle Typen sind nichts anderes als Haskells Ansatz, etwas dynamische Typisierung in der sonst ausschließlich statisch typisierten Sprache zu simulieren.
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:
xxxxxxxxxx
type VName = String
type NName = String
type PLZ = Int
type Ort = String
type Str = String
type HausNr = Int
type Postf = Int
data Adr =
Adr_Str NName VName Str HausNr PLZ Ort |
Adr_Postf NName VName Postf PLZ Ort
deriving (Show)
anschrift = Adr_Postf "Wurst" "Hans" 4532 34645 "Fleischhausen"
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:
xxxxxxxxxx
data Anschrift =
Anschrift String Int Int String
-- einfachheitshalber gleicher Name wie DT
| Postfach Int Int String
-- Spezialfall: Anschrift mit Postfach
deriving (Show)
anschrift = Postfach 410655 99084 "Erfurt"
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:
xxxxxxxxxx
newtype Email = Email String --
email :: String -> Bool
email str = True
test = email (Email "bla@keinplan.de") -- Fehler!
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
).
Fast alle Werte in Haskell sind als Verweis auf ein Objekt im Freispeicher umgesetzt:
xxxxxxxxxx
data T = C Int Char
t = C 3 'a'
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 #
:
xxxxxxxxxx
import GHC.Exts -- primitive Datentypen
zeig_Int_unverpackt :: Int# -> String
zeig_Int_unverpackt n = (show $ I# n) ++ "#"
main = print (zeig_Int_unverpackt 5#)
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.
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.
xxxxxxxxxx
dividieren :: Double -> Double -> Double
dividieren a b = if b == 0 then error "Division durch Null!" else a / b
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:
Um Abstraktionen wie Polymorphie und Bedarfsauswertung zu ermöglichen, sind die allermeisten Datentypen in Strukturen mit zusätzlichen Daten eingebettet (en boxed
).
Jeder Typkonstruktor in Haskell weist eine bestimmte Typart (en kind
) vor, welche seine Arität genau spezifiziert:
xxxxxxxxxx
Prelude> :kind Bool -- nullstellig; statt ':kind' reicht auch ':k'
Bool :: *
Prelude> :k Maybe -- ein Typparameter
Maybe :: * -> *
Prelude> :k Either -- zweistellig
Either :: * -> * -> *
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.
Mit der Freischaltung PolyKinds
sind auch Typparameter variabler Stelligkeit definierbar:
xxxxxxxxxx
data Proxy (a :: k) = MkProxy
maybeRepresentative = MkProxy :: Proxy Maybe
functionRepresentative = MkProxy :: Proxy (->)
helloRepresentative = MkProxy :: Proxy "hello"
showRepresentative = MkProxy :: Proxy Show
functorRepresentative = MkProxy :: Proxy Functor
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:
xxxxxxxxxx
cond = False
expr = if cond then 3 else 4
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:
xxxxxxxxxx
bool cond = false;
int expr = cond ? 2 : 6;
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.
Jeder Ausdruck in Haskell führt wie ein mathematischer Term zu einem neuen Wert:
xxxxxxxxxx
Prelude> u r = 2 * r * pi -- parametrische Bindung / Anweisung
Prelude> u 1.5 -- Ausdruck → Auswertung
9.42477796076938 -- Ergebnis der Auswertung
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
.
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:
xxxxxxxxxx
Prelude> x = 1
Prelude> x' = 3
Prelude> x
1
Prelude> x'
3
Prelude> _x
5
Zu beachten ist, dass Haskell den Unterstrich _
als Kleinbuchstabe behandelt, sodass die folgenden Namen statthaft sind:
xxxxxxxxxx
Prelude> _x = 3
Prelude> _' = 4
Prelude> _3 = 5
Prelude> _x
3
Prelude> _'
4
Prelude> _3
5
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.
xxxxxxxxxx
Prelude> (let x = 2 in x*2) + 3 -- let X = AUSDRUCK in AUSDRUCK
7
Das Schlüsselwort let
erlaubt auch mehrere Bindungen gleichzeitig:
xxxxxxxxxx
{-
NAME PARAMERTER … = let
NAME = AUSDRUCK
…
in AUSDRUCK
-}
integral a b = let
stammf x = ((x ** 4) / 4) + (x ** 2)
fa = stammf a
fb = stammf b
in abs (fb - fa) -- »abs« = Betragsfunktion (en „absolute value“)
main = do
print (integral 2 6.5)
Man beachte, dass alle Ausdrücke unter dem Namen bzw let
eingerückt sind.
Variablenbindungen innerhalb eines do-Blocks werden wie Anweisungen gehandhabt, sodass let
– ohne Rückbezug auf einen Ausdruck durch in
– zu verwenden ist:
xxxxxxxxxx
do
ANWEISUNGEN
let NAME = AUSDRUCK
ANWEISUNGEN
Der zweite Anwendungsfall von let-Anweisungen sind Listenbeschreibungen:
xxxxxxxxxx
Prelude> [(x, y) | x <- [1..3], let y = 2 * x]
[(1,2),(2,4),(3,6)]
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:
xxxxxxxxxx
goldenerSchnitt b = ϕ * b
where
zähler = 1 + (5 ** (1 / 2))
ϕ = zähler / 2
Mit where
wird eine Umgebung geboten, um Variablen zwischen verschiedenen Abschnitten einer Funktion zu teilen, welche jeweils eigenständige Ausdrücke bilden:
xxxxxxxxxx
collatz :: Integer -> Integer
collatz n
| n == 1 = 1
| even = rec n -- wenn n gerade
| not even = rec ((3 * n) + 1) -- wenn n ungerade
where
even = (mod n 2) == 0 -- wahr oder falsch
rec x = collatz (div x 2) -- Rekursion (Selbstaufruf)
main = do
print (collatz 45348797643437346896723673247437898965434356768532875423661)
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.
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:
xxxxxxxxxx
data Lang = DE | EN
app :: (?config :: Lang) => IO ()
app = putStrLn $ case ?config of
DE -> "Hallo Welt!"
EN -> "Hello, world!"
main = let ?config = DE in app
Das Fragezeichen ?
fungiert als Präfix, um implizite Parameter von gewöhnlichen Namensbindungen zu unterscheiden.
In Haskell bestehen nur Verzweigungen, während Schleifen durch Rekursion zu lösen sind. Zudem liegen Kontrollstrukturen als Ausdrücke vor:
xxxxxxxxxx
Prelude> if True then 3 else 5
3
Neben dem althergebrachten wenn … dann … sonst …
-Konstrukt bietet Haskell auch mehrere Arten der Fallunterscheidung.
xxxxxxxxxx
-- if BEDINGUNG then AUSDRUCK else AUSDRUCK
testBuchstabe x = if x >= 'a' && x <= 'z'
then "Kleinbuchstabe"
else if x >= 'A' && x <= 'Z'
then "Großbuchstabe"
else "Kein ASCII-gültiger Buchstabe"
main = do putStrLn (testBuchstabe 'ä')
Bedingte Ausdrücke erfordern stets die Angabe eines Alternativwertes.
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:
xxxxxxxxxx
-- case AUSDRUCK of MUSTER -> AUSDRUCK
komische_zahlenreihe x = case x of
0 -> 63 -- ertes Muster '0'
1 -> 24
2 -> 92
_ -> 1 -- allgemeines Muster, um alle übrigen Fälle abzudecken
-- oder einzeilig:
komische_zahlenreihe x = case x of {0 -> 63; 1 -> 24; 2 -> 92; _ -> 1}
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:
xxxxxxxxxx
komische_zahlenreihe 0 = 63
komische_zahlenreihe 1 = 24
komische_zahlenreihe 2 = 92
komische_zahlenreihe x = 1
-- oder einzeilig (Semikolon als syntaktischer Trenner):
komische_zahlenreihe 0 = 63; komische_zahlenreihe 1 = 24; …
Zu beachten ist, dass der Übersetzer den ersten zutreffenden Fall, beginnend vom Dateianfang, auswählt.
Parameter lassen sich auch unmittelbar prüfen:
xxxxxxxxxx
-- NAME PARAMETER | BEDINGUNG = AUSDRUCK
testBuchstabe x
| x >= 'a' && x <= 'z' = "Kleinbuchstabe"
| x >= 'A' && x <= 'Z' = "Großbuchstabe"
| otherwise = "Kein ASCII-gültiger Buchstabe"
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:
xxxxxxxxxx
f x
| x >= 1 = ((x ^ 2) - x) / x
| otherwise = 0
entspricht:
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:
xxxxxxxxxx
-- NAME PARAMETER | LITERAL <- PARAMTER = AUSDRUCK
fn x y
| 0 <- y = error "Division durch Null!"
| 1 <- x, y > 0 = 1
| 1 <- x = 1 / y
| x > 1, y < 0 = ((x ^ 2) - x) / (- y)
| otherwise = 0
Muster und Bedingungen sind beliebig miteinander kombinierbar, solange diese durch ein Komma voneinander getrennt sind.
Die Spracherweiterung MultiWayIf
schaltet das Verwenden von Wächtern auch für bedingte Ausdrücke frei:
xxxxxxxxxx
fn x y = if | x == 1 -> "a"
| y < 2 -> "b"
| 2 <- y -> "c"
| otherwise -> "d"
Summentypen erfordern, dass alle Varianten abgedeckt werden:
xxxxxxxxxx
data Viereck = Rechteck Double Double | Quadrat Double | Raute Double
umfang (Rechteck 1 1) = 4
umfang (Quadrat 1) = 4
umfang (Raute 1) = 4
umfang (Rechteck a b) = 2 * a * b
umfang (Quadrat a) = 4 * a
umfang (Raute a) = 4 * a
Das einzelne Ansprechen von Komponenten innerhalb eines Musters KONSTRUKTOR x y …
wird auch Dekonstruktion oder Entstrukturierungen genannt.
Die Record-Syntax erlaubt das automatische Erstellen von Funktionen für den Zugriff auf die einzelnen Daten (Argumente) eines Wertkonstruktors.
xxxxxxxxxx
data Config = Config
String -- Benutzername
String -- Localhost
String -- Remotehost
Bool -- Ist Gast?
Bool -- Ist Admin?
String -- gegenwärtiges Verzeichnis
String -- Heimverzeichnis
Integer -- Verbindungszeit
deriving (Eq, Show)
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:
xxxxxxxxxx
getUserName (Configuration un _ _ _ _ _ _ _) = un
getLocalHost (Configuration _ lh _ _ _ _ _ _) = lh
getRemoteHost (Configuration _ _ rh _ _ _ _ _) = rh
getIsGuest (Configuration _ _ _ ig _ _ _ _) = ig
Glücklicherweise bietet Haskell hierfür Syntaxzucker, woraus die erforderlichen Zugriffsfunktionen automatisch generiert werden:
xxxxxxxxxx
data Config = Config {
username, localHost, remoteHost, currentDir, homeDir :: String
, isGuest, isSuperuser :: Bool
, timeConnected :: Integer
}
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.
Durch Auslagern von Datentypen in jeweils eigene Module lassen sich Namenskollisionen vermeiden.
Die Feldnamen werden durch eindeutige Anfügungen kenntlich gemacht:
xxxxxxxxxx
data User = User { u_name :: String, uid :: Int }
data Group = Group { g_name :: String, gid :: Int }
Die GHC-Erweiterung DuplicateRecordFields
erlaubt das mehrfache Verwenden eines Feldnamens für unterschiedliche Datentypen:
xxxxxxxxxx
data User = User { name :: String, uid :: Int }
data Group = Group { name :: String, gid :: Int }
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.
Anfang April 2020 wurde eine neue Erweiterung RecordDotSyntax
beschlossen, welche gestattet, auf die Felder in gewohnter Punktnottation zuzugreifen, womit Haskell endlich echte Records bietet.
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:
xxxxxxxxxx
Prelude> a = 1
Prelude> b = 3
Prelude> a * 6 + b
9
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:
xxxxxxxxxx
Prelude> f a b = a
Prelude> f 36 (24 + 32)
36
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:
xxxxxxxxxx
Prelude> unless condition expr alt = if not condition then expr else alt
Prelude> unless (4 > 5) True False
True
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.
Falls eine Funktion zu einem späteren Zeitpunkt vervollständigt oder implementiert werden soll, bietet Haskell hierfür den generischen Wert undefined
:
xxxxxxxxxx
Prelude> let (x, y) = (undefined, "Hallo!") in y
"Hallo!"
Das Programm kompiliert dennoch, wirft jedoch bei Ausgabe von x einen Laufzeitfehler.
Haskell kennt zwei grundlegende Datenstrukturen, um mehrere Werte zu verwalten:
Tupel | Liste | |
---|---|---|
(EXPR, EXPR, …) | [EXPR, EXPR, …] | |
Struktur | Elemente beliebigen Typs | gleichtypige Elemente |
Typ | (A, B, …) | [A] |
Erweiterbarkeit | feste Größe | ergänzbar um neue Elemente |
xxxxxxxxxx
xxxxxxxxxx
Die Bedarfsauswertung ermöglicht das Beschreiben von nicht terminierenden Datenstrukturen, welche erst bei Anwendung berechnet werden:
xxxxxxxxxx
λ> quadratzahlen n = (n * n) : quadratzahlen (n + 1)
λ> :t (!!)
(!!) :: [a] -> Int -> a
λ> quadratzahlen 0 !! 9
81
λ> squares 7
[49,64,81,100,121,144,169,196,225,… -- Abbruch erst mit Strg + c
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.
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:
xxxxxxxxxx
-- ./Module.hs
import qualified Data.Set as Set
import Data.Set (Set)
s1 = Set.empty
s2 = Set.singleton 'c'
s3 = Set.fromList "Hello, World!"
s4 = Set.fromList ['a'..'z']
xxxxxxxxxx
Prelude> :l Module
*Module> map Set.null [Set.fromList ['a'..'z'], Set.empty]
[False,True]
*Module> Set Data.Set> map Set.size [Set.fromList ['a'..'z'], Set.empty]
[26,0]
*Module> Set.union s2 s3
fromList " !,HWcdelor"
Das Module Data.Set
enthält zahlreiche Funktionen zum Verarbeiten von Mengen.
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:
xxxxxxxxxx
data Quadrupel = Quadrupel Double Double Double Double deriving (Show)
instance Num Quadrupel where
Quadrupel a1 a2 a3 a4 + Quadrupel b1 b2 b3 b4
= Quadrupel (a1 + b1) (a2 + b2) (a3 + b3) (a4 + b4)
quadrupel = Quadrupel 1 5 3 4 + Quadrupel 1 2 3 4
main = print quadrupel
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:
xxxxxxxxxx
Prelude> :info Num
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a
fromInteger :: Integer -> a
-- Defined in ‘GHC.Num’
instance Num Word -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’
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:
xxxxxxxxxx
Prelude> :info Fractional
class Num a => Fractional a where
(/) :: a -> a -> a
recip :: a -> a
fromRational :: Rational -> a
-- Defined in ‘GHC.Real’
instance Fractional Float -- Defined in ‘GHC.Float’
instance Fractional Double -- Defined in ‘GHC.Float’
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!
Haskell bietet einen internen Mechanismus, um vollständige Instanzen für die Typklassen Eq
, Ord
, Enum
, Bounded
, Show
sowie Read
automatisch zu erstellen:
xxxxxxxxxx
data DtBlatt = Sieben | Acht | Neun | Zehn | Unter | Ober | König | Daus
deriving (Eq, Ord, Enum, Bounded, Show, Read)
main = do
putStrLn "Eq-Klasse – Gleichheit / Ungleichheit:"
print (Sieben /= Ober) -- Eq
print (König == Daus)
putStrLn "Ord-Klasse – Größer / Kleiner:"
print (Acht > Unter) -- Ord
print (Acht < Unter)
putStrLn "Enum-Klasse – Vorgänger / Nachfolger:"
print (pred König)
print (succ König)
putStrLn "Bounded-Klasse – Unter- / Oberwert:"
print (minBound :: DtBlatt)
print (maxBound :: DtBlatt)
putStrLn "Show-Klasse – Umwandeln zu Text:"
print König -- Show
putStrLn "Read-Klasse – Lesen aus Text:"
eingabe <- getLine
print (read eingabe :: DtBlatt) -- unpassende Eingaben werden verworfen
Die Spracherweiterung MultiParamTypeClasses
gestattet mehrere Typparameter:
xxxxxxxxxx
class Convertible a b where
convert :: a -> b
instance Convertible Bool Integer where
convert x = if x == True then 1 else 0
instance Convertible Integer Bool where
convert x = if x == 0 then False else True
Multiparametrische Typklassen weisen den Nachteil auf, dass die Eindeutigkeit verloren geht und manuelle Typfixierungen vorgenommen werden müssen, wenn kein Kontext vorliegt:
xxxxxxxxxx
class C a b where
f :: a -> b
instance C Integer String where
f x = "0"
instance C Integer Char where
f x = '0'
x = f (58 :: Integer) :: String
Mit funktionalen Abhängigkeiten – oder kurz Fundeps
– lassen sich Beziehungen zwischen den Typklassenparametern ausdrücken:
xxxxxxxxxx
class C a b | a -> b where
f :: a -> b
instance C Integer String where
f x = "0"
instance C Double String where
f x = "0.0"
x = f (58 :: Integer)
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.
Mittels Erweiterung erlaubt Haskell auch parameterlose Typklassen:
xxxxxxxxxx
class Conf where
setting1 :: Int
setting2 :: Int
instance Conf where
setting1 = 1
setting2 = 0
set :: Conf => Int
set = setting1 + 4
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.
Über trickreiches Verwenden von Typklassen lassen sich typsichere Funktionen mit beliebig vielen Argumenten nachbilden:
xxxxxxxxxx
class Variadic p result where
variadic :: (?operation :: p -> p -> p) => p -> result
instance Variadic p p where
variadic = id
instance (Variadic p result) => Variadic p (p -> result) where
variadic x y = variadic (?operation x y)
-- zum besseren Verständnis auch definierbar als:
-- variadic x = \y -> variadic (?operation x y)
summate :: (Variadic n r, Num n) => n -> r
summate = let ?operation = (+) in variadic
main = print (summate
(43.4 :: Double) (37.28 :: Double) (5.17 :: Double) :: Double)
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.
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:
Typkonstruktor m t
t
korrespondierenden Monadentyp m t
Einheitsfunktion t -> m t
t
auf den korrespondierenden monadischen Wert abreturn
/ pure
Bindungsfunktion (bind-Operator) m a -> (a -> m b) -> m b
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.
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.
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:
xxxxxxxxxx
NAME PARAM =
EXPR ○
EXPR
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:
xxxxxxxxxx
NAME PARAM =
EXPR
EXPR
Folglich kann auch nicht von einem Anweisungsblock wie in imperativen Sprachen gesprochen werden:
xxxxxxxxxx
DT NAME(DT PARAM) {
ANWEISUNG;
ANWEISUNG;
…
return EXPR;
}
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:
xxxxxxxxxx
return :: Monad m => a -> m a
(>>) :: Monad m => m a -> m b -> m b
(>>=) :: Monad m => m a -> (a -> m b) -> m b
return
verpackt einen Wert in eine Monade>>
hängt zwei monadische Ausdrücke bzw Aktionen hintereinander>>=
hängt zwei monadische Ausdrücke hintereinander, wobei das Ergebnis der ersten Aktion als Eingabe (Argument) an die nachfolgende Aktion übergeben wird.Datentypen gelten als Monaden, wenn diese eine Instanz für die Typklasse Monad
bieten:
xxxxxxxxxx
Prelude> :i Monad
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
-- Defined in ‘GHC.Base’
instance Monad (Either e) -- Defined in ‘Data.Either’
instance Monad [] -- Defined in ‘GHC.Base’
instance Monad Maybe -- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
instance Monad ((->) r) -- Defined in ‘GHC.Base’
instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base’
Neben IO
sind gelten noch weitere Typen wie Maybe
als Monaden.
Ü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:
xxxxxxxxxx
data IO a = ReadFile a
| WriteFile a
| Network a
| StdOut a
| StdIn a
…
| GenericIO a
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:
xxxxxxxxxx
result = getLine ++ getLine -- Nicht möglich!
Hier wäre nicht klar, in welcher Reihenfolge die beiden getLine-Funktionen verarbeitet werden würden.
xxxxxxxxxx
erfragName =
putStrLn "Wie heißt du?" >>
getLine >>=
\name -> putStr ("Hallo " ++ name ++ ", ")
-- '++' verknüpft zwei Zeichenketten miteinander
main =
erfragName >> putStrLn "schön dich kennenzulernen!"
unsafePerformIO
Module bezeichnen Quelldateien mit einem jeweils eigenen Namensraum. Der Bezeichner eines Moduls entspricht dessen Dateinamen:
xxxxxxxxxx
-- ./Example.hs
module Example where
f = id
xxxxxxxxxx
Prelude> :l Module.hs
Haddock ist das Tool der Wahl, um die API einer Bibliothek oder Anwendung zu dokumentieren:
xxxxxxxxxx
data Side t = Long t | Short t
golden_ratio = (1 + (5 ** (1 / 2))) / 2
harmonize (Long n) = n / golden_ratio
harmonize (Short n) = n * golden_ratio
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.
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.
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:
xxxxxxxxxx
Prelude> {extract Nothing = 0; extract (Just x) = x} -- Dekonstruktion
Prelude> extract (Just x) = x
Prelude> extract (Just 30)
30
Originale Übersicht: wiki.haskell.org/Keywords
Symbol | Bedeutung |
---|---|
-- | 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) |
as | Umbenennen von Modulimporten |
case of | Fallunterscheidung |
class | Deklaration: Typklasse |
data | Deklaration: Konstruktor / algebraischer Datentyp |
data family | GHC-Erweiterung; Deklaration: Typfamilie |
data instance | GHC-Erweiterung; Deklaration: Instanz einer Typfamilie |
default | Standardimplementierung festlegen |
deriving | Ableiten von Instanzen |
deriving instance | GHC-Erweiterung; alleinstehende Ableitung |
do | Syntaxzucker; Monaden |
foreign import | Import von fremdsprachigen Bibliotheken |
foreign export | erlaubt Funktionsaufruf aus anderen Sprachen |
hiding | Namen vom unmittelbaren Import ausschließen |
if then else | bedingter Ausdruck mit Alternative |
import | Modulimport (Namen werden unmittelbar übernommen) |
import qualified | qualifizierter Import (Namen werden nicht übernommen) |
infix | Deklaration: nicht assoziativer Operator |
infixl | Deklaration: linksassoziativer Operator |
infixr | Deklaration: rechtsassoziativer Operator |
instance | Deklaration: Umsetzung einer Typklasse für einen DT |
let in | lokale Namensbindungen eines Ausdrucks |
module | Deklaration: Namensraum zugehöriger Deklarationen |
newtype | Erzeugen eines neuen Datentyps mit eigenem Konstruktor |
proc | Verallgemeinerung von Monaden zu Pfeilen (en arrows) |
rec (-XDoRec ) | Erlauben von rekursiven Bindungen in do-Blöcken |
type | Deklaration: Typalias (Typsynonym) |
type family | GHC-Erweiterung; Deklaration: Typsynonymenfamilie |
type instance | —: Instanz einer Typsynonymenfamilie |
where | Einführung: Modul, Instanz, Typklasse, GADT |
+
, -
, *
, /
^
, ^^
, **
==
, /=
, <
, <=
>
, >=
Not
, &&
, ||
Das negative Vorzeichen ist als Funktion umgesetzt und bedarf als Teilausdruck einer Klammerung:
xxxxxxxxxx
-x == - x
F (-x) -- x als Argument, Klammerung notwendig!
Für die Division von Ganzzahlen bestehen mehrere Funktionen:
div
→ Abrunden gegen –∞
quot
→ Abrunden Richtung Null
Obwohl der Divisionsoperator /
für Ganzzahlen nicht definiert ist, lassen sich dennoch zwei Ganzzahlen teilen, wobei Haskell diese automatisch zu Gleitkommazahlen umwandelt:
xxxxxxxxxx
Prelude> 2 / 3
0.6666666666666666
xxxxxxxxxx
putChar :: Char -> IO () -- Ausgabe eines einzelnen Zeichens
putStr :: String -> IO () -- Ausgabe einer Zeichenkette
putStrLn :: String -> IO () -- Ausgabe einer Zeichenkette mit Zeilenumbruch
print :: (Show a) => a -> IO () -- Anwenden von »show« auf die Eingabe
xxxxxxxxxx
getChar :: IO Char -- Einlesen eines Einzelzeichens (Charakters)
getLine :: IO String -- Einlesen einer Zeichenkette
xxxxxxxxxx
type FilePath = String
readFile :: FilePath -> IO String -- Dateiinhalt einlesen → Zeichenkette
writeFile :: FilePath -> String -> IO () -- Zeichenkette in eine Datei schreiben
appendFile :: FilePath -> String -> IO () -- Zeichenkette einer Datei anfügen
xxxxxxxxxx
data Maybe a = Nothing | Just a
Überladen und Autoumwandlungen (en coercion
)