H

Haskell am WvS – Hilfe

Arbeitsumgebung, Syntax und Lernweg – kompakt an einem Ort

Hilfe 0.4.2 · Labor 0.1.13
Orientierung

Arbeiten im Haskell-Labor

JupyterLab ist die Arbeitsoberfläche im Browser. Haskell selbst läuft auf dem Schulserver. Für den Unterricht brauchen Sie vor allem den Dateibereich, Haskell / GHCi und Text File.

BrowserJupyterLabSchulserverGHC / GHCi
Hinweis: Diese Hilfe ersetzt kein Skript und keine Unterrichtsmaterialien. Sie bietet kurze Hilfestellungen, erinnert an wichtige Begriffe und zeigt kleine Beispiele. Ausführliche Definitionen, Herleitungen und Aufgaben bleiben Bestandteil des Unterrichts.
Diese Hilfe wieder öffnen: In JupyterLab finden Sie sie jederzeit über Help → Haskell-Hilfe. Die Hilfeseite öffnet sich in einem neuen Browser-Tab, sodass Anleitung und Arbeitsumgebung parallel geöffnet bleiben können.

1. Wo bin ich hier?

Nach dem Einloggen sehen Sie JupyterLab. Ihre Dateien liegen in Ihrem persönlichen Bereich auf dem Schulserver und bleiben dort gespeichert.

JupyterLab-Startseite mit Haskell-Konsole, Textdatei und Dateibereich
1. Dateibereich
Hier liegen Ihre Dateien, zum Beispiel Aufgaben1.hs.
2. Haskell / GHCi
Hier probieren Sie Ausdrücke aus und laden gespeicherte Haskell-Dateien.
3. Text File
Damit legen Sie eine neue Textdatei an und benennen sie anschließend mit .hs.
4. Tabs / Arbeitsbereich
Geöffnete Dateien und Konsolen erscheinen nebeneinander.
Für unsere Haskell-Arbeit benötigen Sie nur drei Dinge: den Dateibereich links, Haskell / GHCi und Text File. Python, Terminal und die übrigen Werkzeuge sind nicht erforderlich. Sie gehören zur standardmäßigen JupyterLab-Umgebung und werden deshalb trotzdem angezeigt.

2. Zugang und Passwort

Zum Haskell-Labor gehört ein eigener Zugang. Im aktuellen Laborstand 0.1.13 können Sie Ihr Haskell-Passwort selbst ändern, ohne dass eine Lehrkraft das Passwort neu setzen muss.

Eigenes Haskell-Passwort ändern

Der Passwortdienst gehört direkt zum Haskell-Labor. Er ist nur für Ihren eigenen Haskell-Zugang vorgesehen.

Passwort ändern

Passwortregeln: Die Regeln werden zentral für das Haskell-Labor festgelegt und beim Ändern automatisch geprüft. Falls ein neues Passwort nicht akzeptiert wird, beachten Sie die dort angezeigte Begründung und wählen Sie ein anderes Passwort. Die Hilfe nennt bewusst keine feste Mindestlänge, damit sie bei späteren Regeländerungen nicht veraltet.
Wichtig: Der Passwortdienst ändert nur Ihr Passwort für das Haskell-Labor. Er ist kein allgemeiner Passwortdienst für andere schulische Systeme.

3. Was läuft eigentlich wo?

JupyterHub verwaltet den Zugang zu Ihrer persönlichen Arbeitsumgebung. Der Haskell User Manager stellt die dafür benötigten Konten und den Passwort-Selbstservice bereit. JupyterLab ist die Oberfläche, die Sie im Browser sehen. GHC/GHCi läuft auf dem Schulserver.

