Funktionale Programmierung mit Haskell/ Kontrollstrukturen

Der do-Block Bearbeiten

In Haskell-Programmen gilt es als schlechter Stil, Programmteile mit geschweiften Klammern und Semikolon zu unterteilen. Stattdessen verwendet man besser Strukturen wie den do-Block:

Beispiel für den do-Block
import System.IO (getLine)
main = do
       print "Bitte geben Sie eine Zahl ein"
       eingabe <- getLine
       let zahl = read eingabe
           x = 5
       if zahl > x then do
                 putStrLn ("Die Zahl ist groesser als fünf")
                 putStrLn ("------------------------------")
                else do
                  if zahl == 5 then do 
                   putStrLn ("Die Zahl ist gleich fünf")
                   putStrLn ("------------------------------")
                         else do
                     putStrLn ("Die Zahl ist kleiner als fünf")
                     putStrLn ("------------------------------")
       putStrLn ("Programmende")

In den Zeilen 2, 7, 10 und 14 beginnen do-Blöcke. Die Einrückung der Folgezeilen zeigt an, zu welchen do-Block eine Zeile gehört. Zeilen 8 und 9 gehören zum do-Block, der in Zeile 7 beginnt. die Zeile 17 gehört zum do-Block der Zeile 2. Unklare Einrückungen beantwortet der Compiler mit Fehlermeldungen. Da die Einrückung in Haskell-Programmen eine wichtige Rolle spielt, sollten auch keine Tabulatoren verwendet werden. Wenn innerhalb des do-Blocks Variablen definiert werden, gelten sie nur dort. Der do-Block soll nur verwendet werden, wenn mehr als eine Anweisung folgt. Bei nur einer Anweisung ist er überflüssig.


Die Anweisungen let .. in und where Bearbeiten

Um lokale Variablen zu kapseln, kann man neben dem do-Block auch die Anweisungskombination let / in oder den where-Befehl verwenden. In den folgenden Funktionen gilt dies für die lokalen Variablen x und y:

Beispiele für let/in und where, Dateiname hypothenuse.hs
hypothenuse1 a b = sqrt (x+y)
  where
   x = a*a
   y = b*b

hypothenuse2 a b =
    let x = a*a
        y = b*b
    in sqrt (x+y) 
     
main = do
  putStrLn ("Die Hypothenuse bei a=3 und b=4 ist " ++ show (hypothenuse1 3 4))
  putStrLn ("Die Hypothenuse bei a=3 und b=4 ist " ++ show (hypothenuse2 3 4))
  Ausgabe
Prelude>  :load hypothenuse.hs 
[1 of 1] Compiling Main             ( hypothenuse.hs, interpreted )
Ok, modules loaded: Main.

*Main> main
Die Hypothenuse bei a=3 und b=4 ist 5.0
Die Hypothenuse bei a=3 und b=4 ist 5.0

*Main>

where bezieht sich auf die unmittelbar vorangehende Funktion sqrt (x+y). Dort werden zwei Variablen verwendet, die erst nach dem where definiert werden. Bei let werden die Variablen sofort definiert und in der in-Anweisung verwendet. Diese beiden Konstrukte können in vielen Fällen beliebig angewandt werden, aber es gibt auch Fälle, in denen nur eine Variante möglich ist. Bei let und where ist die Einrückung eben so wichtig wie beim do-Block


if-then-else Bearbeiten

Die if/then/else-Struktur wurde schon einige Male verwendet, hier ist die Zusammenfassung aller wichtigen Eigenschaften:

  • In jeder if-Anweisung muss ein then und ein else auftauchen.
  • mit return () kann der then oder else-Zweig verlassen werden.
  • Klammerung ist möglich, aber nicht zwingend nötig
  • Bei mehr als einer Anweisung im then oder else-Zweig: mit dem do-Block klammern
  • Falsche Einrückungen führen zu Compilerfehlern!

Hier ist ein if-then-else-Programm zum Üben:

