Die Weltkarte basiert auf NASAs Blue Marble Bildern.
Das Foto vom Arts Gebäude stammt aus dem Archiv der University of Saskatchewan.
zurück |
Sämtliche Bezeichner in Haskell sind case sensitiv. So sind etwa add
, Add
und aDd
drei unterschiedliche Namen. Die Bezeichner für Variablen und Typvariablen (siehe
unten) müssen mit einem Kleinbuchstaben oder einem Unterstrich beginnen, z.B.
add, getChar, _y, [a].
Alle anderen Bezeichner beginnen mit einem Großbuchstaben. Hierbei kann es sich
um Konstruktoren, Typkonstruktoren, Typklassen und Modulnamen handeln (wobei
auf letztere im folgenden nicht eingegangen wird), z.B.:
Integer, [Char], Eq, True.Während Bezeichner stets mit einem Buchstaben oder Unterstrich beginnen und Buchstaben, den Unterstrich, Ziffern und das Anführungszeichen
´
enthalten dürfen,
sind Operatoren aus Symbolen zusammengesetzt, z.B.:
: . -> @ == >>=Auch sie können prinzipiell neu definiert werden. Zu beachten ist hierbei, dass Konstruktoren immer mit einem Doppelpunkt beginnen. Der einfache Doppelpunkt ist als Listenkonstruktor reserviert.
Da Haskell eine funktionale Sprache ist, bestehen Programme im wesentlichen aus einer Aneinanderreihung von Deklarationen. Typsignatur-Deklarationen geben an, welchen Typ eine Variable oder ein Ausdruck haben soll. Sie bestehen aus dem zu erklärenden Ausdruck, gefolgt von einem doppelten Doppelpunkt und dem Typ. Im folgenden Beispiel sind die Typdeklarationen für eine Integer-Konstante, ein Paar und eine einstellige Funktion gegeben.
5 :: Integer (´x´,True) :: (Char,Bool) inc :: Integer -> IntegerIn der Regel ist es nicht nötig, derartige Typdeklarationen in einem Programm anzugeben, da zu jedem Ausdruck der Typ automatisch abgeleitet werden kann. Sie erhöhen aber unter Umständen die Lesbarkeit und werden, wenn vorhanden, auch vom System überprüft.
Die zweite wichtige Form von Deklarationen sind Gleichungen. Sie bilden das eigentliche Programm. Hier ist eine mögliche Definition der Funktion inc, die ihren Parameter inkrementiert, angegeben.
inc n = n+1
Die Schreibweise von Funktionen in Haskell unterscheidet sich wesentlich von der,
die in der Mathematik oder gängigen imperativen Programmiersprachen verwendet
wird. Während man etwa für die zweistellige Addition add(x,y)
zu schreiben
gewohnt ist, was add
im Prinzip als eine Abbildung auf dem Paar (x,y)
ausweist,
formuliert man in Haskell add x y
. Dies wird als (add x) y
interpretiert, wobei
(add x)
noch keine Zahl ist, sondern wiederum eine einstellige Funktion, die zu
ihrem Argument x
addiert. Die Anwendung von (add x)
auf den Parameter y
ergibt
dann erst das gewünschte Ergebnis So betrachtet könnte man sagen, dass es in
Haskell nur einstellige Funktionen gibt. Dafür ist es zulässig, dass eine Funktion
wiederum eine andere Funktion als Ergebnis liefert. Dies drückt sich auch in der
Typnotation für "mehrstellige" Funktionen aus, wie die folgenden Beispiele zeigen.
add :: Integer->Integer->Integer openFile :: FilePath->IOMode->Handle
add
ist also eine Funktion von einem Integer-Parameter auf eine weitere Funktion
Integer->Integer
. Auch das Beispiel openFile
, eine Funktion die bei Angabe
eines Pfades und Zugriffsmodus ein Dateihandle liefert, verdeutlicht diesen
Sachverhalt.
Um ein hohes Maß an Flexibilität zu gewährleisten, ist es wünschenswert, dass sich auch Operatoren wie Funktionen definieren lassen. Diese Anforderung wird von vielen Programmiersprachen erfüllt. Eine Besonderheit von Haskell ist es, dass sich auch neue Operatoren aus den entsprechenden Sonderzeichen zusammensetzen lassen und dass Funktionen und Operatoren vollkommen austauschbar sind. Außer dem unären Minus gibt es in Haskell nur Infix-Operatoren, d.h. Operatoren sind in der Regel als alternative Schreibweise für zweistellige Funktionen aufzufassen. Wird ein Operator in runde Klammern eingeschlossen, kann er syntaktisch exakt wie ein Funktionsbezeichner verwendet werden, wie die folgenden Beispiele zeigen.
(==) :: Integer->Integer->Bool add = (+) inc = (+1)Umgekehrt kann man aber auch Funktionsbezeichner als Infixe einsetzen, wenn man sie in umgekehrte einfache Anführungszeichen setzt:
x `add` y = x+y y `elem` ysDies bietet sich vor allem dann an, wenn es der Lesbarkeit des Programms dient, wie im zweiten dieser Beispiele, wo
`elem`
als Operator "ist Element von" die
Verwandtschaft zum mathematischen ∈ Symbol suggerieren soll.
Wie auch in C gibt es in Haskell zwei Arten von Kommentaren. Ein doppeltes Minuszeichen weist den Rest der aktuellen Zeile als Kommentar aus. An beliebiger Stelle können (auch mehrzeilige) Kommentare eingefügt werden, die in geschwungene Klammern und Minuszeichen eingeschlossen sind.
add x y -- eine Addition add x {- Kommentar -} yIn C werden einzelne Anweisungen, Deklarationen, etc. durch Semikolon getrennt, Blöcke werden durch geschwungene Klammern eingegrenzt. Auf ganz ähnliche Weise können Haskell-Programme strukturiert werden.
let { y = a*b ; f x = (x+y)/y } in f c + f dOhne die Semantik der in diesem Beispiel verwendeten
let
-Struktur erläutern zu
wollen, ist klar, dass mit {
und }
ein Block beschrieben wird, innerhalb dessen
Einzeldefinitionen durch ;
getrennt werden. In Haskell ist es nicht üblich, die Struktur
auf diese Weise explizit zu machen. Statt dessen verwendet man eine als Layout
bezeichnete Formatierungstechnik, die hierzu äquivalent ist. Dabei werden Blöcke
durch gleiche Einrückungstiefe kenntlich gemacht. Einzelne Deklarationen werden
durch Zeilenumbrüche getrennt.
let y = a*b f x = (x+y)/y in f c + f dDennoch kann die Verwendung der expliziten Formatierung u.U. sinnvoll sein, z.B. wenn man mehrere Deklarationen in einer Zeile unterbringen möchte.
Haskell ist anders als z.B. PERL eine typisierte Sprache. Tatsächlich ist es bei der Durchsetzung einer exakten Typübereinstimmung sogar wesentlich strenger als die meisten imperativen Sprachen. So ist etwa automatisches Type casting nicht möglich. Natürlich bietet Haskell die aus anderen Programmiersprachen bekannte Palette an Standardtypen, wie z.B.:
Integer, Rational, Char, BoolDabei ist darauf hinzuweisen, dass etwa Integer ein idealisierter Ganzzahltyp ist, der prinzipiell keine Größenbeschränkung kennt.
Mit Tupeln wird ein strukturierter Typ zur Verfügung gestellt, der in etwa den record
Strukturen in Pascal entspricht. Er ermöglicht es, Variablen (ggf. unterschiedlichen
Typs) zusammenzufassen. Tupel werden in Runde Klammern eingeschlossen.
(3,´x´) :: (Integer,Char) (1,2,1) :: (Integer,Integer,Integer)Wie auch in anderen funktionalen Sprachen spielen Listen in Haskell eine zentrale Rolle ein. Listen können eine Beliebige Anzahl von Elementen desselben Typs enthalten. Hier sind beispielhaft eine Liste von Integer und eine Liste von Listen von Char angegeben:
[1,2,3] :: [Integer] [[´a´],[´b´,´c´],[]] :: [[Char]]
[]
steht für die leere Liste. Eine Liste kann durch Verkettung von Elementen erzeugt
werden, wobei die Kette durch die leere Liste abgeschlossen und der Doppelpunkt
als Konstruktor verwendet wird.
[1,2,3] entspricht also 1:(2:(3:[]))Da der Doppelpunkt rechtsassoziativ ist, können die Klammern weggelassen werden. Hier noch drei häufig gebrauchte Funktionen auf Listen:
length [] = 0 length (y:ys) = 1+length ys head (y:ys) = y tail (y:ys) = ys
length
liefert die Länge einer Liste, head
gibt das erste Element zurück und tail
alles, was auf das erste Element folgt (wenn die Liste mindestens ein Element
enthält).
Um einen Eindruck von den vielfältigen Möglichkeiten zu geben, Listen zu konstruieren, sind hier einige Beispiele für List comprehensions und Folgen gegeben:
[ inc x | x <- [1,2,3] ] ⇒ [2,3,4] [ (y,z) | y <- ys, z <- zs, y<z ] [1..10], [1,3..5], [1,3..]Die Liste in der ersten Zeile wird erzeugt, indem der Ausdruck vor dem senkrechten Strich ("Wächter") für alle Elemente aus der angegebenen Liste
[1,2,3]
ausgewertet wird. Der Pfeil ⇒ steht metasprachlich für "ergibt sich zu". Die zweite
Zeile zeigt denselben Mechanismus anhand eines komplexeren Beispiels. In einer
imperativen Sprache würde man das mit dem Listenverknüpfungsoperator ++
in etwa
formulieren:
result=[]; for y in ys for z in zs if y<z then result=result++[(y,z)];Die dritte Zeile obiger Beispiele ist im wesentlichen selbsterklärend, erwähnenswert ist aber die Möglichkeit, mit
[1,3..]
eine unendliche Liste zu definieren. Dass
diese nicht unmittelbar nach der Definition erzeugt werden kann, ist klar, der Sinn
einer solchen Definition ist im Zusammenhang mit verzögerter Auswertung zu sehen.
Schließlich muss noch festgestellt werden, dass auch Funktionen Typen wie alle anderen sind. Insbesondere können Funktionen selbst Funktionen als Parameter nehmen und wiederum Funktionen als Ergebnis liefern. Das bereits bekannte Beispiel einer "Variable vom Typ Funktion" ist:
add :: Integer->Integer->Integer
Es kommt bisweilen vor, dass die Angabe eines genauen Typs für eine Variable oder
eine Ausdruck nicht möglich oder wünschenswert ist. Ein Beispiel hierfür ist die oben
definierte Funktion length
, die zu einer Liste deren Länge liefert. Gemäß der
Definition funktioniert length
mit Listen beliebiger Elemente. Um dies zu
verdeutlichen, benutzt man bei der Typsignatur-Deklaration anstatt eines konkreten
Listentyps wie [Integer]
oder [Char]
einen allgemeineren, "polymorphen" Typ
[a]
.
length :: [a] -> IntegerDabei ist die Typvariable
a
als universal quantifiziert zu interpretieren, d.h. obige
Deklaration ist gültig für alle Typen a. Diese Typsignatur-Deklaration von length
gibt einen "passenden" Typ an, doch gibt es auch allgemeinere polymorphe Typen,
denen die Definition von length
ebenfalls gerecht wird. Beispiele hierfür sind etwa
[a] -> b, a -> Integer, a -> b, aDavon ist der letzte Typ,
a
, der allgemeinste. Jede denkbare Variable wird durch den
polymorphen Typ a
abgedeckt.
Umgekehrt gibt es polymorphe Typen, denen length
nicht entspricht, z.B.
[a] -> aDer "wahre" Typ einer Variable oder eines Ausdrucks ist der speziellste, dem die Definition gerecht wird. Im obigen Beispiel ist dies eben genau
[a] -> Integer
.
Dieser "wahre" Typ ist eindeutig bestimmt und wird von Haskells Typsystem
automatisch ermittelt.
Selbstverständlich ist es möglich, eigene Typen, auch polymorphe, zu definieren. Vordefinierte Typen haben gegenüber diesen keine hervorgehobene Stellung. In diesem Unterabschnitt wird die Vorgehensweise zunächst an zwei einfachen Beispielen demonstriert, dann folgt mit der Struktur tree ein komplexeres Beispiel für eine rekursive Definition.
Typdefinitionen werden durch das Schlüsselwort data eingeleitet. Darauf folgt dann mit dem Typkonstruktor der "Name" des zu definierenden Typs. Rechts vom Gleichheitszeichen wird der (oder die) Datenkonstruktor(en) angegeben.
data Bool = False | TrueIn diesem Beispiel wird der nicht polymorphe Typ
Bool
definiert. Der Typkonstruktor
Bool
kann im folgenden überall dort auftauchen, wo ein Typ verlangt ist, also
insbesondere in einer Typdeklaration rechts von ::
. Die beiden Datenkonstruktoren
False
und True
hingegen kann man als Konstanten vom Typ Bool
auffassen.
Bool
ist ein einfacher enumerativer Typ, der nur zwei Werte kennt.
Bereits etwas komplizierter ist der polymorphe Typ Point
.
data Point a = Pt a aEin
Point
vom Typ a
wird also konstruiert mit dem Datenkonstruktor Pt
gefolgt von
zwei Koordinaten vom Typ a
. Ein konkreten Beispiel hierfür ist:
Pt 3 5 :: Point IntegerDa Typkonstruktoren und Datenkonstruktoren niemals an derselben Stelle verwendet werden können, besteht keine Verwechslungsgefahr. Daher ist es möglich, für beide denselben Bezeichner zu wählen. Dies kann die Lesbarkeit eines Programms verbessern. Eine alternative Typdefinition für
Point
wäre demnach:
data Point a = Point a aAls Beispiel für eine komplexere Typdefinition soll hier noch eine polymorphe Baumstruktur konstruiert werden.
data Tree a = Leaf a | Branch (Tree a) (Tree a)Zunächst fällt auf, dass die beiden Datenkonstruktoren
Leaf
und Branch
anders als
in den bisherigen Beispielen unterschiedliche Parameter erwarten. Ein Blatt Leaf
erzeugt aus einem Argument vom Typ a
einen Baum, ein Branch
vereinigt zwei
Teilbäume. Die Typen der Konstruktoren sind also:
Branch :: Tree a -> Tree a -> Tree a Leaf :: a -> Tree aUm zu zeigen, wie man mit solch einem Typ arbeiten kann, sei hier noch die Definition einer Funktion
fringe
angegeben, die die Inhalte der Blätter eines
solchen Baumes der Reihe nach in einer Liste ablegt.
fringe :: Tree a -> [a] fringe Leaf x = [x] fringe (Branch l r) = fringe l ++ fringe rWie bereits erwähnt ist dient
++
der Verknüpfung zweier Listen.
Neben der bisher vorgestellten Form von Polymorphismus, dem "parametrischen
Polymorphismus", gibt es mit "ad-hoc Polymorphismus" oder überladen noch eine
weitere wichtige Form, die in vielen Programmiersprachen (oft implizit) realisiert ist.
Parametrischer Polymorphismus ist lässt sich dort einsetzen, wo der konkrete Typ
wirklich keine Rolle bei Berechnungen spielt - man denke etwa an die obige
Definition der Funktion fringe
.
Mit dem Konzept der Typklassen, das hier nur kurz angerissen werden kann, stellt
Haskell eine Möglichkeit bereit, kontrolliertes überladen zu nutzen. Dies soll am
Beispiel des Gleichheits-Operators ==
verdeutlicht werden.
Angenommen, es soll eine Funktion elem definiert werden, die für einen Wert eines beliebigen Typs überprüft, ob er in einer Liste (desselben Typs) vorhanden ist. Die Definition einer solchen Funktion (hier in der Infix-Schreibweise, siehe oben) könnte so aussehen.
x `elem` [] = False x `elem` (y:ys) = x==y || (x `elem` ys)
||
ist das logische oder. Hierbei tritt das Problem auf, dass die Gleichheit ==
, die in
der zweiten Zeile der Definition verwendet wird, nicht zwangsläufig auf jedem Typ
definiert ist. elem
soll demnach nur auf Typen verfügbar sein, für die ==
mit der
gewünschten Semantik (also insbesondere als Operator mit Bool
-Funktionswert)
definiert ist. Solche Typen werden in einer sogenannten Typenklasse
zusammengefasst. Diese gibt i.w. an, welche Operationen und Funktionen für einen
Typ dieser Klasse wie definiert sein müssen.
class Eq a where (==) :: a -> a -> BoolAlle Typen, die der Klasse
Eq
angehören, müssen also (==)
in der beschriebenen
Weise definieren (tatsächlich gehört die Klasse Eq
zum Standardumfang von
Haskell, ihre Definition ist aber etwas umfangreicher). Man sagt, (==)
sei eine
Methode von Eq
. Nun kann man eine Typsignatur-Deklaration für elem
angeben, die
klar macht, dass die Definition von elem
auf Typen der Klasse Eq
beschränkt ist.
elem :: (Eq a) => a -> [a] -> BoolUm einen konkreten Typ (hier
Integer
) als zur Klasse Eq
gehörig, anders
ausgedrückt als eine Instanz von Eq
, zu definieren, dient folgende Schreibweise:
instance Eq Integer where x==y = x `integerEq` yDies setzt natürlich die Definition einer geeigneten Funktion
integerEq
voraus.
Lambda Funktionen dienen dazu, Funktionen "anonym" definieren zu können, d.h. ohne ihnen einen Namen zuzuweisen. Die Schreibweise von Lambda Funktionen soll zunächst am Beispiel der beiden Funktionen
inc x = x+1 add x y = x+ygezeigt werden. Sie lassen sich alternativ definieren, indem man ihrem Namen die entsprechende Funktionsdefinition in Form eines Lambda- Ausdrucks zuweist.
inc = \x -> x+1 add = \x -> \y -> x+yVereinfacht gesprochen ist eine Lambda Funktion durch einen Backslash
\
und eine
Zuordnungsvorschrift gegeben, also im Fall inc
durch \x -> x+1
. Falls es sich,
wie bei add
, um eine Funktion mehrerer Parameter handelt, kann man auf eine
vereinfachte Schreibweise zurückgreifen.
add = \x y -> x+yMit Hilfe dieser Lambda-Schreibweise lässt sich auch die schrittweise Auswertung currifizierter Funktionen mehrerer Parameter, die oben bereits erläutert wurde, gut verdeutlichen.
add 3 4 ⇒ (\x -> 3+x) 4 ⇒ 3+4Doch wozu sind solche Lambda-Funktionen nütze? Zunächst ist ihre Existenz Ausdruck der Tatsache, dass Funktionen ebenso Typen wie
Integer
oder Listen sind.
Denn genauso, wie man z.B. die Integer
-Konstante 3
an beliebiger Stelle im
Programm verwenden kann, ohne vorher meinInt=3
zu definieren, kann man so die
Funktion \x->x+1
nutzen, ohne ihr vorher einen Namen zugewiesen zu haben.
Dass dies nützlich sein kann, soll am Beispiel der Funktion map
gezeigt werden, die
eine Funktion und eine Liste als Parameter nimmt und die Funktionswerte aller
Listenelemente - ebenfalls in einer Liste - zurückliefert.
map :: (a->b) -> [a] -> [b]So kann man
map
z.B. auf die Funktion inc
und eine Integer
-Liste anwenden.
map inc [1,2,3] ⇒ [2,3,4]In diesem Fall ist die Funktion
inc
bereits vordefiniert. Im allgemeinen muss dies
aber nicht der Fall sein, und man wird möglicherweise nicht extra eine neue Funktion
definieren wollen, nur um mit map
eine Liste zu erzeugen.
map (\x -> x+1) [1,2,3] ⇒ [2,3,4]
zurück | Inhalt | weiter |