Haskell lokal installiertHaskell am WvS
GHC / GHCiläuft auf dem eigenen Computerläuft auf dem Schulserver
Bedienungz. B. Terminal + EditorJupyterLab im Browser
Dateienliegen auf dem eigenen Computerliegen im persönlichen Bereich auf dem Server
Internetfür die eigentliche Arbeit nicht zwingend nötigfür den Zugriff auf den Schulserver nötig
Paketeselbst verwaltbardurch die Serverumgebung vorgegeben
Wichtig: Wenn Sie in JupyterLab eine Datei speichern, liegt sie auf dem Server – nicht „im Browser“. Beim nächsten Login sollte sie wieder im Dateibereich vorhanden sein.

Download und Upload sind etwas anderes: Beim Download wird zusätzlich eine Kopie auf Ihrem Gerät gespeichert. Wo diese landet, hängt von Browser und Geräteeinstellungen ab. Ein Upload kopiert eine lokale Datei wieder auf den Server.

4. Ihr erster Arbeitsablauf

Arbeiten Sie die fünf Schritte nacheinander durch. Die Hilfe speichert dabei keinen persönlichen Lernfortschritt.

GHCi öffnen und Haskell direkt ausprobieren.

Klicken Sie im Launcher auf Haskell / GHCi und probieren Sie:

2 + 3
7 * 6
"Hallo" ++ " Haskell"
Eine Haskell-Datei anlegen.

Öffnen Sie im Launcher Text File und geben Sie ein:

quadrat x = x * x

Benennen Sie die Datei im Dateibereich in start.hs um. Entscheidend ist die Endung .hs.

Datei speichern und in GHCi laden.
:load start.hs
quadrat 5

Als Ergebnis sollte 25 erscheinen.

Datei ändern und neu laden.

Ergänzen Sie in start.hs:

doppelt x = 2 * x

Speichern Sie die Datei zuerst. Wechseln Sie dann zu GHCi:

:reload
doppelt 12
Verstehen, wo die Datei gespeichert ist.

start.hs liegt jetzt in Ihrem persönlichen Bereich auf dem Schulserver. Beim nächsten Arbeiten öffnen Sie die Datei wieder links im Dateibereich. Ein Download ist nur nötig, wenn Sie zusätzlich eine Kopie auf Ihrem eigenen Gerät haben möchten.

Grundmuster: Bearbeiten → Speichern → :load bzw. :reload → Testen

5. Vier Dinge, die häufig schiefgehen

Änderung nicht sichtbar?
Datei speichern und danach in GHCi :reload ausführen.
Datei nicht gefunden?
Dateinamen und Endung .hs prüfen. Für den Einstieg sollten Datei und GHCi im gleichen Arbeitsverzeichnis liegen.
:load oder :reload?
:load lädt eine bestimmte Datei. :reload lädt die zuletzt geladene Datei erneut.
Browser geschlossen?
Eine zuvor in JupyterLab gespeicherte Server-Datei ist dadurch nicht automatisch gelöscht. Sie sollte beim nächsten Login wieder im Dateibereich vorhanden sein.
Ziel erreicht: Sie können sich in JupyterLab orientieren, Haskell direkt in GHCi testen, eine .hs-Datei erstellen, speichern, laden, ändern und erneut ausführen.

6. Wenn etwas nicht funktioniert

Wenn Haskell oder JupyterLab ungewöhnlich reagiert, gehen Sie schrittweise vor. Oft reicht bereits ein erneutes Laden der Haskell-Datei.

1Datei speichern
2:reload
3Hub Control Panel
4Server neu starten

Persönliche Arbeitsumgebung neu starten

Öffnen Sie in JupyterLab File → Hub Control Panel.

Ausschnitt des JupyterLab-Menüs File; Hub Control Panel ist markiert

Auf der folgenden JupyterHub-Seite können Sie Ihre persönliche Serverinstanz mit Stop My Server stoppen und anschließend über My Server wieder öffnen.

Ihre gespeicherten .hs-Dateien bleiben erhalten. Nicht gespeicherte Änderungen können dagegen verloren gehen. Der Neustart betrifft nur Ihre persönliche Arbeitsumgebung – nicht den zentralen JupyterHub und nicht Ihre Dateien auf dem Server. Der Passwortdienst ist davon getrennt.

