Info Anleitung & Hinweise

Dieses Dokument enthält Informationen zur benötigten Software für diesen Kurs, eine kurze Einführung in Haskell mit Code-Beispielen und Hinweise bezüglich der Abgabe von Arbeitsblättern.

Benötigte Software (GHC, Shrimp, csv-Viewer)

Zum Ausführen von Haskell-Code wird der Glasgow Haskell Compiler (kurz GHC) benötigt. Der minimal installer ist ausreichend. Falls Sie Haskell nicht über einen Packetmanager installieren möchten, können Sie alternativ auch die Binaries auf https://www.haskell.org/ghc direkt herunterladen (Download -> Current Stable Release). Fügen Sie in diesem Fall den Pfad, in dem die Programme ghc und ghci liegen, zur path-Umgebungsvariable hinzu.

Zusätzlich wird das Programm Shrimp (spc) benötigt, um bestimmte Aufgabe zu bearbeiten. Dieses ermöglicht es, auf einfach Weise Programme als Flussdiagramme zu beschreiben, welche dann in Haskell übersetzt werden. Um sich mit der Anwendung von Shrimp vertraut zu machen, folgen Sie dem Tutorial auf der Startseite der Dokumentation.

Beim Ausführen von Shrimp-Programmen (auch sp-Programme genannt) können Ausführungsspuren (engl. traces) generiert werden, welche im CSV-Format abgespeichert werden. Zum Betrachten solcher Dateien wird ein Tabellenkalkulationsprogramm wie z.B. LibreOffic Calc empfohlen.

Kurzeinführung in Haskell

In der Datei AB/Examples.hs sind einige Beispiele, welche Sie testen und modifizieren können. Öffnen Sie die Datei dazu mit dem Interpreter ghci. Im Interpreter können Ausdrücke wie z.B. 2*(3+4) oder couple "abc" "123" eingegeben werden. Mit dem Befehl :r können Sie die Datei neu laden, z.B. nachdem Sie diese verändert haben und mit :q beenden Sie den Interpreter. Mit dem Befehl :t können Sie den Typ einer Funktion ausgeben lassen, z.B. :t couple. Mit einem Ausrufezeichen können Sie Befehle der Kommandozeile ausführen, z.B. :!clear (bzw. :!cls auf Windows).

Funktionen und Ausdrücke

Funktionen werden durch Ausdrücke definiert. Der Kontrollfluss, welcher in imperativen Sprachen durch Kontrollstrukturen (if, for, while, etc.) gesteuert werden kann, muss in Haskell mittels Rekursion realisiert werden.

Ausdrücke werden in Präfix-Notation angegeben. Ein Aufruf f(x, g(y, z), myVar) würde in Haskell als (f x (g y z) myVar) geschrieben werden. Binäre Operatoren sind zweistellige Funktionen, dessen Namen nicht aus alphanumerischen Zeichen bestehen, wie z.B. +, -, *, &&, ||, ==. Diese werden in Infix-Notation verwendet: (((3 * (2 + x)) > y) && (fun z)). Die Klammern am Anfang und Ende eines Ausdrucks können weggelassen werden, z.B. wäre f x (g y z) myVar auch in Ordnung.

Funktions- und Variablennamen fangen immer mit einem Kleinbuchstaben an. Sie dürfen auch ein Apostroph ' enthalten, sofern es nicht an erster Stelle vorkommt. Der folgende Code definiert eine Funktion f2, welche drei Eingabeparameter x, y, y' hat und y zurück gibt, falls x ungerade ist, ansonsten y'.

f2 x y y' = (if' ((mod x 2) == 1) y y')
Die Funktion if' hat drei Eingabeparameter und gibt den zweiten zurück, falls der erste Parameter wahr ist, ansonsten den dritten.

Eine Konstante kann als eine Funktion gesehen werden, welche keine Eingabeparameter hat. Beispiele für verschiedene Literale:

pi = 3.14 
baseNo = 2 
str = "a string" -- type String
chr = 'x' -- type Char
flag_q2 = False -- type Bool 
list = [2,3,5,7] -- list of numbers
{-
 the following constant contains a 3x3 matrix
 with entries from 1 to 9
-}
matrix = 
  [
    [1,2,3],    
    [4,5,6],
    [7,8,9]
  ]
Ein Ausdruck kann über mehrere Zeilen gehen, wie im Beispiel von matrix. Jede Zeile des Ausdrucks muss aber mit mehr Leerzeichen eingerückt sein als das erste Zeichen des Funktionsnamens (in diesem Fall m). Es sollten keine Tabulatorzeichen verwendet werden. Das erste Beispiel könnte auch so geschrieben werden:
f2 x y y' = 
  (if' ((mod x 2) == 1) 
    y 
    y'
  )