Beispiele für If/Then/Else, Dateiname iftest.hs
-- If/Then/Else - Beispiele zum Üben
main = do
      let a = 10
          b = 300
          c = 9
          d = "XX"
      if a > c then print "Zehn ist groesser als neun"
               else do
                 return ()
                 print "hier kommt das Programm nicht hin"
      if b /= c   -- so wird die Ungleichheit beschrieben
      then print "Dreihundert ist ungleich neun"
      else if d  ==['X']++['X'] 
          then print "XX ist wie zwei einzelne X"
          else do
               print "hier kommt das Programm nicht hin"
               print "hier kommt das Programm nicht hin"
      if a >= 5+5 || 8==9 then print "10 ist groesser gleich 5+5 oder acht ist neun"
                          else print "Kein if ohne else" 
      if a <= 5+5 && 8==9 then print "10 ist kleiner gleich 5+5 und acht ist neun"
                          else print "Die und-Bedingung ist fehlgeschlagen"
      if not (a <= 5+5 && 8==9) then print "Not aendert alles"
                                else print "hier kommt das Programm nicht hin"
      print "Ende des Tests"
  Ausgabe
Prelude>  :load iftest.hs 
*Main> main
"Zehn ist groesser als neun"
"Dreihundert ist ungleich neun"
"10 ist groesser gleich 5+5 oder acht ist neun"
"Die und-Bedingung hat fehlgeschlagen"
"Not aendert alles"
"Ende des Tests"


Fallunterscheidungen mit dem guard-Pattern Bearbeiten

Das case-Konstrukt kann in Haskell auch eleganter beschrieben werden. Im folgenden Beispiel wird ein Datum, bestehend aus Tag, Monat und Jahr, auf seine logische Gültigkeit hin geprüft:

Beispiel für ein guard-Pattern, Dateiname guard.hs
evaluiereDat dd mm yy = richtig
        where
        richtig
         | dd<1                 = "Der Tag ist kleiner als Eins"
         | dd>31                = "Der Tag ist groesser als 31"
         | mm<1                 = "Der Monat ist kleiner als Eins"
         | mm>12                = "Der Monat ist groesser als 12"
         | yy<1582              = "Das Jahr ist kleiner als 1582"
         | yy>2999              = "Das Jahr ist groesser als 2999"
         | (dd==31)&&(elem mm [2,4,6,9,11]) = "Dieser Monat hat nur 30 Tage"
         | (mm==2)&&(dd==30)     = "Den 30. Februar gibt es nicht"
         | (mm==2)&&(dd==29)&&((mod yy 400) ==0) = "OK"
         | (mm==2)&&(dd==29)&&((mod yy 100) ==0) = ("Das Jahr " ++ show yy ++ " war/ist kein Schaltjahr")
         | (mm==2)&&(dd==29)&&((mod yy 4)    >0) = ("Das Jahr " ++ show yy ++ " war/ist kein Schaltjahr")
         | otherwise            = "OK"
  Ausgabe
Prelude> :l guard.hs 
[1 of 1] Compiling Main             ( guard.hs, interpreted )
Ok, modules loaded: Main.
*Main> evaluiereDat 10 10 2000
"OK"
*Main> evaluiereDat 33 29 88
"Der Tag ist groesser als 31"
*Main> evaluiereDat 31 4 1900
"Dieser Monat hat nur 30 Tage"
*Main> evaluiereDat 18 13 2000
"Der Monat ist groesser als 12"
*Main> evaluiereDat 10 10 888
"Das Jahr ist kleiner als 1582"
*Main> evaluiereDat 10 10 99999
"Das Jahr ist groesser als 2999"
*Main> evaluiereDat 30 2 2004
"Den 30. Februar gibt es nicht"
*Main> evaluiereDat 29 2 2000
"OK"
*Main> evaluiereDat 29 2 1900
"Das Jahr 1900 war/ist kein Schaltjahr"
*Main> evaluiereDat 29 2 1981
"Das Jahr 1981 war/ist kein Schaltjahr"
*Main> evaluiereDat 29 2 1984
"OK"
*Main> [(x, y, z)|z<-[2000..2002], y<-[1..12], x<-[1..31], ((evaluiereDat x y z)=="OK")]

Die Funktion evauliereDat hat die drei Parameter dd für Tag, mm für Monat und yy für Jahr. In jeder Zeile, die mit einer pipe beginnt, findet eine Evaluierung statt. Bei falschen Eingaben wird ein erklärender Text zurückgegeben, bei korrekten Eingaben ein "OK". Die Evaluierung bricht ab, wenn eine Bedingung zutrifft. Z.B. trifft beim Datum 29.2.2000 in Zeile 12 die Regel zu, dass Schaltjahre durch 400 teilbar sind. Die Regel in Zeile 13 würde dann ebenfalls zutreffen (Schaltjahre sind nicht durch 100 teilbar), doch durch den Abbruch in Zeile 12 wird diese Regel nicht mehr evaluiert.

