H

Haskell am WvS – Hilfe

Arbeitsumgebung, Syntax und Lernweg – kompakt an einem Ort

Hilfe 0.5.3 · 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.

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 Logik→Haskell-Funktion→Pattern Matching / Guards

Eine Funktion definieren

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

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

Bei einer einfachen Funktionsdefinition stehen links vom Gleichheitszeichen der Funktionsname und die Parameterbezeichner. Rechts steht der Ausdruck, der das Ergebnis berechnet. Beim Pattern Matching können an der Stelle der Parameterbezeichner auch konkretere Muster stehen.

Pattern Matching – Muster vergleichen

Pattern Matching ist der Mustervergleich der Argumente einer Funktion mit den links vom Gleichheitszeichen angegebenen Mustern. Die Funktionsgleichungen werden von oben nach unten geprüft; die erste passende Gleichung 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.

Bezeichner und Platzhalter im Muster

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

zweite (_,b) = b

Ein Bezeichner in einem Muster bindet den dazu passenden Wert. Der Unterstrich _ ist ein Platzhalter: Er passt auf einen typgerechten Wert, bindet ihn aber nicht an einen Namen.

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 Mustern und Strukturen; 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; let und case werden hier bewusst nicht eingeführt.

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 Pattern Matching: Das Tupel wird durch das Muster (a,b,c) 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. Schreibweisen wie [1..5] oder [1,3..11] erzeugen Werte mit gleichmäßigen Abständen.

["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]
init [3,1,6][3,1]
last [3,1,6]6
reverse [3,1,6][6,1,3]
length [3,1,6]3
[3,1,6] !! 11 (Index beginnt bei 0)
[1,2] ++ [3,4][1,2,3,4]

++ verbindet zwei Listen. Die Zerlegung einer Liste in erstes Element und Restliste sowie der Cons-Operator : folgen nach den Listengeneratoren im nächsten Lernschritt.

Listen erzeugen

Listengeneratoren (List Comprehensions)

Mit Listengeneratoren werden Listen beschreibend erzeugt. Entscheidend sind dabei die Begriffe Ergebnisausdruck, Muster, Ausgangsliste, Generator und – falls vorhanden – Filter/Bedingung.

Ergebnisausdruck→Generator: Muster <- Ausgangsliste→Filter→Ergebnisliste

Grundform ohne Filter

[ Ergebnisausdruck | Muster <- Ausgangsliste ]

Der Teil Muster <- Ausgangsliste heißt Generator. Links von <- steht formal ein Pattern, also ein Muster. Im einfachsten Fall ist dieses Muster nur ein Bezeichner wie x.

[x | x <- [1..5]]

x nimmt nacheinander die Werte aus der Ausgangsliste an. Das Ergebnis ist hier wieder [1,2,3,4,5].

Den Ergebnisausdruck verändern

[x*x | x <- [1..5]]
[(x, x*x) | x <- [1..5]]

Der Ausdruck links vom senkrechten Strich bestimmt die Form der Elemente in der Ergebnisliste. Das Muster im Generator bleibt dabei x.

Werte mit einer Bedingung filtern

[ Ergebnisausdruck | Muster <- Ausgangsliste, Bedingung ]

Nur Werte, für die die Bedingung True ergibt, gelangen in die Ergebnisliste.

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

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

Mehrere Bedingungen

[x | x <- [1..100], div x 5 > 4, mod x 7 == 0]

Mehrere Filter werden durch Kommas getrennt. Ein Wert wird nur übernommen, wenn alle Bedingungen erfüllt sind; die Kommas wirken hier also wie eine logische UND-Verknüpfung der Bedingungen.

Mehrere Generatoren

[(x,y) | x <- [1..3], y <- ['a'..'c']]

Bei mehreren Generatoren kann man sich die Auswertung ähnlich wie geschachtelte Durchläufe vorstellen: Der erste Generator bildet den äußeren, der zweite den inneren Durchlauf.

x <- [1..3]       -- äußerer Generator
y <- ['a'..'c']   -- innerer Generator

Transfer: Eingaberäume systematisch erzeugen

Beispiel mit einer logischen Funktion ansehen
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]]

