2. Definition von Funktionen fact und comb

 

Die Funktion fact Neben dem 'Haskell-Fenster' sollte man immer noch zusätzlich ein Editorfenster geöffnet haben. Hier können wir den  Quelltext eigener, nicht von Haskell bereits zu Verfügung gestellter Funktion implementieren. In unserem ersten Beispiel schreiben wir den Quelltext für eine Funktion die die Fakultät der übergebenen Zahl berechnet. Wir nenn diese Funktion fact. Was schreiben wir in die ASCII-Datei?

module EigeneFunktionen where

fact :: Integer->Integer
fact n = product[1..n]

In der ersten Zeile wird der Name festgelegt, unter dem man die Funktionen sammelt. In unserer Datei ist eine Funktion fact definiert. Dies geschieht in zwei Teilen.

  • Im ersten Teil, er steht in der ersten Zeile unserer Funktionsdefinition, werden Definitionsmenge und Wertemenge, oder anders ausgedrückt die Typen der Eingangs- und Ausgangsparameter festgelegt. Das geht so, dass dem Namen der Funktion ein doppelter Doppelpunkt folgt. Danach steht der Typ des Eingabewertes gefolgt von einem Pfeil ( Minuszeichen dann Größerzeichen), hinter dem der Typ des Ausgabewertes steht. In unserem Fall muss also in beiden Fällen das Wort Integer stehen.
  • Im zweiten Teil, also in der zweiten Zeile,  wird die Funktionalität von fact programmiert. Wieder wird die Zeile durch den Namen der Funktion eingeleitet. Danach folgen die Namen des bzw. der Eingabeparameter (die Namen stehen für lokale Variable). Hinter dem Gleichheitszeichen, es signalisiert, jetzt kommt der Funktionsterm, steht das Kalkül, mit dem der Funktionswert berechnet wird. In diesem Term kommen Funktionsaufrufe von Funktionen vor, die z.B. in Prelude definiert sind.

 

Danach speichern wir die Datei als Kapitel02.hs in einem Verzeichnis unserer Wahl ab. Damit kennt das Haskell-System die neue Funktion noch nicht. Das geht so:

Wir wechseln wieder in unser GHCi-Fenster. Mit

Prelude> :cd <Pfadname>

wechselt man das aktive Verzeichnis. Danach lädt man die Datei mit

Prelude> :load Kapitel02.hs
Compiling EigeneFunktionen ( Kapitel02.hs, interpreted )
Ok, modules loaded: EigeneFunktionen.
*EigeneFunktionen>

Das System meldet, dass der Kode in der Datei kompiliert wird und die Module von EigeneFunktionen geladen sind und damit zum Aufruf bereit stehen. Erkennt der Compiler Fehler, so werden diese hier gemeldet. Wir können nun die eben geschriebene Funktion fact benutzen.

*EigeneFunktionen> fact 8
40320
cmTypeOfName: it
it :: Integer

Die Datei Kapitel02.hs lässt sich um zusätzliche Funktionen erweitern, z.B. durch eine Funktion, die Binomialkoeffizient unter Verwendung der Funktion fact berechnet:

Wir fügen der Datei die folgenden Zeilen hinzu:
 

comb
           

comb :: Integer -> Integer -> Integer
comb a b = fact a `div`(fact b * fact (a-b))

comb definiert den Binomialkoeffizienten "a über b" und ist,  wie wir in der Mathematik sagen, eine Funktion mit zwei Funktionsvariablen und einem Funktionswert, . Hier sagen wir gerne, dass comb zwei Parameter und einen Rückgabewert hat. In der ersten Programmzeile unserer neuen Funktion erkennen wir der Reihe nach die Typen der beiden Parameter und des Rückgabewertes.
 

Die zweite Zeile erklärt sich selbst, denn es gilt.

wobei der Bruchstrich eine Ganzzahldivision (`div`, die div-einrahmenden Striche sind Accent-grave-Zeichen) repräsentiert. `div` ist also eine infix-notierte Funktion

Wir berechnen die Chance einen 6er im Lotto zu gewinnen, setzen zuvor die Ausgabe zurück, dass kein Ausgabetyp mehr angezeigt wird:

*EigeneFunktionen> :unset +t
*EigeneFunktionen> comb 49 6
13983816

 

toUpperCase toUpperCase soll eine Funktion sein, die einen Klein-Buchstaben in einen Groß-Buchstaben umwandelt und einen Groß-Buchstaben unverändert lässt. Genauer müssten wir sagen, toUpperCase ist eine Funktion die einem Klein-Buchstaben seinen entsprechenden Groß-Buchstaben zuordnet. Groß-Buchstaben werden auf sich selbst abgebildet. Bei der Implementierung helfen uns zwei Funktionen ord und chr. Dabei gilt ord :: Char -> Integer und bildet ein Zeichen auf seinen ASCII-Code ab.  Und umgekehrt chr :: Integer -> Char bildet eine Zahl, als ASCII-Code interpretiert, auf das entsprechende Zeichen ab. die beiden Funktionen ord und chr stehen anders als bei Winhugs beim GHCi im Prelude nicht zur Verfügung und muss deshalb mit import Char importiert werden.
 

import Char     //muss an den Anfang der Datei

...


toUpperCase :: Char -> Char
isLow       :: Char -> Bool

toUpperCase c | isLow c = chr (ord c - (ord 'a' - ord 'A'))
              | otherwise = c


isLow c = 'a' <= c && c <= 'z'

Test der Funktion

