F# - funktionales Pendant zu C#

Seite 2: Typsicherheit, deklarative Programmierung

Inhaltsverzeichnis

Der Aspekt wirkt zunächst ein wenig verwunderlich – schließlich handelt es sich bei C# ebenfalls um eine typsichere Sprache. Jedoch treibt die funktionale Programmierung das Konzept auf die Spitze, indem bei ihr jeder Ausdruck innerhalb eines Programms einem konkreten Typen entspricht. Die Typsicherheit stellt nicht nur die syntaktische, sondern auch die semantische Korrektheit der Software ansatzweise sicher.

F# setzt die Typisierung ebenfalls wesentlich konsequenter um als C#: Beispielsweise kann man mit der Sprache Zahlen eine Maßeinheit mitgeben: Eine Ganzzahl lässt sich demnach mit der Maßeinheit km versehen, eine andere mit Stunde, sodass sich durch Division der beiden eine Zahl der Maßeinheit km/Stunde ergibt. Erwartet ein anderer Programmteil die Geschwindigkeit nicht als km/Stunde, sondern als Meilen/Stunde, führt das in F# zu einem Typkonflikt:

let maxSpeed = 50.0<km/h>
let actualSpeed = 40.0<mile/h>

if(actualSpeed > maxSpeed) then
printfn "Sie fahren zu schnell!"

ERROR FS0001: Type mismatch.
Expecting a float<mile/h> but given a float<km/h>.
The unit of measure 'mile/h' does not match the unit of measure 'km/h'.

Typen können in der funktionalen Programmierung einem von zwei Konzepten folgen. Entweder handelt es sich um einen einfachen (wie einem der eingebauten Wertetypen) oder um einen komplexen Typ. Letzterer kann wiederum ein zusammengesetzter sein, der mehrere einfache oder komplexe kapselt, oder ein Auswahltyp, der einen der unterschiedlichen vorgegebenen Werte annehmen kann.

Interessant ist daran zweierlei: Einerseits lassen sich zusammengesetzte Typen nicht nur als Klassen implementieren, sondern beispielsweise auch als sogenannte Tupel darstellen, was es ermöglicht, mehr als einen Wert aus einer Funktion zurückzugeben. Andererseits bieten die meisten funktionalen Sprachen – darunter F# – direkte Unterstützung zur Arbeit mit Auswahltypen.

Seit Microsoft mit .NET 3.5 und C# 3.0 die LINQ-Technik eingeführt hat, ist die deklarative Entwicklung in der objektorientierten Microsoft-Welt salonfähig geworden: LINQ war lediglich der erste Ansatz – inzwischen haben mit XAML und den Parallel Extensions für .NET weitere deklarative Techniken das Microsoft-Feld betreten. Ihnen ist gemeinsam, dass sie nicht mehr im Detail beschreiben, wie eine Aufgabe, sondern was überhaupt zu lösen ist. In LINQ wird das besonders deutlich: Statt anzugeben, wie die einzelnen Elemente zu sortieren sind, gibt man lediglich mit der Erweiterungsmethode OrderBy an, dass und nach welchem Kriterium sortiert werden soll.

var processes =
from p in Process.GetProcesses()
orderby p.ProcessName
select p;

Wie OrderBy das intern umsetzt, bleibt als Blackbox verborgen. An der Stelle wird Komfort also definitiv höher bewertet als das letzte Quäntchen Performance zu erzielen – was im Regelfall allerdings eher hilfreich als störend sein dürfte.

Funktionale Programmierung erhebt die deklarative Entwicklung nun zum Standard: Funktionen beschreiben nicht mehr, wie die einzelnen Algorithmen im Detail arbeiten, stattdessen modellieren sie die Algorithmen:

let rec factorial(n) =
if (n <= 1) then
1
else
n * factorial(n - 1)

Die Was-Programmierung wird demnach zum Prinzip erhoben. Das erleichtert die Komponierbarkeit und Analyse von Algorithmen wesentlich, da diese sich bausteinartig zusammenstecken und miteinander kombinieren lassen.

Gerade für spezielle algorithmische Anwendungsgebiete wie Animationen eignet sich die deklarative Programmierung deutlich besser als die klassische, wodurch F# an der Stelle auch eine geeignete Sprache für XAML ist.

Der vierte Aspekt funktionaler Programmierung ist schließlich die Rekursion. Zwar gibt es sie in der nichtfunktionalen Programmierung ebenfalls, jedoch spielt sie in der funktionalen eine zentrale Rolle: Hier dient Rekursion nicht nur als Alternative zum iterativem Vorgehen, sie ist stattdessen die einzige Möglichkeit. Was sich zunächst wie eine Einschränkung anhört, stellt sich als probates Mittel zur effizienten Verarbeitung jeglicher Datenstrukturen dar:

let rec sumList list =
match list with
| [] -> 0
| head::tail -> head + sumList(tail)

Als Einwand gegen Rekursion mag zunächst der erhöhte Bedarf an Laufzeit und Speicher gelten, durch die Struktur der Sprache F# entsprechen viele rekursiv definierte Funktionen jedoch dem Schema der Endrekursion, sodass der Compiler sie in eine iterative Methode umwandelt und dadurch nicht nur beschleunigt, sondern auch speichersparend ausführt.

F# fehlen einige Sprachkonstrukte, die in Sprachen wie C# durchaus verbreitet sind: Am auffälligsten ist das bei Schleifen, die komplett mit der Rekursion nachzubilden sind. Allerdings weist F# Konstrukte auf, die anderen, nichtfunktionalen Sprachen in der Regel fehlen. Allen voran sei das Pattern Matching genannt, das bereits im letzten Code-Schnipsel Anwendung gefunden hat: Damit lassen sich verschiedene Ausdrücke auflisten, die Code zugewiesen bekommen haben. Essenziell ist dabei, dass die Ausdrücke jeweils unterschiedliche Instanzen repräsentieren, im Beispiel oben etwa die leere Liste und eine weitere, die in ein Kopfelement und eine verbleibende Liste (die gegebenenfalls wiederum der leeren Liste entspricht) aufgespalten wird.

Zunächst scheint das Sprachmerkmal nicht mehr als eine aufgebohrte switch-Anweisung zu sein, dem sich Ausdrücke anstelle von Konstanten übergeben lassen. Doch das Besondere am Pattern Matching in F# ist, dass der Compiler überprüft, ob alle Fälle beachtet wurden, und gegebenenfalls warnt, dass einer übersehen wurde. Das gewährleistet, selten benötigte, aber relevante Fälle aus Versehen nicht zu behandeln.

Eine weitere Besonderheit von F# ist, dass die Sprache Funktionen letztlich nicht anders handhabt als andere Typen. Das bedeutet insbesondere, dass sich Funktionen als Parameter und Rückgabewerte von Funktionen verwenden lassen. Die Funktion, die andere entgegen nimmt oder zurückgibt und sie verarbeitet, bezeichnet man in dem Fall als "Higher Order Function":

let twice (input:int) f  = f(f(input))

twice 2 (fun n -> n * n)

Das Konzept gibt es in C# in eingeschränkter Form, indem Delegates über- oder zurückgegeben werden. F# treibt es jedoch weiter. Beispielsweise ist es möglich, Funktionen zu verketten, wobei sich die Anzahl der Parameter verringern lässt, indem man einige Parameter mit Standardwerten vorbelegt. Das Verfahren heißt "Partial Function Application".

let add a b = a + b
let addTen = add 10

List.map addTen [ 1 .. 10 ]

Ein Begriff, der in dem Zusammenhang ebenfalls häufig anzutreffen ist, ist das "Currying". Damit bezeichnet man ein Verfahren, das eine Funktion mit n Parametern abbildet, die lediglich den ersten Parameter entgegen nimmt und eine weitere Funktion zurückliefert, die n - 1 Parameter erwartet.