Das Beispiel nutzt zwei Generatoren, um alle Eingangsbelegungen zu erzeugen. Es ist ein Transferbeispiel; die Grundbegriffe des Listengenerators bleiben dieselben.

Nächster Lernschritt

Cons-Operator und Rekursion

Nach den Listengeneratoren folgt die strukturelle Sicht auf Listen: Mit : wird eine Liste aufgebaut, mit dem Muster (x:xs) kann eine nichtleere Liste zerlegt werden. Danach nutzen wir Rekursion für wiederholte Verarbeitung.

: / (x:xs)→Rekursionsanker→Rekursionsschritt→rekursive Listen

Der Cons-Operator :

Rechts vom Gleichheitszeichen setzt : ein Element vor eine Liste.

1 : [2,3]      -- [1,2,3]
'a' : "bc"     -- "abc"

Links vom Gleichheitszeichen kann dieselbe Schreibweise als Muster auftreten:

kopf (x:xs) = x

Das Muster (x:xs) passt auf eine nichtleere Liste. x bindet das erste Element, xs die Restliste. Es ist kein Tupel.

Rekursionsanker und Rekursionsschritt

Eine rekursive Definition braucht mindestens einen Fall ohne weiteren Selbstaufruf und einen Fall, in dem die Funktion sich mit einer veränderten Eingabe erneut aufruft. Das folgende Beispiel ist für n >= 0 gedacht.

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

Eine rekursive Auswertung nachvollziehen

summeBis 4
= 4 + summeBis 3
= 4 + (3 + summeBis 2)
= 4 + (3 + (2 + summeBis 1))
= 4 + (3 + (2 + (1 + summeBis 0)))
= 4 + (3 + (2 + (1 + 0)))
= 10

Der Rekursionsanker beendet die Kette der Selbstaufrufe. Erst danach sind alle noch offenen Teilausdrücke vollständig bestimmbar.

Für den Einstieg: Zuerst normale Rekursion sicher verstehen. Akkumulatoren sind eine spätere Vertiefung und werden hier noch nicht benötigt.
Rekursion auf Datenstrukturen

Listen rekursiv verarbeiten

Eine Liste ist entweder leer oder besteht aus erstem Element und Restliste. Genau diese Struktur lässt sich mit Pattern Matching und Rekursion direkt abbilden.

[]oder(x:xs)

Die zwei grundlegenden Listenmuster

MusterBedeutung
[]leere Liste
(x:xs)nichtleere Liste: x erstes Element, xs Restliste

Wird das erste Element nicht benötigt, kann der Platzhalter verwendet werden: (_:xs).

Länge einer Liste rekursiv bestimmen

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

Der Wert des ersten Elements ist für die Länge unwichtig. Deshalb steht dort _. Im Rekursionsschritt wird nur noch die Restliste xs verarbeitet.

Elemente verarbeiten

summe []     = 0
summe (x:xs) = x + summe xs

produkt []     = 1
produkt (x:xs) = x * produkt xs

Die Struktur ist gleich; nur Ankerwert und Verknüpfung unterscheiden sich.

Ein Element suchen

istEnthalten _ [] = False
istEnthalten y (x:xs)
  | y == x    = True
  | otherwise = istEnthalten y xs

Hier greifen Listenmuster, Guards und Rekursion ineinander.

Die Spezifikation entscheidet über die Fälle

Nicht jede Funktion muss für jede denkbare Eingabe definiert sein. Wenn eine Aufgabe beispielsweise ausdrücklich eine nichtleere Liste voraussetzt, muss nicht automatisch ein zusätzlicher Fall für [] mit einer Fehlermeldung erfunden werden. Entscheidend ist die angegebene Spezifikation.

Insbesondere wird nicht reflexartig error "..." ergänzt. Prüfen Sie zuerst, welche Eingaben laut Aufgabenstellung überhaupt vorgesehen sind.
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 den Rekursionsanker 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.
Auch Laufzeiten vergleichen: Mit dem zentral installierten Modul Zeitmessung lassen sich verschiedene Sortierverfahren für gleich große Eingaben praktisch vergleichen. Siehe 11 Laufzeit.
Programme strukturieren

Eigene Module verwenden