Der Typ einer einstelligen Funktion, dessen Eingabeparameter vom Typ Int ist und dessen Rückgabewert vom Typ Bool, lautet Int -> Bool. Nehmen wir an, dass in f2 die Variable x vom Typ Int und die Variablen y, y' vom Typ String sind. Dann würde die Signatur der Funktion wie folgt aussehen:

f2 :: Int -> String -> String -> String
Die Typen der Eingabeparameter sind durch -> getrennt und der letzte Typ ist der Typ des Rückgabewerts. Der Operator -> ist rechtsassoziativ und der obige Typausdruck wird daher als Int -> (String -> (String -> String)) interpretiert. Das heißt, f2 kann als einstellige Funktion gesehen werden, welche einen Parameter vom Typ Int hat und dessen Rückgabewert vom Typ (String -> (String -> String)) ist; der Rückgabewert ist selbst eine Funktion. Die Interpretation einer mehrstelligen Funktion als eine Reihe von einstelligen Funktionen wird als Currying bezeichnet. Der Ausdruck (f2 1) gibt eine Funktion zurück, welcher der Funktion f2 entspricht, wobei der erste Eingabeparameter auf den Wert 1 festgelegt wird. Dies wird als partielle Applikation bezeichnet. Falls für eine Funktion keine Signatur angegeben wird, dann versucht der Compiler bzw. Interpreter eine möglichst allgemeinen Signatur selbst abzuleiten.

Die where-Klausel erlaubt es, mehrfach vorkommende oder lange Teilausdrücke bei der Definition einer Funktion separat zu definieren.

f3 x = z
  where
    y = x + x
    z = y * y 
Die Bezeichner y und z sind nur innerhalb der Definition von f3 gültig. Ist z.B. y bereits auf globaler Ebene definiert, dann wird die Definition aus der where-Klausel bevorzugt (shadowing). Mehrfach verschachtelte where-Klauseln sind möglich.

Datentypen und Pattern Matching

Mit dem Schlüsselwort type können Typaliase eingeführt werden, z.B. type Age = Int. Ein Alias wird identisch behandelt wie der Typausdruck auf der rechten Seite. Jedes Vorkommen von Age kann durch Int ersetzt werden und andersherum, ohne die Semantik des Programms zu beeinflussen.

Ein Datentyp in Haskell besteht aus einem Namen und mindestens einem Konstruktor. Ein Konstruktor beschreibt wie ein Datentyp repräsentiert wird. Ein Konstruktor besteht aus einem Namen und einer Reihe von Typausdrücken. Zum Beispiel kann eine Farbe mittels RGB oder HSV repräsentiert werden. In beiden Fällen werde drei Werte benötigt, welche als Float ausgedrückt werden können.

data Color = RGB Float Float Float | HSV Hue Saturation Value
type Hue = Float        -- [0°,359°] = [0.0,1.0]
type Saturation = Float -- [0%,100%] = [0.0,1.0]
type Value = Float      -- [0%,100%] = [0.0,1.0]

black  = RGB 0.0 0.0 0.0
black' = HSV 0.5 0.6 0.0
red    = RGB 1.0 0.0 0.0
red'   = HSV 0.0 1.0 1.0

Bezeichner von Datentypen und Konstruktoren beginnen stets mit einem Großbuchstaben. Dadurch können Sie von Variablen- und Funktionsbezeichnern unterschieden werden. Ein Konstruktor kann den gleichen Namen haben wie der Datentyp (üblich, wenn es nur einen Konstruktor gibt). Mit Typaliasen kann man dokumentieren, welche Art von Wert man an welcher Stelle des Konstruktors erwartet, wie im obigen Beispiel für HSV. Ein Konstruktor kann wie eine Funktion aufgerufen werden, um einen Wert zu definieren.

Mittels Pattern Matching kann auf die Werte eines Konstruktors zugegriffen werden. Ein Pattern ist ein Ausdruck, der auf der rechten Seite von = vorkommt und nur aus Konstruktoren und Variablen besteht. Ein Pattern beschreibt eine Menge von Werten, die eine gewisse Struktur aufweisen (z.B. Listen mit mindestens zwei Elementen) und kann Teile dieser Strukturen an Variablen binden (z.B. "sei y das zweite Element der Liste").

redVal (RGB x _ _) = x --redVal red == 1.0
swapRedBlue (RGB r g b) = (RGB b g r)
Die Funktion redVal gibt den Rot-Farbwert eines Werts vom Typ Color zurück, welcher mittels des Konstruktors RGB definiert wurde. Die Unterstriche stehen dafür, dass die Werte an diesen Stellen nicht benötigt werden. Der Ausdruck redVal red' würde zur Fehlermeldung Non-exhaustive patterns führen, da die Funktion nicht definiert ist für den Konstruktor HSV. Man könnte eine weitere Zeile redVal (HSV h s v) = ... hinzufügen, welche den Rotwert aus der HSV-Repräsentation berechnet.