Der unterste Befehl, dessen Ergebnis hier nicht dargestellt wurde, baut einen gültigen Kalender der Jahre 2000 bis 2002.

Fallunterscheidungen mit case Bearbeiten

Der $-Operator Bearbeiten

Der $-Operator wird schon im Kapitel "Funktionen auf Listen" intensiv genutzt. Er wendet den Befehl links vom $ an auf den Wert, der rechts davon ermittelt wurde. Hier die Konkatenation einiger Befehle:

Dialog in ghci - $-Operator
Prelude> reverse $ show $ sum $ take 5 $ filter (>1010) $[1000,1003..1030]
"0905"
Prelude> floor $ sqrt 10
3
Prelude>

Die Befehle der ersten Anweisung lauten im Einzelnen:

  • Baue eine Liste auf mit allen Zahlen von 1000 bis 1030 in Dreierschritten
  • Nimm davon nur die Zahlen, die größer sind als 1010
  • Nimm davon nur die ersten 5 Zahlen
  • Summiere diese fünf Zahlen auf
  • Verwandle die entstandene Zahl in einen String
  • Zeige diesen String in umgekehrer Reihenfolge an.

Die Befehle der zweiten Anweisung lauten: Nimm die Quadratwurzel von 10 und runde das Ergebnis ab.

Function Composition mit dem Punkt-Operator Bearbeiten

Function composition nennt man das Hintereinanderschalten von Funktionen mit dem dot-Operator (.). Er ersetzt die Klammerung von manchen Funktionen. Der Ausdruck f=func1(func2 x) wird mit dem dot-Operator zu f = func1 . func2 . Wenn es nur einen Übergabewert (hier x) gibt und er am Ende der Definitionszeile steht, kann auf dessen Nennung verzichtet werden.

Dialog in ghci -Punkt-Operator
Prelude> let a="zaehle die Buchstaben des letzten Wortes"

Prelude> length (last (words a))
6
Prelude> let f x =length ( last ( words x))    -- Darstellung mit Klammern
Prelude> let f x =length . last . words $ x    -- wenn x der einzige Parameter ist und ganz hinten steht, kann auf ihn verzichtet werden
Prelude> let f   =length . last . words
Prelude> f a
6

Prelude> f "Hier ein anderer String 1234567890"
10

Prelude> -- Hier ein Beispiel mit Quersummenberechnung
Prelude> let quersumme a = sum (map Data.Char.digitToInt (show a))
Prelude> quersumme $ (2^1000)
1366
Prelude> quersumme.quersumme $ (2^1000)
16
Prelude> quersumme.quersumme.quersumme $ (2^1000)
7
Prelude>

Lazy Evaluation Bearbeiten

In jeder Definition von Haskell wird erwähnt, dass Haskell das Paradigma Lazy Evaluation betreibt. Das erkennt man an solchen Ausdrücken:

Lazy Evaluation
Prelude> let a=[1..]
Prelude>

Der Variablen a wird also eine unendlich große Liste zugewiesen, und der Interpreter meldet keinen Fehler? Ja, denn der Interpreter hat nur die Definition entgegengenommen und wird die Auswertung erst dann starten, wenn ein Ergebnis gefordert wird, etwa jetzt:

Prelude> let a=[1..]
Prelude> take 10 a
[1,2,3,4,5,6,7,8,9,10]
Prelude> sum (take 10 a)
55

Aus einer Liste, die ursprünglich unendlich groß war, werden letztlich nur die ersten zehn Werte benötigt. In Haskell ist also die Definition eines Werts nicht unbedingt gleich der Zeitpunkt der Auswertung. Der Interpreter geht davon aus, dass für das eigentliche Ergebnis weniger zu tun ist als ursprünglich gefordert und wartet daher mit der Auswertung so lange wie möglich. Das zeigt auch das nächste Beispiel, in dem alle Zahlen von eins bis 100000 multipliziert werden.

Prelude> let a = product [1..100000]            -- Die Definition geht ganz schnell
Prelude> a                                      -- Die Auswertung dauert!
282422940796034787429342157802453551847         -- usw...

Das Gegenteil von lazy evaluation ist strict evaluation. In manchen Fällen wird von Haskell-Funktionen eine strict evaluation erwartet, deshalb gibt es für viele Befehle auch eine strict-Varianten, die in eigenen .Strict-Modulen angeboten werden[1].


  1. siehe [7] im Quellenverzeichnis