Mit Modulen können bereits definierte Funktionen in einer eigenen Datei gesammelt und in anderen Haskell-Dateien wiederverwendet werden. Im Unterricht dient dafür das Modul mit ggT und kgV als Referenz.

GgtKgvModul.hs→Exportschnittstelle→import→Verwendung

1. Moduldatei und Modulname

Der Modulname beginnt mit einem Großbuchstaben. Bei einem benannten Modul muss der Dateiname zum Modulnamen passen.

GgtKgvModul.hs enthält die Modulzeile module GgtKgvModul (ggT, kgV) where.

Darunter stehen die bereits erarbeiteten Definitionen von ggT und kgV. Die Namen in der Klammer hinter module bilden die Exportschnittstelle. Nur diese Namen werden nach außen angeboten.

Die Hilfe wiederholt die Definitionen von ggT und kgV hier bewusst nicht; im Modul-Kapitel geht es um Struktur, Export und Import, nicht um eine neue Lösung dieser Funktionen.

2. Modul in einer anderen Datei verwenden

-- andere Haskell-Datei im selben Ordner
import GgtKgvModul

beispiel1 = ggT 84 30
beispiel2 = kgV 12 18

Die Moduldatei und die verwendende Datei liegen für diesen einfachen Fall im selben Ordner. Geladen wird anschließend die verwendende Datei; der Import zieht das Modul hinzu.

3. Nur ausgewählte Funktionen importieren

import GgtKgvModul (ggT)

So wird aus dem Modul nur der angegebene Name in der eigenen Datei verfügbar.

4. Lokales Modul oder zentral installiertes Modul?

ArtBeispielWo liegt es?
eigenes lokales ModulGgtKgvModulals GgtKgvModul.hs im Arbeitsordner
zentral installiertes ModulZeitmessungauf dem Haskell-Server; keine Moduldatei im Schülerordner nötig
import Zeitmessung
Merksatz: import macht die exportierten Namen eines Moduls in der eigenen Datei verfügbar.
Algorithmen untersuchen

Laufzeit messen

Das zentral installierte Modul Zeitmessung misst Laufzeiten. Ab Version 0.1.3 schützt es auch die Auswertung der Eingabe vor übermäßigem Zeit- und Allokationsaufwand.

1. Modul in GHCi einbinden

ghci> :l meineDatei.hs
ghci> import Zeitmessung
ghci> Zeitmessung.hilfe

Mehr ist nicht zu installieren. Das Modul ist im Haskell-Labor zentral verfügbar. Zeitmessung.hilfe zeigt jederzeit die Kurz-API; für Messungen ist die direkte Nutzung im GHCi meist am einfachsten.

2. Die acht Befehle

AufrufBedeutung
Zeitmessung.hilfekomplette Kurz-API mit Beispielen im GHCi anzeigen
laufzeit0 wertDefinition ohne Parameter / bereits definierten Wert messen
laufzeit f xFunktion mit einem Argument messen
laufzeit2 f x yFunktion mit zwei Argumenten messen
laufzeit3 f x y zFunktion mit drei Argumenten messen
setLimit 20Zeitlimit auf 20 Sekunden setzen
getLimitaktuelles Zeitlimit anzeigen
resetLimitStandard von 10 Sekunden wiederherstellen
Wie ging der Aufruf noch einmal? Nach import Zeitmessung einfach Zeitmessung.hilfe eingeben. Die qualifizierte Form bleibt auch dann eindeutig, wenn Ihre eigene Datei bereits eine Funktion hilfe enthält.
Das gesetzte Limit gilt nur in der aktuellen GHCi-Sitzung. Nach einem Neustart sind wieder 50 Sekunden eingestellt.

3. Beispiele

Ohne Parameter: ein Wert

ghci> laufzeit0 porto1

Eine Definition wie porto1 = [...] ist in Haskell keine Funktion mit null Argumenten, sondern ein Wert. Dafür ist laufzeit0 gedacht.

Sharing beachten: Wenn porto1 vorher schon ausgegeben wurde, kann der Wert bereits berechnet und zwischengespeichert sein. Für eine frische Messung ggf. :r, danach import Zeitmessung und erst dann laufzeit0 porto1.