Ein Konstruktor kann auch ganz allein ohne Typausdrücke vorkommen. Zum Beispiel ist der Datentyp Bool definiert als data Bool = True | False. Ein Datentyp der nur aus Konstruktoren ohne Typausdrücke besteht, kann als Enumeration gesehen werden. Entsprechend kann über Pattern Matching eine Fallunterscheidung vorgenommen werden, je nachdem ob eine Variable vom Typ Bool wahr oder falsch ist.

if' True  x y = x
if' False x y = y

Listen haben zwei spezielle Konstruktoren [] und :. Auf der rechten Seite vom = ist der Ausdruck (2:[3,4,5]) gleichbedeutend mit prepend 2 [3,4,5], was [2,3,4,5] ergibt. Auf der linken Seite als Pattern haben sie folgende Bedeutung:

listTest [x,y] = "list with exactly two elements"
listTest (x:(y:z)) = "list with at least two elements"
listTest z = "all other cases"
Beim Aufruf der Funktion listTest werden die Patterns der Reihe nach geprüft und die erste Definition gewählt, dessen Pattern passt. Zuerst wird also geprüft, ob die Liste genau zwei Elemente hat, danach, ob sie mindestens zwei hat und in allen anderen Fällen wird die dritte Definition gewählt. Würde man die erste und dritte Zeile vertauschen, würde die Funktion immer "all other cases" zurückgeben.

Type Classes

Eine Typklasse beschreibt eine Menge von Typen, für die bestimmte Funktionen definiert sind. Zum Beispiel können zwei Werte des Typs Int oder Float beide mit der Funktion < verglichen werden. Dies ist möglich, weil beide Typen Mitglieder der Typklasse Ord (von ordered) sind. Mitglieder dieser Typklasse müssen die Funktionen <, <=, >, >= definieren. Typklassen sind ähnlich zu Interfaces in anderen Programmiersprachen.

Mitglieder der Typklasse Eq (von equal) müssen die Funktionen == und /= definieren. Bis auf Funktionstypen (Typen der Form * -> *) sind alle Typen, welche in dem Kurs genutzt werden, Mitglieder von Eq und können daher auf Gleichheit geprüft werden.

Mitglieder der Typklasse Show können mittels der Funktion show in eine String umgewandelt werden. Mitglieder der Typklasse Read können mittels der Funktion read von einem String zurück umgewandelt werden. Der Rückgabetyp muss dabei angegeben werden, z.B. ( (read "[1,2,3]") :: [Int]).

Für selbst-definierte Datentypen gibt es mittels des Schlüsselwortes deriving die Möglichkeit, dass die Funktionen einer bestimmten Typklasse automatisch für diesen Typ definiert werden. Dadurch wird der selbst-definierte Typ zum Mitglied dieser Typklasse.

data Color = RGB Float Float Float | HSV Hue Saturation Value
  deriving (Eq,Show,Read)
