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.
Browser→JupyterLab→Schulserver→GHC / 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.
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.
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 installiert
Haskell am WvS
GHC / GHCi
läuft auf dem eigenen Computer
läuft auf dem Schulserver
Bedienung
z. B. Terminal + Editor
JupyterLab im Browser
Dateien
liegen auf dem eigenen Computer
liegen im persönlichen Bereich auf dem Server
Internet
für die eigentliche Arbeit nicht zwingend nötig
für den Zugriff auf den Schulserver nötig
Pakete
selbst verwaltbar
durch 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.
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.
JupyterHubHub Control Panel
Stop My ServerMy Server
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.
GHCi wertet einen eingegebenen Ausdruck aus und zeigt das Ergebnis an.
Wichtige GHCi-Befehle
Befehl
Bedeutung
:load datei.hs oder :l datei.hs
Datei laden
:reload oder :r
zuletzt geladene Datei erneut laden
:type ausdruck oder :t ausdruck
Typ eines Ausdrucks anzeigen
:quit oder :q
GHCi 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.
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.
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
Typ
Beispiel
Bedeutung
Int
42
ganze Zahl in festem Wertebereich
Integer
123456789012345
beliebig große ganze Zahl
Float
3.14
Gleitkommazahl
Bool
True, False
Wahrheitswert
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
Ausdruck
Ergebnis / 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.
Wertebereich→Generator→Filter→Ergebnisliste
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
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
Muster
Bedeutung
[]
leere Liste
(x:xs)
nichtleere Liste mit erstem Element x und Restliste 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
Frage
Worauf 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.