Grundlegende Konzepte


... [Seminar "Haskell"] ... [Inhaltsübersicht] ... [zurück] ... [weiter] ...

Funktionen



Theoretische Grundlagen

Funktionen kommt eine zentrale Bedeutung in der funktionalen Programmierung zu. Während in der imperativen Programmierung die Ausführung eines Programmes die Abarbeitung von Anweisungssequenzen bedeutet, wird in der funktionalen Programmierung statt dessen eine Funktion ausgewertet.

Eine Funktion im mathematischen Sinne ist eine Zuordnungsvorschrift, die jedem Element aus dem Quellbereich genau ein Element aus dem Zielbereich zuordnet. Für die Funktion

square :: Integer -> Integer
square x = x * x

besteht sowohl der Quell- als auch der Zielbereich aus der Menge aller Integer-Werte. Jedem (definierten) Integer wird sein Quadrat zugeordnet, dem wird der undefinierte Integer zugeordnet. Der Typ der Funktion square ist Integer -> Integer.

Eine Funktion f :: A -> B liefert also Resultate vom Typ B in Abhängigkeit der Argumente vom Typ A. f(x) beschreibt das Resultat der Anwendung der Funktion f auf das Argument x. Folglich denotiert f(x) einen Wert vom Typ B, vorausgesetzt dass x einen Wert vom Typ A denotiert. Eine gleichwertige Schreibweise zu f(x) ist f x. Die Klammerung ist nur notwendig, wenn das Argument aus einem komplexeren Ausdruck besteht.

In der funktionalen Programmierung wird strenger zwischen der Funktion f und ihrer Anwendung auf ein Argument f(x) unterschieden als in den meisten mathematischen Ausarbeitungen. Wer funktionale Programmierung betreibt, begreift Funktionen als Werte, die sich durch nichts von anderen Werten unterscheiden. Sie könne zum Beispiel als Argumente an andere Funktionen übergeben werden oder selbst Funktionsresultate sein.


Gleichheit von Funktionen

Zwei Funktionen sind gleich, wenn gilt:

f x  =  g x     für alle x,

wenn also für gleiche Argumente stets gleiche Resultate erzielt werden. Wie diese Resultate funktionsintern berechnet werden, spielt für die Gleichheit keine Rolle. So werden die folgenden beiden Funktionen zur Verdoppelung des Argumentwertes als gleich betrachtet, obwohl die Umsetzung der Verdoppelungsoperation sich grundsätzlich unterscheidet:

doublep :: Integer -> Integer
doublep x = x + x

doublem :: Integer -> Integer
doublem x = 2 * x

Currying

Schon bei der Notation der Anwendung einer Funktion f auf ihr Argument x lassen sich durch Verwendung der Schreibweise f x statt f(x) eine Menge eigentlich überflüssiger Klammern im Code sparen. Dasselbe Ziel wird mit dem Currying (nach dem amerikanischen Logiker Haskell B. Curry) verfolgt. Ansatzpunkte für die Klammerersparnis sind hier strukturierte Funktionsargumente. So besteht zum Beispiel das Argument für die Funktion plus aus einem Integer-Paar:

plus :: (Integer, Integer) -> Integer
plus (x,y) = x + y

In der curried-Version nimmt die Funktion plusc die beiden Integer-Werte nicht mehr als Paar, sondern einzeln nacheinander entgegen:

plusc :: Integer -> (Integer -> Integer)
plusc x y = x + y

Genauer gesagt erwartet die Funktion plusc einen Integer-Wert x als Argument und liefert als Resultat eine Funktion plusc x, die wiederum einen Integer-Wert y als Argument erwartet und einen Integer-Wert (die Summe von x und y) als Resultat zurückgibt. Dabei tut die Funktion plusc x nichts anderes, als x zum übergebenen Integer-Argument zu addieren.

Neben der Reduzierung der Klammern im Code bietet das Currying einen zweiten Vorteil: Die curried-Version einer Funktion erwartet stets nur ein einfaches Argument und liefert als Resultat eine Funktion, die auch für sich genommen nützlich sein kann. So lässt sich zum Beispiel eine Inkrement-Funktion wie folgt formulieren:

inc = plusc 1