7. Anonymes Feedback zur Haskell-Hilfe

Ihre kurze Rückmeldung hilft dabei, die Haskell-Arbeitsumgebung und diese Anleitung weiterzuentwickeln. Alle Angaben sind freiwillig. Bitte keine Namen oder andere persönliche Daten in die Freitextfelder schreiben.

Freitext

Von dieser Hilfeseite werden keine Haskell-Dateien oder Programminhalte mitgesendet.

Werkzeug

GHCi – ausprobieren, laden, prüfen

GHCi ist die interaktive Haskell-Umgebung. Sie eignet sich zum schnellen Testen von Ausdrücken und zum Ausführen der Funktionen aus Ihren .hs-Dateien.

Ausdrücke direkt testen

13 + 42
2 ^ 10
(+) 16 26
"Ha" ++ "skell"
7 > 4
True && False

GHCi wertet einen eingegebenen Ausdruck aus und zeigt das Ergebnis an.

Wichtige GHCi-Befehle

BefehlBedeutung
:load datei.hs oder :l datei.hsDatei laden
:reload oder :rzuletzt geladene Datei erneut laden
:type ausdruck oder :t ausdruckTyp eines Ausdrucks anzeigen
:quit oder :qGHCi beenden

Fehlermeldungen lesen

Fehlermeldungen sind Teil der Arbeit. Lesen Sie zuerst die genannte Datei und Zeile, danach die eigentliche Meldung. Prüfen Sie besonders Schreibweise, Klammern, Einrückung und Typen.

Nach einer Korrektur: speichern und anschließend :reload.
Arbeitsweise

Haskell-Dateien

Eine Haskell-Datei sammelt Funktionsdefinitionen. GHCi lädt die Datei; getestet werden die Funktionen anschließend am Prompt.

Einfacher Aufbau

square :: Int -> Int
square x = x ^ 2

doppelt :: Int -> Int
doppelt x = 2 * x

Typangabe und Funktionsdefinition stehen normalerweise direkt beieinander.

Kommentare

-- einzeiliger Kommentar
quadrat x = x * x  -- Kommentar am Zeilenende

Kommentare werden nicht ausgeführt. Nutzen Sie sie sparsam für Hinweise, die der Code selbst nicht schon deutlich macht.

Der Arbeitszyklus

Datei bearbeiten → speichern → :reload → Funktionsaufruf testen → bei Bedarf korrigieren
Grundlage

Funktionen, Pattern Matching und Guards

Haskell-Programme bestehen im Wesentlichen aus Funktionsdefinitionen. An logischen Funktionen lässt sich der Aufbau besonders gut untersuchen, weil die zugehörigen Wahrheitstabellen bereits bekannt sind.

bekannte LogikHaskell-FunktionPattern Matching / Guards

Eine Funktion definieren

add5 :: Int -> Int
add5 x = x + 5

bmi :: Float -> Float -> Float
bmi gewicht groesse = gewicht / (groesse * groesse)

Links vom Gleichheitszeichen stehen Funktionsname und Parameter. Rechts steht der Ausdruck, der das Ergebnis berechnet.

Pattern Matching – Fälle durch Muster unterscheiden

Beim Pattern Matching werden verschiedene Eingabemuster durch mehrere Definitionszeilen beschrieben. Die erste passende Zeile wird verwendet.

und :: Bool -> Bool -> Bool
und True  True  = True
und True  False = False
und False True  = False
und False False = False

Die vier Zeilen entsprechen direkt den vier möglichen Belegungen einer Wahrheitstabelle mit zwei Eingängen.

Ein anderes Datenmodell: 0 und 1

nand :: Int -> Int -> Int
nand 1 1 = 0
nand _ _ = 1

Der Unterstrich _ steht als eine Art Platzhalter für einen Wert. Dieser kann einen beliebigen Wert annehmen (typgerecht!); der konkrete Wert wird an dieser Stelle im Pattern Matching nicht weiter verwendet.

Guards – Fälle durch Bedingungen unterscheiden

