2 Funktionen im Detail
Sie sind sicher nicht überrascht zu hören, dass funktionale Programmierung in F#, wie auch in anderen Sprachen, die ganz oder teilweise der funktionalen Idee verhaftet sind, auf modularen Funktionen basiert. Manche solche Funktionen schreiben Sie selbst, andere sind Bestandteil der F#-Laufzeitumgebung oder sogar des .NET Frameworks.
Jenseits der „einfachen“ Deklaration und Implementierung von Funktionen sind Techniken notwendig, um den Aspekt der Modularisierung zu realisieren. Wenn Funktionen als Bausteine dienen sollen, muss es möglich und sogar einfach sein, sie zusammenzufügen, aus ihnen neue Funktionen zu bauen. Die dazu dienlichen Techniken werden gern unter der Bezeichnung Function Construction zusammengefasst.
Dieses Kapitel deckt beide Bereiche ab. Sie werden sehen, welche detaillierten Fähigkeiten F# zur Deklaration und Implementierung von Funktionen bietet und wie diese mit Techniken der Function Construction zu größeren Elementen zusammengebaut werden können.
2.1 Deklarationen
Im ersten Kapitel haben Sie bereits gesehen, wie Sie in F# Funktionen deklarieren können. Hier ist noch einmal die einfache Funktion add, die bereits als Beispiel gezeigt wurde:
let add x y = x + y
Listing 2.1: Die einfache Funktion „add“
Sie haben auch gesehen, dass die Funktionsimplementierung umgebrochen und über mehrere Zeilen gestreckt werden kann, wobei die Einrückung die Struktur der Funktion repräsentiert. Etwa so:
let add x y =
x + y
Listing 2.2: „add“, deklariert mit Umbruch
Wie andere moderne Programmiersprachen bietet F# die Möglichkeit, Funktionen auch anonym, also namenlos, zu erzeugen. C# etwa bietet hierzu zwei unterschiedliche Syntaxvarianten, die anonymen Funktionen und die Lambda-Ausdrücke. Letztere sind in den meisten anderen Sprachen, besonders denen aus der funktionalen Sprachwelt, der „normale“ Mechanismus zur Erzeugung von anonymen Funktionen. F# ist hier keine Ausnahme: mit dem Schlüsselwort fun (kurz für Function, nicht als Übersetzung von Spaß zu verstehen) erzeugen Sie einen Lambda-Ausdruck:
let add' = fun x y -> x + y
Listing 2.3: „add’“, als Lambdaausdruck implementiert
Die Syntax add', gesprochen add Strich, die hier verwendet wird, um eine Variante der Funktion add zu erzeugen, haben Sie schon zuvor gesehen. Mit dem Schlüsselwort fun wird die Erzeugung eines Lambdaausdrucks eingeleitet. Eine Parameterliste für den Ausdruck folgt dem Schlüsselwort, und auf der rechten Seite des Operators ->, gesprochen goes to, findet sich die Implementierung, der Body, des Ausdrucks.
Hinweis: Sicherlich haben Sie bemerkt, dass die Funktion, die hier in Form des Lambdaausdrucks deklariert wird, in dem Wert add‘ abgelegt und somit gewissermaßen mit einem Namen versehen wird. Man könnte also darüber streiten, ob in diesem Fall von einer anonymen Funktion die Rede sein darf ? schließlich steht die Funktion unter dem Namen add‘ zum Aufruf zur Verfügung. Allerdings nennt man oft auch in dieser Situation die Funktion anonym, da kein Name dafür innerhalb der syntaktischen Sprachelemente der Deklaration festgelegt wurde. Die Zuweisung an den Wert add‘ geschieht hier nur, damit ein vollständiges, gültiges Statement im Sinne von F# daraus wird. Praktisch werden Lambdaausdrücke oft auch ohne Zuweisung erzeugt, wie Sie weiter unten sehen werden.
Offensichtlich ist der syntaktische Unterschied zwischen einer „normalen“ Funktionsdeklaration und der eines Lambdaausdrucks in F# nicht groß. Tatsächlich sind auch die Ergebnisse beider Techniken kaum zu unterscheiden. Wenn Sie die beiden Zeilen in Visual Studio eingeben und sich die Meldung von IntelliSense mit der Maus ansehen, sehen Sie dies:
val add: int -> int -> int
val add': int -> int -> int
Listing 2.4: Signaturen von „add“ und „add’“ im Vergleich
In diesem Fall, in dem auch der Lambdaausdruck unmittelbar einem Wert zugewiesen wird, erzeugt der Compiler für beide Funktionen tatsächlich genau denselben Code. Das ist grundsätzlich unterschiedlich von der Vorgehensweise anderer .NET-Sprachcompiler, wie etwa C#, wo vollwertige benannte Methoden prinzipiell anders gehandhabt werden als anonyme Methoden.
2.2 Funktionen sind Werte
Sie haben es sich anhand der bisherigen Beispiele dieses Kapitels vielleicht schon gedacht: Funktionen sind in F# auch „nur“ Werte, die ebenso wie andere Werte abgelegt, aber auch als Parameter übergeben oder als Rückgabewerte zurückgegeben werden können. Dabei sorgt die Unterstützung von Typherleitung dafür, dass dies unglaublich einfach geht. Sehen Sie sich dieses Stück Code an:
let checkThis item f =
if f item then
printfn "HIT"
else
printfn "MISS"
checkThis 5 (fun x -> x > 3)
checkThis "hi there" (fun x -> x.Length > 5)
Listing 2.5: Eine Funktion als Parameter übergeben
Die Funktion checkThis hat zwei Parameter mit den Namen item und f. Es gibt keine Typannotationen, sodass der Compiler selbst alle Typen herleiten muss. In IntelliSense wird die Signatur der Funktion so angezeigt:
val checkThis: 'a -> ('a -> bool) -> unit
Listing 2.6: Die Signatur von „checkThis“
Der Typ des ersten Parameters wird mit 'a angegeben, wobei es sich um einen generischen Typparameter handelt. Der Compiler hält es also nicht für nötig, irgendwelche Einschränkungen für den Typ zu definieren. Wichtig: Das bedeutet nicht, dass der Compiler nichts über den Typ weiß. Die Funktion tut nichts mit dem Wert item, weshalb dessen Typ besonderen Anforderungen genügen müsste. F# betreibt in diesem Fall „automatische Verallgemeinerung“ (englisch: Automatic Generalization) und beschreibt den Typ so umfassend wie möglich, also in diesem Fall mit einem offenen generischen Parameter.
Der zweite Parameter hat den Typ 'a -> bool. Die Anwesenheit des goes-to-Operators deutet darauf hin, dass f als Funktionstyp erkannt worden ist. Diese Tatsache leitet der Compiler aus der Verwendung von f in dem if-Ausdruck her. In dem Ausdruck if f item then... kann f syntaktisch nur für eine Funktion stehen, die einen Parameter entgegennimmt (dieser muss vom selben Typ sein wie der zuvor besprochene Funktionsparameter item) und bool als Rückgabetyp erzeugt (Letzteres, weil f nur mit einem bool-Rueckgabetyp in dem if-Ausdruck verwendet werden kann).
Der letzte Teil der Funktionssignatur von checkThis ist das unit am Ende der goes-to-Kette. unit in F# ist ähnlich wie etwa void in C#: ein Typ, der keiner ist. Wenn unit in Signaturen vorkommt, bedeutet dies, dass eben kein Rückgabewert erzeugt wird, oder auch, in anderen Situationen, dass keine Parameter übergeben werden. Wenn Sie gern explizite Typannotationen verwenden möchten, können Sie die Funktion auch so deklarieren:
let checkThis' (item: 'c) (f: 'c -> bool) : unit =
if f item then
printfn "HIT"
else
printfn "MISS"
Listing 2.7: „checkThis’“, deklariert mit Typannotationen
Damit nehmen Sie dem Compiler die Arbeit ab, die Typen selbst herzuleiten. Sie verlieren aber auch die Flexibilität, die sich aus automatischer Typherleitung ergibt, gar nicht von der Kürze des Codes zu reden. Die meisten F#-Programmierer verwenden Typannotationen nur, wenn es notwendig ist ? hierfür gibt es verschiedene Gründe, die in späteren Beispielen noch erwähnt werden. Im Allgemeinen überlässt man aber gern dem Compiler die Arbeit, solange die Herleitung nicht irrtümlich falsche Typen annimmt.
2.3 Function Construction
Function Construction, deutsch „Funktionskonstruktion“, bezeichnet eine Familie von Techniken, mit deren Hilfe funktionale Programmierer aus existierenden Funktionen neue bauen. Die Idee ist, dass die Funktion zum Baustein wird. Objektorientierte Programmierer verwenden Klassen und Interfaces als Bausteine; wenn Gedanken von effizienter Wiederverwendung einmal implementierter Funktionalität in der funktionalen Welt umgesetzt werden sollen, tritt die Funktion an die Stelle des wiederverwendbaren Moduls. In den folgenden zwei Abschnitten lernen Sie zwei wichtige Techniken kennen, mit denen F# Ihnen diese Arbeit erleichtert.
2.3.1 Partial Application
Die folgenden beiden Varianten der einfache Funktion add haben Sie schon gesehen:
let add x y = x + y
let add‘ = fun x y -> x + y
Listing 2.8: Varianten von „add“
Wie Sie wissen, werden beide Deklarationen vom Compiler mit derselben Signatur hergeleitet:
int -> int -> int
Es gibt noch eine dritte Variation, mit der sich ebenfalls dieselbe Signatur erzeugen lässt:
let add'' x = fun y -> x +...