Mit der obigen Definition ist es z.B. möglich einen Wert des Typs Color mittels show in einen String umzuwandeln und mit read wieder zurück umzuwandeln. Da Color auch Mitglied von Eq ist, können seine Werte auch auf Gleichheit geprüft werden. Zwei Werte gelten als gleich, wenn sie mit dem gleichen Konstruktor erzeugt wurden und die einzelnen Komponenten gleich sind (red == red' wäre False). Das Ableiten funktioniert nur, wenn alle Typausdrücke der Konstruktoren auch Mitglieder der Typklasse sind, für welche die Funktionen abgeleitet werden sollen.

Signaturen polymorpher Funktionen können mit Type Constraints ausgedrückt werden. Die Signatur einer Funktion, welche Listen mit Elementen beliebigen Typs sortiert, würde so aussehen:

mySort :: (Ord a) => [a] -> [a]
Ein Bezeichner in einem Typausdruck, welcher mit einem Kleinbuchstaben beginnt, bezeichnet eine Typvariable. Der Ausdruck [a] steht für eine Liste mit Elementen vom Typ a, wobei a ein beliebiger Typ sein kann. Da man nur Listen sortieren kann, bei der eine Ordnung für die Elemente der Liste gegeben ist, muss a ein Mitglied der Typklasse Ord sein. Dies wird durch das Type Constraint (Ord a) => ausgedrückt.

Weitere Informationen zu Typklassen gibt es auf Haskell/Classes and types auf Wikibooks.

Tracing / Debugging

Mit der Funktion trace können Strings zum Debuggen ausgegeben werden. Der erste Eingabeparameter ist der String, welcher ausgegeben werden soll und der zweite Eingabeparameter ist der Rückgabewert. Ein Wert beliebigen Typs kann mit der Funktion show in einen String umgewandelt werden, vorausgesetzt dieser Typ ist ein Mitglied der Typklasse Show.

In der Datei Test.hs sind ab dem Kommentar --Tracing examples Beispiele, wie man die Funktion konkret zum Tracen einer Funktion benutzen kann. Die Reihenfolge der Trace-Ausgabe hängt von der Reihenfolge ab, in der die Ausdrücke ausgewertet werden. Aufgrund der Evaluationsstrategie von Haskell (lazy evaluation) ist diese Reihenfolge nicht ganz offensichtlich. Für reverse2 "abc" stehen die Ausgaben new res: z.B. erst ganz am Ende.

Zum Testen Ihres Codes für die Arbeitsblätter können Sie die Datei AB/Testing.hs verwenden und entsprechende import-Befehle hinzufügen. Dadurch kann vermieden werden, die import-Befehle in den Vorlagen für die Lösungen zu modifizieren.

Module

Ein Modul entspricht einer hs-Datei. Der Modulname kann aus mehreren Teilen bestehen, welche durch . getrennt sind, z.B. MyProject.ModuleSet1.ModuleName. GHC würde dieses Modul standardmäßig in der Datei MyProject/ModuleSet1/ModuleName.hs erwarten, wobei dieser Pfad relativ zur hs-Datei ist, welche interpretiert bzw. kompiliert werden soll.

Ein Modul exportiert Funktionen, Typen, Konstruktoren und Typklassen. Wenn nichts spezifiziert ist, werden alle in diesem Modul definierten Entitäten exportiert. Alternativ kann man eine explizite Exportliste angeben, die benennt, welche Entitäten exportiert werden sollen. Alle Entitäten, welche nicht in dieser Liste enthalten sind, können nicht von außerhalb des Moduls benutzt werden (privat). Die module-Zeile sollte immer ganz am Anfang der hs-Datei stehen. Die zweite Zeile würde den Typen MyType mit Konstruktoren ConsA und ConsB und die Funktionen fun1 und fun2 exportieren.

module MyModules.XyZ where
module MyModules.XyZ (MyType(ConsA,ConsB), fun1, fun2) where

Zum Importieren von Modulen wird der Befehl import genutzt. Falls keine Importliste angegeben wird, werden alle Entitäten, welche das angegebene Modul exportiert, importiert. Die zweite Zeile würde nur den Datentypen MyType ohne Konstruktoren und die Funktion fun2 importieren.

import MyModules.XyZ
import MyModules.XyZ (MyType,fun2)
Falls aus mehr als einem Modul Entitäten mit dem gleichen Namen importiert werden, dann müssen diese qualifiziert referenziert werden, z.B. MyModules.XyZ.fun2. Um dies zu verkürzen, können Aliase für die Module vergeben werden, z.B. import MyModules.XyZ as Xy. Dann kann die Funktion mit Xy.fun2 referenziert werden. Weitere Optionen für das Importieren stehen im Haskell-Wiki.

Learn You a Haskell

Learn You a Haskell ist ein Tutorial, welches leicht verständlich die Grundlagen von Haskell erklärt. Falls die obige Einführung nicht ausreicht, können Sie sich zusätzlich folgende Teile des Tutorials anschauen:

Hinweise zu Abgaben

Für die Abgabe eines Arbeitsblatts ABi schicken Sie bitte alle dazugehörigen Dateien ABi_j.hs und alle Ordner ABi_j als zip-Archiv per E-Mail mit dem Betreff "Abgabe ABi" an sysprog@thi.uni-hannover.de. Die zip-Datei soll nur hs- und sp-Dateien enthalten! Falls es für eine Datei ABi_j.hs keinen gleichnamigen Ordner gibt, heißt dies, dass Sie auch keine sp-Programme verwenden dürfen.

Bevor die Dateien ABi_j.hs getestet werden, werden sie zuerst mit spc kompiliert (sollten keine sp-Programme enthalten sein, dann verändert sich nichts). Anschließend durchläuft Ihre Abgabe eine Reihe von Testfällen. Falls alle Tests erfolgreich sind, wird Ihnen dies mitgeteilt und es erfolgt anschließend eine mündliche Abnahme mit allen Gruppenmitgliedern per Videochat. Alle Gruppenmitglieder müssen alle Fragen bezüglich des Codes beantworten können. Nach erfolgreicher mündlicher Abnahme gilt ein Arbeitsblatt als bestanden.

Wenn beim Testen oder bei der Abnahme ein Fehler in der Abgabe festgestellt wird, dann gilt die Abgabe als Fehlversuch (siehe dazu die Datei regelung.pdf im StudIP). Falls alle Testfälle nicht innerhalb eines großzügigen Zeitlimits durchlaufen, gilt dies ebenfalls als Fehlversuch.