Zu jeder Funktion mit einem strukturierten Argument kann eine curried-Version erstellt werden. Entweder tut das der Programmierer selbst, indem er plusc x y = x + y definiert. Es existiert aber auch eine vordefinierte Funktion curry, mit deren Hilfe Funktionen mit einem strukturierten Argument in ihre curried-Version konvertiert werden können.

curry :: ((a,b) -> c) -> (a -> b -> c)
curry f x y = f (x,y)
Die Funktion plusc kann demnach auch durch
plusc = curry plus
definiert werden.


Funktionskomposition

Zu einem guten Programmierstil gehört auch, komplexe Funktionen als Komposition von einfacheren Teilfunktionen aufzubauen. Die Notation für die Komposition zweier Funktionen f und g in Haskell lautet:

(f . g) (x)

Das erste Klammerpaar ist notwendig, da sonst versucht würde, f mit g(x) zusammenzusetzen. g(x) ist jedoch keine Funktion, sondern die Anwendung einer Funktion auf ein Argument. Das zweite Klammerpaar kann (wie bei jeder Anwendung einer Funktion auf ein Argument) weggelassen werden:

(f . g) x

Die Bedeutung einer solchen Komposition zweier Funktionen liegt in der Hintereinanderausführung: Im Beispiel wird zuerst g auf x angewendet, auf das Resultat wird dann f angewendet:

(f . g) (x)   =   f (g (x))

Daraus folgt, dass nicht jedes Funktionspaar auf diese weise zusammengefügt werden kann. Der Resultattyp der zuerst angewandten Funktion muss mit dem Argumenttyp der danach angewandten Funktion übereinstimmen. Dies gilt natürlich auch bei der Komposition von mehr als zwei Funktiuonen. Für eine solche Kompositionssequenz gilt dann auch das Assoziativgesetz:

f . g . h   =   (f . g) . h   =   f . (g . h)

Infix- und Prefix-Notation

Operatoren sind in Haskell nichts anderes als Funktionen, die zwischen statt vor ihren beiden Argumenten stehen (Infix-Notation). Um Verwechslungen mit den in Prefix-Notation stehenden Funktionen zu vermeiden, werden für die Infix-Operatoren andere Funktionsnamen oder aber spezielle Symbole verwendet.
Beispiele:

3 + 4
3 < 4

Für viele einfache arithmethische Operationen stehen in Haskell bereits solche Infix-Operatoren zur Verfügung, so z.B. für Addition (+), Subtraktion (-), Multiplikation (*), Division(/) und Exponentialrechnung (^).

Um einen Operator in eine Funktion in Prefix-Notation zu verwandeln, wird der Operator in Klammern eingeschlossen.
Beispiele:

(+) 3 4
(<) 3 4

Umgekehrt können auch Prefix-Funktionen zu Infix-Funktionen werden, indem sie in rückwärts gerichtete Hochkommata (backquotes) eingeschlossen werden:

3 'plusc' 4 = plusc 3 4
15 'div' 4 = div 15 4

Sections

Noch mächtiger wird die Prefix-Notation von Operatoren, wenn zusätzlich zu dem Operator ein Argument mit in die Klammern eingeschlossen wird.
Beispiel:

(x+) y   =   x + y   =   (+y) x

Auf diese Weise lassen sich mit fast allen zweistelligen Operatoren Prefix-Funktionen kreieren.
Beispiele:

Verdopplungsfunktion: (*2)
Inkrementfunktion: (+1)
Kehrwertfunktion: (1/)

Prioritäten und Assoziativitäten

Die Prioritäten der wichtigsten Operatoren sind in der folgenden Tabelle dargestellt:

 1  Funktionsanwendung
 2  ^
 3  *, /, div, mod
 4  +,-
 5  >, <, >=, <=
 6  ==, /=
 7  &&
 8  ||

Es gibt jedoch Fälle, in denen diese Prioritätsregeln für eine eindeutige Auswertung eines Ausdruckes nicht ausreichen.
Beispiele:

5 - 4 - 2
A -> B -> C

Um in solchen Situationen Mehrdeutigkeiten auszuschliessen gelten folgende Assoziativitätsregeln:

Die obigen Beispiele werden also wie folgt ausgewertet:
(5 - 4) - 2
A -> (B -> C)

... [Seminar "Haskell"] ... [Inhaltsübersicht] ... [zurück] ... [weiter] ...          top of the page