Guards stehen hinter einem senkrechten Strich. Die Bedingungen werden von oben nach unten geprüft.

betrag :: Int -> Int
betrag x
  | x >= 0    = x
  | otherwise = -x

otherwise ist der Auffangfall, wenn keine vorherige Bedingung erfüllt wurde. Auch vor otherwise muss der Guard-Strich | stehen.

Faustregel: Pattern Matching passt gut zu klar unterscheidbaren Eingabeformen; Guards passen gut zu Bedingungen und Wertebereichen.

Funktionen miteinander verwenden

nicht3 :: Int -> Int
nicht3 0 = 1
nicht3 1 = 0

und3 a b = nand (nand a b) (nand a b)

Eine Funktion kann andere bereits definierte Funktionen aufrufen. So lassen sich komplexere Berechnungen aus kleinen Bausteinen zusammensetzen.

Kurz: lokale Hilfsdefinition mit where

where kann eine kleine Hilfsdefinition direkt an eine Funktion binden. Für unseren Lernweg ist das kein eigener Schwerpunkt.

summeHoch3 x y = summe * summe * summe  -- berechnet (x + y)^3
  where
    summe = x + y
Grundlage

Typen lesen und angeben

Typen helfen dabei zu erkennen, welche Eingaben eine Funktion erwartet und welche Art von Ergebnis sie liefert.

Häufige Typen

TypBeispielBedeutung
Int42ganze Zahl in festem Wertebereich
Integer123456789012345beliebig große ganze Zahl
Float3.14Gleitkommazahl
BoolTrue, FalseWahrheitswert
Char'a'ein Zeichen
String"Hallo"Zeichenkette

Funktions-Typen lesen

square :: Int -> Int
max2   :: Int -> Int -> Int
nand   :: Int -> Int -> Int

Bei max2 und nand stehen zwei Eingabetypen vor dem Ergebnistyp. Der Ergebnistyp steht ganz rechts. Für den Einstieg genügt diese Lesart: zwei Eingaben hinein, ein Ergebnis heraus.

Typen in GHCi untersuchen

:t True
:t "Hallo"
:t [1,2,3]
:t (2,'a')

Mit :t können Sie Vermutungen über Typen direkt überprüfen.

Datenstrukturen

Tupel und Listen

Tupel und Listen sehen ähnlich aus, haben aber unterschiedliche Aufgaben. Tupel dürfen verschiedene Typen zusammenfassen; Listen enthalten Elemente gleichen Typs.

Tupel

(2,3)
('a',2)
("Ada", 1815, True)

Tupel stehen in runden Klammern. Ihre Komponenten dürfen unterschiedliche Typen haben.

fst (2,3)       -- 2
snd (2,3)       -- 3

dritte (a,b,c) = c

Das letzte Beispiel zeigt erneut Pattern Matching: Das Tupel wird direkt in seine Bestandteile zerlegt.

Listen

[2,4,3,4]
[1..5]
[1,3..11]
['a'..'z']

Alle Elemente einer Liste müssen denselben Typ besitzen. Doppelte Elemente sind erlaubt; auch die leere Liste [] ist eine Liste.

["Das", "ist", 3] ist keine korrekte Liste, weil Zeichenketten und eine Zahl gemischt werden.

Einige eingebaute Listenfunktionen

AusdruckErgebnis / Wirkung
head [3,1,6]3
tail [3,1,6][1,6]
length [3,1,6]3
reverse [3,1,6][6,1,3]
[1,2] ++ [3,4][1,2,3,4]
1 : [2,3][1,2,3]

Der Operator : heißt Cons-Operator und setzt ein Element vor eine Liste. Der Operator ++ verbindet zwei Listen.

Die Schreibweise (x:xs)

(x:xs) beschreibt eine nichtleere Liste: x ist das erste Element, xs die Restliste. Der Doppelpunkt : ist dabei der Cons-Operator.

kopf (x:xs) = x
rest (x:xs) = xs

