Das Nachschlagewerk für alte, neue und exotische Sprachen

F# – Die funktionale Alternative zu .NET
Kommentare

Schon seit Visual Studio 2010 ist F# Standardbestandteil der Microsoft-Entwicklungswelt. Davor wurde die Sprache bereits seit 2005 entwickelt, basierend auf syntaktischen Grundlagen, die aus bestehenden funktionalen Hybridsprachen übernommen wurden. Somit ist eigentlich nichts Neues an dieser Sprache – und dennoch ist sie für viele .NET-Programmierer noch immer eine Unbekannte, ein Werkzeug, mit dessen Einsatz man sich noch nicht im Detail beschäftigt hat.

Schon seit frühen Zeiten seiner Entwicklung war F# bei bestimmten Anwendergruppen auf reges Interesse gestoßen. Als eine Sprache, die sich funktionale Einflüsse zumindest als einen wichtigen Punkt auf die Fahne schrieb, bot sich F# offensichtlich für diejenigen algorithmisch geprägten Projekte an, die schon immer gern funktional geführt wurden. Aber mit dem Hintergrund der breit gefächerten Funktionalität des .NET Framework gab es hier etwas, das keine andere funktionale Sprache bieten konnte: die Fähigkeit, Anwendungen für die verschiedensten Zielsysteme zu schreiben, mit und ohne grafischer Benutzerschnittstelle, Server oder Client, mit einfachstem Datenbankinterface, als Webanwendung oder für Mobilplattformen. Gleichzeitig ist F# selbst hybrid in seinen Ansätzen, sodass die Interaktion mit all diesen verschiedenen Subsystemen so einfach ist, wie Sie es sich nur vorstellen können.

Fortschritte

Seither hat sich die Entwicklung weiter fortgesetzt – sowohl im Bereich der Sprachen als auch für .NET insgesamt. Für F# bedeutet das hauptsächlich, dass seine Ideen heute relevanter sind als je zuvor. In C# und VB.NET haben seit Visual Studio 2008 immer mehr funktionale Ideen Einzug gehalten, in großem Umfang mit der Sprachunterstützung für das sehr funktional geprägte LINQ, das in .NET 3.5 eingeführt wurde. Parallelisierbarkeit ist ein Thema, das aufgrund der typischen Architekturen aktueller Prozessoren fast jede Software betrifft und über das viele Programmierer in die Welt der funktionalen Programmierung finden, da sie sich von puren Funktionen und unveränderbaren Daten neue Lösungswege versprechen. F# bietet als Programmiersprache den besten Weg zu diesen funktionalen Zielen, der Ihnen auf der .NET-Plattform offensteht, während die Mittel der Objektorientierung, mit denen Sie vertraut sind, nicht zurückstehen müssen.
Die Vergangenheit hat gezeigt, dass F# eine Plattform der Entwicklung in mehr als einer Hinsicht ist: Sprachfeatures aus F# haben oft den Weg in andere .NET-Sprachen gefunden und maßgeblich dazu beigetragen, dass wichtige Neuerungen in die Plattform integriert wurden. Ob Sie dabei an große Dinge denken, wie Generics in .NET 2.0, oder an kleinere, wie die Einführung der Tupeltypen in .NET 4.0 – F# spielt in diesen Bereichen immer wieder eine Vorreiterrolle. Die in C# 5.0 neuen Async-Schemata gibt es konzeptionell in F# schon viel länger, und die in F# 3.0 eingeführte Funktionalität zu Information Rich Programming verspricht wiederum neue Fähigkeiten, die womöglich in Zukunft in Richtung C# oder VB.NET migriert werden könnten. Selbst das aktuell diskutierte Roslyn-Projekt, in dem Microsoft den Sprachen VB.NET und C# zu besserem Verständnis ihrer selbst verhelfen will, verleitet zum Nachdenken, da der Ursprung dieser Ideen in der Funktion eval liegt, die vor vielen Jahrzehnten von der funktionalen Sprache Lisp eingeführt wurde.

Hallo Welt

Als erstes Beispiel für die Sprache F# finden Sie hier eine Implementation des wohl ältesten Programmierbeispiels der Welt: das Hallo-Welt-Programm. Ein kleiner Zusatz ist auch enthalten, indem das Programm den Anwender nach seinem Wohlbefinden fragt und eine entsprechende Ausgabe erzeugt. Schauen Sie sich diesen Quelltext an:

printfn "Hello world!"
printfn "How are you doing? "
printf " -> "

let response = System.Console.ReadLine()

printfn "Great to you hear you're %s!" response

Das übliche „Hallo Welt“ lässt sich natürlich bereits mit der ersten Zeile dieses Beispiels umsetzen. Dort wird lediglich eine eingebaute Funktion printfn verwendet, die einen formatierten Text auf der Konsole ausgeben kann. Danach werden einige weitere Ausgaben erzeugt und ein Text mithilfe der Standard-.NET-Funktion ReadLine abgefragt. In der letzten Zeile können Sie auch den Aspekt der Formatierung erkennen, die von printfn unterstützt wird: der Platzhalter %s wird bei der Ausgabe durch den zuvor eingegebenen Text ersetzt. Nach einem Programmlauf könnte die gesamte Ausgabe wie folgt aussehen:

Hello world!
How are you doing?
 -> very well
Great to you hear you're very well!

Bereits an diesem einfachen Beispiel lassen sich gewisse Aspekte der Sprache F# erkennen: Zunächst fällt auf, dass das Programm einen linearen, skriptartigen Charakter hat. Es gibt keine syntaktischen Konstrukte, wie etwa eine Main-Methode oder eine bestimmte Klasse, die zur Ausführung unbedingt erforderlich wären. Auch die Syntax der einzelnen Aufrufe an printfn ist sehr einfach, da sie ohne Klammern oder Kommata in der Parameterliste auskommt. Die „Variable“ response hat anscheinend keinen bestimmten Typ – tatsächlich hat sie einen Typ, aber dieser wird vom Compiler automatisch hergeleitet. Schließlich ist noch zu sehen, dass der Übergang zum Laufzeitsystem von .NET fließend ist, da zum Einlesen der Benutzereingabe ein simpler Aufruf an eine .NET-Klasse ausgeführt wird.

Werte

Ebenso wie die meisten Programmiersprachen kennt auch F# die Idee des „benannten Werts“. In vielen Sprachen redet man von „Variablen“, die „Werte“ speichern. Solche Variablen haben gewöhnlich einen Typ, der den Datentyp des Werts bestimmt, der in ihnen gespeichert werden kann. Es ist ebenso üblich, dass der Wert einer Variablen sich während des Programmablaufs beliebig, und beliebig häufig, verändern kann – daher stammt die Bezeichnung „Variable“.
Variablen im beschriebenen Sinne gibt es in F# auch, allerdings werden Sie stattdessen meistens mit unveränderbaren Werten arbeiten. Die Idee, Werte nach ihrer anfänglichen Zuweisung nicht mehr zu ändern (englisch: Immutability), wird von vielen funktionalen Programmiersprachen umgesetzt, und stellt auch in F# den Standard dar. So ist der Wert response im vorherigen Beispiel nicht veränderbar und sollte deshalb nicht als Variable bezeichnet werden. Eine Wertezuweisung geschieht immer mit dem Schlüsselwort let, und der Compiler ermittelt den Typ des gewünschten Werts automatisch.
F# ist eine statisch typisierte Sprache. Daraus ergibt sich unter anderem, dass alle Werte Typen haben, die bereits zur Zeit des Compilerlaufs feststehen. Auf den ersten Blick hingegen scheinen im Code die Werte allerdings typlos zu sein. Der Grund hierfür ist, dass F#, ganz in der Tradition funktionaler Programmiersprachen, sehr stark auf die automatische Herleitung von Typen (englisch: type inference) durch den Compiler setzt. Auch in anderen .NET-Sprachen hat diese Technik mittlerweile Einzug gehalten, allerdings in relativ stark eingeschränkter Form. In F# ist die Umsetzung so leistungsfähig, dass eine explizite Erwähnung eines Typs im Code eine seltene Ausnahme darstellt.

Aufmacherbild: 3d man holding an upward pointing red arrow while standing in amongst a number of white arrows in a concept of success and achievement von Shutterstock / Urheberrecht: PlusONE

[ header = Seite 2: Funktionen ]

Funktionen

Auch zur Deklaration von Funktionen verwenden Sie in F# das Schlüsselwort let – ein erster Hinweis auf die Tatsache, dass Funktionen auch „nur“ Werte darstellen. Hier ist eine einfache Funktion:

let square x = x * x

