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