Diese Form wird später für rekursive Listenfunktionen besonders wichtig. Die Hilfe erinnert hier bewusst nur an die Schreibweise und den Fachbegriff; sie ersetzt nicht die ausführliche Behandlung im Unterricht.

Listen erzeugen

Listengeneratoren

Mit Listengeneratoren werden Listen beschreibend erzeugt: aus einem Wertebereich, einem oder mehreren Generatoren und optionalen Bedingungen.

WertebereichGeneratorFilterErgebnisliste

Grundform

[ ausdruck | variable <- quelle, bedingung ]

Beispiel: alle durch 3 teilbaren Zahlen von 1 bis 20.

[x | x <- [1..20], mod x 3 == 0]

Ergebnis: [3,6,9,12,15,18].

Die Form der Listenelemente bestimmen

[(x, x*x) | x <- [1..20], mod x 3 == 0]

Der Ausdruck links vom senkrechten Strich bestimmt, was in der Ergebnisliste landet. Hier wird aus jedem passenden x ein Tupel aus Zahl und Quadrat.

Mehrere Generatoren

[(x,y) | x <- [0,1], y <- [0,1]]

Ergebnis:

[(0,0),(0,1),(1,0),(1,1)]

Damit lassen sich sehr bequem alle möglichen Belegungen zweier logischer Eingänge erzeugen.

Von der logischen Funktion zur Wahrheitstabelle

Jetzt greifen bekannte Logik und Listengenerator ineinander.

nand :: Int -> Int -> Int
nand 1 1 = 0
nand _ _ = 1

belegung2 = [(a,b) | a <- [0,1], b <- [0,1]]

wahrheitNand =
  [(a,b,nand a b) | a <- [0,1], b <- [0,1]]

wahrheitNand erzeugt die Eingangsbelegungen und berechnet für jede Belegung den Ausgang der Haskell-Funktion.

Das Beispiel zeigt ein übertragbares Muster. In Aufgaben kann dieselbe Idee mit einer anderen logischen Funktion und einem anderen Funktionsnamen vorkommen.
Wichtiges Muster: Erst eine Funktion definieren, dann ihren Eingaberaum mit einem Listengenerator systematisch untersuchen.

Mehrere Bedingungen

[(i,j,i*j) |
  i <- [1..100],
  j <- [3,6..99],
  mod (i+j) 7 == 0]

Mehrere Bedingungen werden durch Kommas getrennt. Nur Kombinationen, die alle Bedingungen erfüllen, gelangen in die Ergebnisliste.

Algorithmische Idee

Rekursion

In Haskell gibt es keine klassischen imperativen Schleifen. Für selbst definierte Wiederholungen arbeiten wir mit Rekursion: Eine Funktion ruft sich mit einer veränderten Eingabe erneut auf.

Rekursionsanker und Rekursionsschritt

Eine rekursive Definition braucht zwei Dinge:

Rekursionsanker
Der Fall, bei dem kein weiterer Selbstaufruf erfolgt.
Rekursionsschritt
Die Funktion ruft sich mit einer veränderten Eingabe erneut auf.
summe :: Int -> Int
summe 0 = 0                 -- Rekursionsanker
summe n = n + summe (n-1)  -- Rekursionsschritt

Eine rekursive Auswertung nachvollziehen

summe 4
= 4 + summe 3
= 4 + 3 + summe 2
= 4 + 3 + 2 + summe 1
= 4 + 3 + 2 + 1 + summe 0
= 4 + 3 + 2 + 1 + 0
= 10

Der Rekursionsanker beendet die Kette. Danach können die noch offenen Ausdrücke vollständig ausgewertet werden.

Ein weiteres Muster: ggT

ggt :: Int -> Int -> Int
ggt a 0 = a
ggt a b
  | a > b     = ggt (a-b) b
  | otherwise = ggt a (b-a)

Hier treffen Pattern Matching, Guards und Rekursion zusammen. Entscheidend ist, dass sich die Eingaben in Richtung des Abbruchfalls verändern.