*EigeneFunktionen> toUpperCase 'z'
'Z'
 


 
Neben der Tatsache, dass man 'Deklarationen' zusammengefasst an den Anfang stellen kann, lernen wir hier ein Verfahren kennen, das nicht nur bei Haskell sondern allgemein in der Informatik von großer Wichtigkeit ist, dem pattern matching, oder ins Deutsche übersetzt dem Musterabgleich. Betrachten wir zunächst die Kodes hinter den senkrechten Strichen | (guards genannt).  Liefert die Funktion isLow bei einem Parameterwert von z.B. 'a' den Wert True, so ist der Funktionswert chr (ord c - (ord 'a' - ord 'A')). Nachrechnen zeigt, dass das das Zeichen 'A' ist.  Dem senkrechten Strich folgt also eine Bedingung, die als Funktions formuliert ist. Betrachten wir den zweiten senkrechten Strich: Die Funktion otherwise liefert generell den Wert True. Was passiert also nun? Die Folge der Ausdrücke hinter den senkrechten Strichen wird von oben nach unten ausgewertet. Liefert der Ausdruck hinter dem ersten Strich den Wert True, wir haben damit die Wertezuweisung  c -> chr (ord c - (ord 'a' - ord 'A')), wird der Rest wird nicht mehr abgearbeitet. Kann der Ausdruck hinter dem ersten senkrechten Strich aber nicht mit True abgeglichen werden, so wird ein Abgleich mit True in der nächsten Zeile versucht. Die Zuweisung c -> c erfolgt also genau dann wenn isLow für c False liefert, d.h. c ist ein Großbuchstabe und damit liefert c -> c genau das gewünschte. Der Kode der Funktion isLow lässt sich leicht verstehen, wenn man weiß, dass <, <=, > und >= Funktionen (infix) sind, die hier zwei  Zeichen auf lexikalische Ordnung prüfen und dass mit && eine logische AND-Funktion (ebenfalls infix) implementiert ist.
 
Bemerkung Die Funktionen toUpperCase und toLowerCase sind in dem Modul Prelude bereits implementiert, heißen dort toUpper und toLower. Entsprechend heißt die Hilfsfunktionen isLow dort isLower. So wird ein Überschreiben der Funktionen verhindert.
 
Implementierung einer infix-notierten Funktion Wir haben im letzten Abschnitt die '.'-Funktion zur Komposition von Funktionen kennen gelernt. Wir wollen jetzt eine solche Funktion selbst implementieren. Wir wollen sie '#'-Funktion nennen.

(#) :: (Integer -> Integer) -> (Integer -> Integer) -> (Integer -> Integer)
(f # g) x = f (g x)

Die runden Klammern um den Funktionsnamen legen fest, dass die hier deklarierte Funktion ein infix-Operator ist. Die Variablen der Funktion f, g und f#g sind Funktionen, ihre Typen also jeweils (Integer -> Integer).

Testen der Koposition:

*EigeneFunktionen> (succ#succ) 3
5

Damit ist aber die #-Funktion noch lange nicht der '.'- Koposition gleichwertig. Das nächste Beispiel zeigt dies:

*EigeneFunktionen> (reverse#reverse) "Zappa"

<interactive>:1:
Couldn't match `[a]' against `Integer'
Expected type: Integer -> Integer
Inferred type: [a] -> [a]
In the first argument of `(#)', namely `reverse'
In the definition of `it': it = (reverse # reverse) "Zappa"
 

Downlaod:
Kapitel02.hs

 
Wir ändern die Definition der '#'-Funktion ab und ersetzen Integer durch Typenvariable und machen unsere Funktion '#' polymorph:
(#) :: (b -> c) -> (a -> b) -> (a -> c)
(f # g) x = f (g x)

Tests:

*EigeneFunktionen> (succ#succ) 4
6
*EigeneFunktionen> (reverse#reverse) "Zappa"
"Zappa"
*EigeneFunktionen>

Verwenden wir Typenvariable erkennen wir jetzt auch sehr schön, dass f nach g ausgewertet wird.
 

Übung 1 Schreiben Sie eine Datei Binomi.hs in der die Funktionen binomi1, binomi2 und binomi3 definiert werden. Die Spezifikation von binomi1 ergibt sich aus der Forderung: binomi1 berechnet für die übergebenen Werte a und b (a+b)2, wobei a und b vom Typ Integer sein sollen. Die Spezifikationen von binomi2 und binomi3 sind entsprechend.
Übung 2 Schreiben Sie eine Datei Flaechen.hs mit den Funktionen quadratFlaeche, rechteckFlaeche und kreisFlaeche, benutzen sie als Variablentyp die einfache Gleitkommazahl Float. Die Zahl pi ist schon vordefiniert und kann benutzt werden.
Übung 3 Implementieren Sie toLowerCase. Die Funktionalität ergibt sich aus der Analogie zu toUpperCase.
Übung 4 Implementieren Sie eine Funktion inc, die die Funktion succ nachempfindet und ein Funktion dec, die eine Zahl auf ihr um 1 dekrementiere Zahl abbildet. So sollten (dec.inc) und (inc.dec) jeweils die identische Abbildung sein.
Übung 5 Schreiben Sie eine Funktion istPalindrom, die einen eingegebenen String prüft, ob er ein Palindrom ist.
Übung 6 Programmieren Sie den Infix-Operator '+++' der die gleiche Funktionalität hat wie der normale Infix-Operator '+' für Integer-Werte.
Lösungen Zu den Lösungen
 

(C) Michael pohlig 2005 www.pohlig.de