Ein Argument

laufzeit porto2 222
laufzeit fib 35
laufzeit reverse [1..100000]
laufzeit listenmaximum [1..100000]

Zwei Argumente

laufzeit2 hlaenge "Informatik" 0
laufzeit2 istEnthalten 99999 [1..100000]

Drei Argumente

laufzeit3 hfib 35 1 1

Tupel

Ein Tupel ist für die Zeitmessung ein Argument:

laufzeit quadratsumme (3,4)

4. GHCi ist nicht dasselbe wie eine .hs-Datei

Im GHCi dürfen Ausdrücke direkt ausgeführt werden:

ghci> laufzeit porto2 222

In einer .hs-Datei darf derselbe Ausdruck nicht allein auf oberster Ebene stehen. Dort stehen Definitionen bzw. Deklarationen:

import Zeitmessung

testPorto2 :: IO ()
testPorto2 = laufzeit porto2 222

Nach dem Laden kann dann im GHCi testPorto2 aufgerufen werden.

5. Funktionen höherer Ordnung

Eine Funktion als Argument wird am besten vorher teilweise angewendet:

laufzeit (map quadrat) [1..100000]
laufzeit (filter even) [1..100000]

So erhält laufzeit wieder eine normale Funktion mit einem Datenargument.

6. Ausgabe verstehen

ghci> laufzeit fib 40
Berechnung läuft ....
Laufzeit: 4.238741 s

Jeder Punkt ist nur ein Lebenszeichen ungefähr pro Sekunde. Er ist kein prozentualer Fortschritt.

ghci> getLimit
Zeitlimit: 10 Sekunden

ghci> laufzeit fib 50
Berechnung läuft ..........
Zeitlimit von 10 Sekunden überschritten.
Berechnung wurde abgebrochen. GHCi läuft weiter.
Tipp: kleinere Eingabe verwenden oder mit setLimit n das Zeitlimit ändern.
Hilfe: Zeitmessung.hilfe

Eine Zeitüberschreitung ist kein Bedienfehler. Bei setLimit 0 erscheint dagegen ein Hinweis auf Zeitmessung.hilfe. Die angezeigte Laufzeit erfasst weiterhin nur die Funktionsauswertung, nicht die Vorbereitung der Eingabe.

7. Schutz vor zu aufwendigen Berechnungen

Standard: 10 Sekunden. Die Zeitbegrenzung beginnt vor der vollständigen Auswertung der Eingabe und gilt für Vorbereitung und Berechnung zusammen. Mit setLimit 20 lässt sich das Limit für die aktuelle GHCi-Sitzung anpassen.

Ein zusätzlicher interner Allokationsschutz begrenzt die insgesamt zugeteilte Speichermenge während eines Messaufrufs (anfänglich 512 MiB). Er misst nicht den tatsächlich belegten Arbeitsspeicher und ist keine Garantie gegen einen Systemabbruch.

ghci> laufzeit reverse [1..100000000]
Berechnung läuft ...
Sicherheitslimit für Rechen-/Speicheraufwand erreicht.
Berechnung wurde abgebrochen. GHCi läuft weiter.

Je nachdem, welcher Grenzwert zuerst erreicht wird, kann stattdessen die Meldung „Zeitlimit von 10 Sekunden überschritten“ erscheinen. Im Normalfall bleibt GHCi erhalten. Falls Linux GHCi trotzdem beendet, startet der Watchdog GHCi neu; gespeicherte .hs-Dateien sind davon nicht betroffen.

8. Messwerte sinnvoll vergleichen

  • Dieselbe Funktion mit mehreren Eingabegrößen messen.
  • Beim Vergleich verschiedener Algorithmen möglichst dieselben Eingaben verwenden.
  • Eine Messung bei Bedarf wiederholen; kleine Zeiten schwanken.
  • Keine unendlichen Listen verwenden: der Schutz soll hängenbleibende Berechnungen abbrechen, ersetzt aber kein sinnvolles Eingabedesign.
Einordnung: Das Werkzeug dient zum Unterrichtsvergleich von Algorithmen in GHCi. Es ersetzt kein professionelles Benchmarking.