Vertiefung: Akkumulator

Manche rekursiven Funktionen führen ein Zwischenergebnis als zusätzlichen Parameter mit. Diesen Parameter nennt man Akkumulator. Für viele grundlegende Aufgaben ist das zunächst nicht nötig.

Beispiel ansehen
summeAkk n = hilfe n 0

hilfe 0 akk = akk
hilfe n akk = hilfe (n-1) (akk+n)
Rekursion auf Datenstrukturen

Listen rekursiv verarbeiten

Eine Liste ist entweder leer oder besteht aus erstem Element und Restliste. Genau diese Struktur passt zu Pattern Matching und Rekursion.

[]oder(x:xs)

Die zwei Listenfälle

MusterBedeutung
[]leere Liste
(x:xs)nichtleere Liste mit erstem Element x und Restliste xs

Kopf und Rest selbst definieren

kopf :: [a] -> a
kopf (x:xs) = x

rest :: [a] -> [a]
rest []     = []
rest (x:xs) = xs

Das Muster (x:xs) zerlegt die Liste. Es ist kein Tupel.

Länge einer Liste rekursiv bestimmen

laenge :: [a] -> Int
laenge []     = 0
laenge (x:xs) = 1 + laenge xs

Beim Rekursionsschritt wird nicht die ursprüngliche Liste, sondern nur noch die Restliste xs verarbeitet.

Ein Element suchen

istEnthalten :: Eq a => a -> [a] -> Bool
istEnthalten y [] = False
istEnthalten y (x:xs)
  | y == x    = True
  | otherwise = istEnthalten y xs

Hier greifen Listenmuster, Guards und Rekursion ineinander. Dieses Muster ist für viele weitere Listenalgorithmen wichtig.

Zielbereich

Sortieralgorithmen in Haskell

Beim Sortieren kommt das bisherige Rüstzeug zusammen: Funktionen, Vergleiche, Pattern Matching, Guards, Listen, Listengeneratoren und Rekursion.

Ein Element in eine sortierte Liste einfügen

einfuegen :: Ord a => a -> [a] -> [a]
einfuegen y [] = [y]
einfuegen y (x:xs)
  | y <= x    = y : x : xs
  | otherwise = x : einfuegen y xs

Ist die Liste leer, besteht das Ergebnis nur aus y. Sonst wird entschieden, ob y vor das erste Element gehört oder in der Restliste weitergesucht werden muss.

Insertion Sort

isort :: Ord a => [a] -> [a]
isort []     = []
isort (x:xs) = einfuegen x (isort xs)

Die Restliste wird zuerst rekursiv sortiert. Danach wird x mit einfuegen an die richtige Stelle gesetzt.

Beim Lesen eines Sortieralgorithmus: zuerst Anker erkennen, dann den Rekursionsschritt, anschließend an einer kleinen Liste auswerten.

Quicksort mit Listengeneratoren

quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
  quicksort [y | y <- xs, y <= x]
  ++ [x] ++
  quicksort [y | y <- xs, y > x]

Das erste Element x dient als Trennelement. Zwei Listengeneratoren teilen die Restliste in kleinere beziehungsweise gleich große und größere Elemente. Beide Teillisten werden rekursiv sortiert.

Algorithmen vergleichen

FrageWorauf achten?
Was ist der Rekursionsanker?Welche Eingabe kann direkt beantwortet werden?
Wie wird das Problem kleiner?Welche Restliste oder Teilliste wird rekursiv bearbeitet?
Wo findet der Vergleich statt?Guard, Vergleichsfunktion oder Filter?
Wie entsteht die Ergebnisliste?:, ++, Hilfsfunktion oder Listengenerator?
Wie lässt sich die Funktion verändern?Andere Ordnung, andere Datenstruktur oder eigene Vergleichsfunktion?
Ziel: Einen Sortieralgorithmus nicht nur abschreiben, sondern seine Auswertung nachvollziehen, Teilfunktionen erklären und Varianten selbst entwickeln können.