Diese Zeile deklariert eine Funktion namens square mit einem Parameter x und einem Rückgabewert, der berechnet wird aus x multipliziert mit sich selbst. Die Benennung einfacher Funktionen in Kleinbuchstaben oder zumindest mit einem kleinen Buchstaben am Anfang ist in F# üblich. Hier ist eine Funktion mit zwei Parametern:

let add x y = 
  x + y

Zwei interessante Details zu diesen Funktionsdeklarationen sind Ihnen vielleicht bereits aufgefallen: Als Erstes sind da die Rückgabewerte, die ohne explizites return erzeugt werden. Die zugrunde liegende Annahme ist, dass Funktionen immer einen Rückgabewert haben – dafür ist deswegen keine besondere Syntax erforderlich. Der Wert des letzten Ausdrucks in einer Funktion ist automatisch auch der Wert, der zurückgegeben wird. In der Funktion square ist der letzte Ausdruck x * x, in der Funktion add ist es x + y. Das Resultat dieser Rechenoperationen wird damit jeweils automatisch aus der Funktion zurückgegeben.
Das zweite Detail ist, dass die Funktion add, anders als square, nicht in einer einzelnen Zeile deklariert wird. Es ist zwar üblich, Funktionen kurz zu deklarieren und sich auch der syntaktischen Fähigkeiten der Sprache zu bedienen, um Code kurz und prägnant zu gestalten, aber die Sprache bietet auch alle notwendigen Mittel, um komplexe Funktionen übersichtlich zu gestalten und zu formatieren. Wichtig ist allerdings, dass es keine besonderen Schlüsselwörter oder syntaktische Elemente zur Blockbildung gibt. Stattdessen werden Blöcke einzig durch die Einrückung des Codes erzeugt.
Zum Umgang mit Funktionen fehlt bisher noch ein wichtiges Element: das Aufrufen von Funktionen. Hier sind zwei Beispiele für den Aufruf der Funktionen square und add:

let square11 = square 11
let add5and3 = add 5 3

Wie Sie sehen, werden die Parameter für die Funktionsaufrufe jeweils einfach durch Leerzeichen getrennt hinter dem Namen der Funktion notiert. Es gibt keine weiteren syntaktischen Elemente für die Parameterliste selbst, wie etwa Klammern oder trennende Kommata. Der Rückgabewert eines Funktionsaufrufs kann natürlich wiederum in einem benannten Wert abgelegt werden.
Interessanter wird es, wenn Funktionsaufrufe kombiniert werden. Sie haben bereits in der Einführung die Hilfsfunktion printfn kennengelernt, mit der Informationen auf der Konsole ausgegeben werden können. Die folgende Zeile verwendet diese Hilfsfunktion, um das Resultat eines anderen Funktionsaufrufs direkt auszugeben:

printfn "Square 11: %d" (square 11)

Beachten Sie, dass in einem solchen Szenario die Klammern die Priorisierung, die Evaluierungsreihenfolge der Ausdrücke definieren. Sie sind kein syntaktisches Element des Funktionsaufrufs selbst. Merken Sie sich diesen Punkt gut. Es wird ein wenig dauern, bis Sie sich daran gewöhnt haben – nach einiger Zeit jedoch werden Sie diesen Ansatz ganz natürlich finden. Hier sind zwei weitere gültige Aufrufe an die beiden bisher beschriebenen Funktionen:

printfn "Add 5 and 3: %d" (add 5 3)
printfn "Add 5 and the square of 11: %d" (add 5 (square 11))

Daten speichern

Neben der Verarbeitung von Informationen ist deren Haltung und Speicherung die zweite wichtige Disziplin, in der eine Programmiersprache dem Entwickler zur Hand gehen sollte. In F# gibt es in diesem Bereich eine Reihe verschiedener Ansätze, und einer der leistungsfähigsten ist der, direkt mit Datentypen aus dem .NET Framework zu arbeiten:

let list = new System.Collections.Generic.List<int>()
list.Add(10)
list.Add(15)
printfn "First element: %d" (list.[0])
printfn "Second element: %d" (list.[1])

