Einführung in die Programmierung mit Haskell

Werbung
Manuel M. T. Chakravarty
Gabriele C. Keller
Einführung in die
Programmierung mit Haskell
ein Imprint von Pearson Education
München • Boston • San Francisco • Harlow, England
Don Mills, Ontario • Sydney • Mexico City
Madrid • Amsterdam
Kapitel
3
Grundlegende Kontrollstrukturen und Typen
»Man findet stets, dass sich ein funktionierendes komplexes System aus einem funktionierenden einfachen System entwickelt hat.«
Anonym
Die Funktionen, die wir bislang definiert haben, waren auf Grundoperationen beschränkt,
z. B. die Inkrementierung einer gegebenen Zahl. In diesem Kapitel werden etwas kompliziertere Funktionen besprochen und grundlegende Listenoperationen kurz dargestellt.
3.1
Programme werden aus Modulen
gebildet
Für gewöhnlich besteht ein Programm aus einer Vielzahl von Funktions- und Typdefinitionen. Offensichtlich ist es nicht ratsam, all dies in einer einzigen Datei zu speichern.
Moderne Programmiersprachen bieten daher Möglichkeiten zur Strukturierung von Programmen, indem sie zulassen, dass verwandte Definitionen in logischen Einheiten
zusammengefasst und in getrennten Dateien gespeichert werden. In Haskell werden diese
Einheiten Module genannt.
Sehen wir uns die folgende Definition eines Haskell-Moduls namens Simple an:
-- Beispielmodul
-- Manuel M. T. Chakravarty, Juli 2000
--- Dies ist ein einfaches Beispiel für eine Moduldefinition
module Sample
where
-- Quadratwurzel einer gegebenen Zahl berechnen
square :: Int -> Int
square x = x * x
-- Überprüfen, ob alle drei gegebenen Zahlen gleich sind
-threeEqual
:: Int -> Int -> Int -> Bool
threeEqual a b c = (a == b) && (b == c)
26
Kapitel 3 – Grundlegende Kontrollstrukturen und Typen
Das Modul beginnt mit dem Kopf, d. h. einem Kommentar, der eine einzeilige Beschreibung des Moduls, den Autor und das Erstellungsdatum enthält und den Zweck des Moduls kurz beschreibt.
Die erste Quelltextzeile beginnt mit dem Schlüsselwort module, dem der Name des Moduls, das Schlüsselwort where und die zum Modul gehörigen Definitionen folgen. Beachten Sie, dass Modulnamen im Gegensatz zu Funktions- oder Variablennamen mit einem
Großbuchstaben beginnen müssen.
In Haskell gibt es ein spezielles Modul namens Prelude.hs, das beim Start von GHCi automatisch geladen wird. Das Modul Prelude enthält alle Funktionen, die in Haskell vordefiniert sind, z. B. +, length usw.
Da wir mit einfachen, kurzen Programmen beginnen, fügen wir fürs Erste alle Funktionsdefinitionen eines Programms in ein einziges Modul ein. Später erklären wir, wie man
komplexere Programme mithilfe von Modulen strukturieren kann. Dies ist ein zentrales
Thema in der Softwareentwicklung, und Module spielen bei der Strukturierung umfangreicher Softwaresysteme eine wichtige Rolle. Wir kommen auf dieses Thema später zurück.
3.2
Verzweigungen im Programmablauf
Bislang haben alle unsere Programme bedingungslos dieselben Berechnungen durchgeführt.
3.2.1
Wahlmöglichkeiten werden durch
bedingte Anweisungen umgesetzt
Wie können wir eine Funktion mit der Definition
max :: Int -> Int -> Int
implementieren, die das größere ihrer beiden Argumente zurückgibt; d. h. sowohl für den
Ausdruck max 5 2 als auch für den Ausdruck max 2 5 soll das Ergebnis 5 berechnet werden, für max 1 7 das Ergebnis 7 etc.? Für zwei zufällig gewählte Zahlen x und y soll
max x y das Ergebnis x liefern, wenn x >= y, andernfalls soll y zurückgegeben werden.
Dies kann in Haskell durch einen so genannten Bedingungsausdruck oder If-then-elseAusdruck repräsentiert werden:
if (Bedingung) then (Wert, wenn die Bedingung wahr ist) else (Wert, wenn
die Bedingung falsch ist)
3.2 Verzweigungen im Programmablauf
27
Nun können wir max folgendermaßen implementieren:
max
:: Int -> Int -> Int
max x y = if x >= y then x else y
Sehen wir uns jetzt die Berechnung von max 5 2 an
max 5 2 ⇒
if 5 >= 2 then 5 else 2 ⇒
if True then 5 else 2
⇒
5
Bedingte Anweisungen sind ein wichtiger Bestandteil der Programmierung, weil sie es
uns ermöglichen, abhängig von verschiedenen Eingabewerten unterschiedliche Berechnungen durchzuführen. Es folgt ein weiteres Beispiel:
signum :: Int -> Int
signum x = if x < 0 then -1 else if x == 0 then 0 else 1
3.2.2
Funktionsdefinitionen mit Seitenbedingungen
Das Verschachteln von bedingten Anweisungen führt, wie wir in der obigen Definition von
signum sehen, zu schlecht lesbaren Programmen. Daher ermöglichen manche Programiersprachen als alternative Syntax die Verwendung von Funktionsdefinitionen mit Seitenbedingungen.
signum :: Int -> Int
signum x | x < 0 = -1
| x == 0 = 0
| x > 0 = 1
signum 7 ⇒(1) 1
Hier sind drei Zwischenschritte zu berechnen:
7 < 0 ⇒
7 == 0 ⇒
7 > 0 ⇒
False
False
True
Schließlich können wir die Definition von max wie folgt mithilfe von Funktionen mit Seitenbedingungen neu formulieren:
max
:: Int -> Int -> Int
max x y | x >= y
= x
| otherwise = y
Die Wahl der Schreibweise ist dem persönlichen Geschmack des Programmierers überlassen.
28
Kapitel 3 – Grundlegende Kontrollstrukturen und Typen
3.3
Zuweisungen
Mit einer Zuweisung wird einem Wert ein Name zugewiesen. Nachfolgend kann über diesen Namen auf den Wert Bezug genommen werden. Beispielsweise können wir nach der
Zuweisung
pi = 3.141592653589793
einfach pi statt 3.141592653589793 angeben.
3.3.1
Zuordnung von Namen und Werten
Wir können den neu eingeführten Namen in einer anderen Funktionsdefinition verwenden:
pi :: Float
pi = 3.141592653589793
circleArea
:: Float -> Float
circleArea radius = pi * radius * radius
Gelegentlich müssen wir einen Namen definieren, der nur in innerhalb einer Funktion
verwendet wird. In diesem Fall sollten wir eine lokale Variablenzuweisung benutzen. Beispiel:
pi :: Float
pi = 3.141592653589793
circleArea'
:: Float -> Float
circleArea' diameter = pi * radius * radius
where
radius = diameter / 2.0
Diese Funktion wird folgendermaßen berechnet:
circleArea' 6.0 ⇒
⇒
⇒
⇒
⇒
⇒
pi * radius * radius where radius = 6.0 / 2.0
pi * radius * radius where radius =3.0
pi*3.0*3.0
pi * 9.0
3.141592653589793 * 9.0
28.2743
3.4 Tupel: Kombinationen verschiedener Datenelemente
3.4
29
Tupel: Kombinationen verschiedener
Datenelemente
Wir haben gesehen, dass einer Funktion mehrere Werte übergeben werden, aber noch
nicht, dass eine Funktion mehrere Werte zurückgeben kann. Dies lässt sich mithilfe von
Tupeln erreichen:
addMul :: Int -> Int -> (Int, Int)
addMul xy = (x + y, x * y)
Mit einem Tupel werden mehrere Komponenten (im obigen Beispiel zwei Werte vom Typ
Int, also ganze Zahlen) zu einem Wert zusammengefasst. Dieser zusammengesetzte Wert
kann wie eine Einheit bearbeitet und als Wert von einer Funktion zurückgegeben werden.
Die Bildung eines zusammengesetzten Werts ist jedoch nur die eine Seite der Medaille.
Wir brauchen auch eine Methode zum Zerlegen dieser Werte. Hierzu verwenden wir eine
Notation, die ein Gegenstück zur Notation der Tupelbildung darstellt:
fstFromIntPair
:: (Int, Int) -> Int
fstFromIntPair (x, y) = x
sndFromIntPair
:: (Int, Int) -> Int
sndFromIntPair (x, y) = y
Im Argument von fstFromlntPair nehmen wir nicht über eine Variable auf das zusammengesetzte Argument als Ganzes Bezug. Stattdessen zerlegen wir das Wertepaar in
seine Komponenten x und y. Wenn wir addMul und fstFromlntPair kombinieren, erhalten
wir:
fstFromIntPair (addMul 56) ⇒
⇒
⇒
3.4.1
fstFromIntPair (5 + 6, 5 * 6)
fstFromIntPair (11, 30)
11
Beispiel: Punkte
Tupel können nicht nur zur Rückgabe mehrerer Ergebniswerte eingesetzt werden, sondern auch zur Darstellung von Datenelementen, die sich nicht durch einen Wert eines
grundlegenden Datentyps repräsentiert lassen. Ein schönes Beispiel hierfür sind die
Punkte in einem zweidimensionalen kartesischen Koordinatensystem, die durch ein Paar
ganzzahliger Werte beschrieben werden können. Damit wir nicht jedes Mal, wenn wir
den Typ eines Punkts angeben, die wenig informative Angabe (Int, Int) wiederholen
müssen, können wir den neuen Typnamen Point einführen (dies ähnelt der Einführung
von Namen für häufig verwendete Werte, die weiter oben erläutert wurde):
type Point = (Int, Int)
30
Kapitel 3 – Grundlegende Kontrollstrukturen und Typen
Mit dieser Definition können wir einige einfache Operationen mit Punkten definieren:
-- Ursprung des Koordinatensystems
-origin :: Point
origin = (0, 0)
-- Einen gegebenen Punkt nach rechts verschieben
-moveRight
:: Point -> Int -> Point
moveRight (x, y) distance = (x + distance, y)
-- Einen gegebenen Punkt nach oben verschieben
-moveUp
:: Point -> Int -> Point
moveUp (x, y) distance = (x, y + distance)
3.4.2
Beispiel: Farbpunkte
Wenn wir die Punkte um das Merkmal Colour (Farbe) erweitern, tritt eine weitere wichtige Eigenschaft der Tupel zu Tage: Die Komponenten von Tupeln können unterschiedlichen Typs sein. Wenn wir die Farbwerte in Worten beschreiben, also jeweils durch eine
Zeichenfolge (String) repräsentieren, erhalten wir folgende Definition:
-- Farben werden durch Zeichenfolgen beschrieben
-type Colour = String
-- Neuer Name für den Typ, der Farbpunkte repräsentiert
-type ColourPoint = (Int, Int, Colour)
Diese Definition lässt folgende Operationen mit Farbpunkten zu:
-- Ursprung des Koordinatensystems in einer gegebenen Farbe
-origin
:: Colour -> ColourPoint
origin colour = (0, 0, colour)
-- Einen Farbpunkt vertikal und horizontal verschieben
-move :: ColourPoint -> Int -> Int -> ColourPoint
move (x, y, colour) xDistance yDistance =
(x + xDistance, y + yDistance, colour)
— Abstand zwischen zwei Farbpunkten berechnen
-distance :: ColourPoint -> ColourPoint -> Float
3.4 Tupel: Kombinationen verschiedener Datenelemente
31
distance (xl, yl, colourl) (x2, y2, colour2) =
sqrt (intToFloat (dx * dx + dy * dy))
where
dx = x2 – xl
dy = y2 - yl
Beachten Sie, dass wir in der letzten Definition eine where-Klausel verwenden, um die
Ausdrücke x2 - xl und y2 – yl nicht wiederholen zu müssen. Die Hilfsfunktion intTo
Float ist wie folgt definiert:
intToFloat
:: Int -> Float
intToFloat x = fromInteger (toInteger x)
Die beiden Funktionen fromInteger und toInteger sind in Prelude vordefiniert und erleichtern Umwandlungen zwischen verschiedenen Typen numerischer Werte.
3.4.3
Wichtige Symmetrien in Haskell
Wenn wir die Syntax der Werte und Typen von Tupel vergleichen, stellen wir fest, dass
sie einander entsprechen. Betrachten Sie folgendes Beispiel:
(10, 15, "green") :: (Int, Int, String)
Wenn wir die Werte 10, 15 und "green" durch ihre jeweiligen Typen Int, Int und String
ersetzen, erhalten wir den Typ des Tupels. Überdies gibt es eine Parallele zwischen der
Bildung eines Terms und der Zerlegung des Terms. Betrachten Sie folgendes Beispiel:
startPoint = (0, 0, "black")
colourOfPoint (x, y, colour) = colour
Wenn wir die Komponenten der Tupelkonstruktion (0, 0, "black") durch Variablennamen
(hier x, y, colour) ersetzen, erhalten wir das Muster, das zur Zerlegung des entsprechenden Tupels eingesetzt werden kann.
3.4.4
Einige Tupel mit speziellen Namen
In der folgenden Tabelle sind einige Tupeltypen und ihre Namen aufgeführt:
#
Ausdruck
Name
0
1
2
()
nicht definiert
(x1, x2)
Unit
nicht definiert
Paar
32
Kapitel 3 – Grundlegende Kontrollstrukturen und Typen
#
Ausdruck
Name
3
4
5
(x1, x2, x3)
(x1, x2, x3, x4)
(x1, x2, x3, x4, x5)
…
(x1, …, xn)
Tripel
Quadrupel
Quintupel
n
3.5
n-Tupel
Listen: Viele Werte desselben Typs
Tupel bieten die Möglichkeit, eine feste Anzahl von Werten unterschiedlichen Typs zu
kombinieren. In vielen Anwendungen muss man jedoch in der Lage sein, zusammengesetzte Typen zu bearbeiten, die eine unterschiedliche Anzahl von Elementen eines
einzigen Typs enthalten können. Zu diesem Zweck gibt es Listen.
oddNumbers
:: Int -> [Int]
oddNumbers maxNumber = [1, 3..maxNumber]
Die Anzahl der von dieser Funktion zurückgegebenen Werte hängt vom Argument ab, im
Gegensatz zur weiter vorn vorgestellten Funktion addMul, bei der nur der Rückgabewert,
aber nicht die Anzahl der Rückgabewerte von der Eingabe abhing. Betrachten Sie folgendes Beispiel:
oddNumbers 10
oddNumbers 15
⋅ [1, 3, 5, 7, 9]
⋅ [1, 3, 5, 7, 9, 11, 13, 15]
Der Unterschied zwischen Tupel und Listen wird deutlich, wenn man ihre Typdefinitionen vergleicht, beispielsweise
(1, 2, "green") :: (Int, Int, String)
und
[1, 2, 3, 4] :: [Int]
Die Anzahl der Komponenten wird in der Typdefinition eines Tupels explizit angegeben,
in der Typdefinition einer Liste dagegen nicht. Folglich können die Elemente von Tupeln
heterogen sein, während die Elemente von Listen homogen sein müssen.
3.5.1
Nützliche Listenfunktionen
„ Werte werden in eckige Klammern eingeschlossen:
[4, 2, 6, 7, 2]
["red", "green", "blue"]
[x, y, z]
3.5 Listen: Viele Werte desselben Typs
33
„ Wir können einer gegebenen Liste am Anfang ein weiteres Element hinzufügen:
"yellow" : ["red", "green", "blue"]
⇒ ["yellow", "red", "green", "blue"]
Mit dem Operator : können nur Elemente am Listenanfang hinzugefügt werden. Daher gilt:
["red", "green", "blue"] : "yellow"
⇒ Fehler!
„ Mit dem Operator ++ können zwei Listen zusammengefügt werden:
[4, 2, 3] ++ [3, 1, 2, 7]
⇒ [4, 2, 3, 3, 1, 2, 7]
„ Mit dem Operator !! können wir ein beliebiges Element ausgeben, das an einer bestimmten Position der Liste steht.
[0, 1, 2, 3] !! 2 ⇒
2
(Die Zählung des Listenindex beginnt bei 0!)
„ Mit den beiden Funktionen head und tail kann eine Liste in das erste Element bzw.
die übrigen Elemente unterteilt werden:
head [0, 1, 2, 3] ⇒
tail [0, 1, 2, 3] ⇒
0
[1, 2, 3]
„ Die Länge einer Liste ermitteln wir mit length:
length [0, 1, 2, 3]
⇒
4
„ Mit sum und product können wir die Elemente einer Liste addieren bzw. multiplizieren:
sum [0, 1, 2, 3]
product [1, 2, 3, 4]
⇒
⇒
6
24
Wie können wir "yellow" mithilfe dieser Funktionen am Ende der Liste ["red",
"green", "blue"] einfügen? Den Operator : können wir hierzu nicht verwenden. Wir
müssen ++ folgendermaßen benutzen:
["red", "green", "blue"] ++ ["yellow"]
Beachten Sie die eckigen Klammern um "yellow", mit denen eine Liste gebildet wird.
Wir packen "yellow" also in eine aus einem Element bestehende Liste, die wir dann an
die Liste ["red", "green", "blue"] anhängen.
34
Kapitel 3 – Grundlegende Kontrollstrukturen und Typen
3.5.2
Unterschiede zwischen Listen und Tupeln
Da Listen und Tupel häufig verwechselt werden, geben wir im Folgenden einen Überblick über die Unterschiede zwischen Listen und Tupeln. Tupel haben die folgenden Eigenschaften:
„ Feste Größe, d. h. feste Anzahl von Komponenten:
(1, 2) :: (Int, Int)und (l, 2, 3) :: (Int, Int, Int) sind unterschiedlichen
Typs.
„ Die Komponenten können unterschiedlichen Typs sein:
(5, "Hallo") ist völlig korrekt.
Im Gegensatz dazu haben Listen die folgenden Eigenschaften:
„ Variable Größe, d. h. die Anzahl von Komponenten kann variieren.
Daher sind [1, 2] :: [Int] und [1, 2, 3] :: [Int] Listen desselben Typs.
„ Die Komponenten müssen vom gleichen Typ sein.
[5, "Hello"] ist unzulässig!
3.5.3
Zeichenfolgen als Listen
In Haskell sind Zeichenfolgen, also Werte vom Typ String, eigentlich eine spezielle
Form von Listen. Der Typ String ist in Haskell folgendermaßen definiert:
type String = [Char]
Dies bedeutet insbesondere, dass mit Zeichenfolgen Listenoperationen durchgeführt werden können.
"Hello" !! 1
⇒ 'e'
In Haskell gelten "Hello" und ['H', 'e', 'l', 'l', 'o'] als identische Ausdrücke. Dies
ist sehr praktisch, weil es, wie wir später sehen werden, viele mächtige Listenbearbeitungsoperationen gibt und diese zur Bearbeitung von Zeichenfolgen sofort einsetzbar
sind.
3.6
Layout
Im Gegensatz zu vielen anderen Programmiersprachen berücksichtigt der Haskell-Interpreter oder -Compiler die Formatierung des Quelltexts. Mit anderen Worten, die Verwendung von Einrückungen und Zeilenumbrüchen ist mit einigen Einschränkungen verbunden. Auf diese Weise kann sich die Sprache einiger Konstruktionen entledigen, die in
anderen Sprachen eingeführt wurden, um Doppeldeutigkeiten der Eingabe auszuräumen.
3.6 Layout
35
Vergleichen Sie
foo x = a + b
where
a = 1 + x
b = 2
mit
foo x = a + b
where
a = 1 + x
b = 2
Beides sind zulässige Programme. Im ersten Programm ist b lokal innerhalb von foo definiert, während im zweiten Programm die Verwendung von b nicht auf foo beschränkt ist.
Als Beispiel für ein korrektes Layout wird hier die Funktion distance angeführt, die wir
weiter oben besprochen haben:
distance :: ColourPoint -> ColourPoint -> Float
distance (x1, y1, colour1) (x2, y2, colour2) =
sqrt (fromInt (dx * dx + dy * dy))
where
dx = x2 – x1
dy = y2 - y1
Es gibt drei Layoutregeln, die Sie befolgen müssen, um syntaktisch korrekte Programme
zu erhalten:
1. Der gesamte zu einer Funktionsdefinition gehörige Programmcode muss sich weiter
rechts als das erste Zeichen der betreffenden Definition befinden (also rechts vom ersten Zeichen des Funktionsnamens stehen). Im Fall der Funktion distance muss sich
der gesamte Quelltext rechts von der Spalte befinden, die den Buchstaben d des Funktionsnamens distance enthält.
2. Analog hierzu muss sich der gesamte Quelltext einer lokalen Definition in einer
where-Klausel rechts vom ersten Zeichen des Namens der definierten Variablen befinden.
3. Alle Definitionen innerhalb einer where-Klausel müssen bündig untereinander ausgerichtet werden, beispielsweise beginnen die Definitionen von dx und dy im obigen
Beispiel in derselben Spalte.
36
Kapitel 3 – Grundlegende Kontrollstrukturen und Typen
3.7 Übungen
1. Schreiben Sie eine Funktion sort2 :: Int -> Int -> (Int, Int), die zwei
Werte vom Typ Int als Argumente akzeptiert und diese als sortiertes Paar zurückgibt, so dass sort2 5 3 gleich (3, 5) ist. Wie definieren Sie diese Funktion mit einem Bedingungsausdruck, wie lösen Sie die Funktion unter Verwendung von Funktionen mit Seitenbedingungen?
2. Betrachten Sie die Funktion almostEqual :: (Int, Int) -> (Int, Int)
-> Bool, welche die Werte von zwei Wertepaaren des Typs Int miteinander
vergleicht. Sie liefert das Ergebnis True, wenn beide Paare dieselben Werte
enthalten, wobei deren Reihenfolge nicht von Belang ist. Beispielsweise gilt,
almostEqual (3,4) (4,3) ist True, aber almostEqual (3,4) (3,5) ist False.
Welche der folgenden Definitionen geben den korrekten Wert zurück? Welche
Definition halten Sie für guten Programmierstil? Warum? Fügen Sie den korrekten Definitionen Kommentare hinzu, damit diese verständlicher und leichter lesbar werden. ((&&) :: Bool -> Bool -> Bool entspricht dem logischen
'Und', (||) :: Bool -> Bool -> Bool entspricht dem logischen 'Oder', und
(==) prüft, ob zwei Werte gleich sind.)
almostEqual (xl, yl) (x2, y2)
| (xl == x2) && (yl == y2) = True
| (xl == y2) && (yl == x2) = True
| otherwise
= False
almostEqual (xl,
| (xl == x2) =
| (xl == y2) =
| otherwise =
yl) (x2, y2)
(yl == y2)
(yl == x2)
False
almostEqual pairl pair2 =
(pairl == pair2) || (swap pairl == pair2)
where swap (x,y) = (y,x)
almostEqual pairl pair2 =
(pairl == pair2) || (swap pairl == swap pair2)
where swap (x,y) = (y,x)
almostEqual (xl, yl) (x2, y2) =
if (xl == x2) then
then if (yl == y2)
then True
else False
else if (xl == y2)
then if (x2 == yl)
then True
else False
else False
Herunterladen