Die Syntax für den Umgang mit .NET-Typen sieht auf den ersten Blick womöglich etwas ungewohnt aus, aber letztlich sind in diesem Bereich keine Grenzen gesetzt. Allerdings bietet F# als funktionale Sprache auch einige „eigene“, in diesem Umfeld übliche Datentypen an, die in der Verarbeitung durch funktionale Algorithmen einfacher zu verwenden sein können. Außerdem gibt es sehr kurze und einfache syntaktische Darstellungen von Daten im Code. Hier einige kurze Beispiele:

// Zwei Tupel unterschiedlicher Länge: 
let t1 = 12, 5, 7
let t2 = "hi", true

Tupel sind kombinierte Datentypen, die sich aus unterschiedlichen anderen Datentypen zusammensetzen. Der F#-Compiler leitet den kombinierten Typ automatisch her, indem die einzelnen Teiltypen aufgelistet werden, getrennt durch *. Der Typ von t1 ist also int * int * int, und der Typ von t2 ist string * bool.

// Dies sind Werte von einem Optionstyp:
let o1 = Some(5)
let o2 = None

Die Optionstypen stellen die F#-Implementation eines oft benötigten Schemas dar, nämlich der Idee, dass bestimmte Werte manchmal gar nicht gegeben sein könnten. In C-verwandten Sprachen wird dafür oft null oder ein ähnlicher Platzhalter eingesetzt, was aber auch zu Schwierigkeiten führen kann, wie in C# durch die Aufteilung in Wert- und Referenztypen. Viele funktionale Sprachen verwenden für denselben Zweck Mechanismen wie die F#-Optionstypen.

// Diese beiden Zeilen erzeugen Listen:
let empty = []
let intList = [12;1;15;27]

Listen, Sequenzen und ähnliche Datentypen sind in funktionalen Sprachen ebenfalls oft als syntaktische Elemente direkt im Quelltext unterstützt (Listing 1).

// Hier zwei Discriminated Unions
// Dieser Typ ist kompatibel mit einem Standard .NET enum:
type MyEnum = 
  | First = 0
  | Second = 1

let enum1 = First
let enum2 = Second

// Es ist auch möglich, zusätzlich Daten abzulegen:
type Product = 
  | OwnProduct of string
  | SupplierReference of int

let product1 = OwnProduct("Bread")
let product2 = SupplierReference(14)

Discriminated Unions sind Datentypen, die sich ähnlich den Tupeln aus bestehenden Typen zusammensetzen. Allerdings können sie komplexer sein, und die Benennung der unterschiedlichen „Fälle“ macht sie einfacher in der Anwendung.

Komplexität ganz einfach

Indem Sie Kombinationen dieser beschriebenen Datentypen verwenden, lassen sich auch komplexe Daten recht einfach modellieren. Zum Beispiel können Sie einen Datentyp für eine generische verkettete Liste in F# so deklarieren:

type 'a List = E | L of 'a * 'a List

Die Syntax ‚a, die hier verwendet wird, bezeichnet einen generischen Parameter. ‚a List stellt also den Datentyp dar, der deklariert wird: Er heißt List und benötigt einen generischen Parameter, mit dem Namen ‚a. Der Datentyp ist eine Discriminated Union, die beiden möglichen „Fälle“ sind E und L of ‚a * ‚a List. Der Fall E (Empty) ist der, wo das Ende der verketteten Liste erreicht wird, der Fall L (Leaf) ist der, wo auf ein Datenelement sowie auf eine Liste von Folgeelementen verwiesen wird. Daher speichert der Fall L Daten vom Tupeltyp ‚a * ‚a List. Dieser Tupeltyp besteht aus den beiden Elementen ‚a (das ist der generische Typ des einzelnen Listenelements, also der Datenwert selbst) und ‚a List (der Rest der Liste, dies ist rekursiv derselbe Typ, der soeben deklariert wird).

Fazit

F# kann viel, und dieser Artikel konnte lediglich die Grundzüge beschreiben und einige komplexere Beispiele bieten, die Lust auf mehr machen sollen. Hoffentlich ist Ihr Interesse an dieser alternativen Programmiersprache für .NET geweckt. Es gibt noch viel zu entdecken, aber das Grundziel ist einfach: Ihnen als Programmierer das leistungsstärkste Werkzeug an die Hand zu geben, das Sie haben können.

Buchtipp

Autor Oliver Sturm
Titel    F#
Untertitel Einstieg und praktische Anwendung
Seiten 188
Preis 29,90 Euro
Verlag entwickler.press
Jahr 2012
ISBN 978-3-86802-083-0
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -