Einführung in F#
Funktionale Programmierverfahren in .NET Framework
Ted Neward
Themen in diesem Artikel:
- Installieren von F#
- Grundlagen der F#-Sprache
- .NET-Interoperabilität
- Asynchrones F#
|
In diesem Artikel werden folgende Technologien verwendet:
.NET Framework, F#
|

Inhalt
F# bietet als neues Mitglied der Microsoft® .NET Framework-Familie Typsicherheit, Leistung sowie die Funktionen einer Skriptsprache – und das alles innerhalb der .NET-Umgebung. Diese funktionale Sprache wurde von Don Syme von Microsoft Research als syntaxkompatible OCaml-Variante für CLR erstellt, die sich jedoch schnell aus der Testumgebung in die Produktionsumgebung begeben hat.
Während sich Konzepte der funktionalen Programmierung immer häufiger über Technologien wie .NET-Generika und LINQ in etabliertere Sprachen wie C# und Visual Basic® einschleichen, ist die Sichtbarkeit von F# innerhalb der .NET-Community in einem derartigen Ausmaß gewachsen, dass Microsoft im November 2007 die Aufnahme von F# in die traditionelle Gruppe unterstützter .NET-Programmiersprachen ankündigte.
Jahrelang wurde der Bereich funktionaler Sprachen (ML, Haskell und so weiter) als geeigneter für die akademische Forschung statt für die professionelle Entwicklung betrachtet. Es ist keinesfalls so, dass diese Sprachen nicht interessant wären. Einige wichtige Verbesserungen an .NET – Generika, LINQ, PLINQ und Futures zum Beispiel – stammen aus der Anwendung funktionaler Programmierungskonzepte auf Sprachen, für die dies etwas ganz Neues war. Das mangelnde Interesse an diesen Sprachen war bislang eher darauf zurückzuführen, dass die Plattformen, auf die sie abzielten, für Entwickler von Windows®-Programmen von geringer Bedeutung waren. Sie ließen sich nicht gut in die zugrunde liegende Plattform integrieren, oder sie unterstützten nicht wichtige Funktionen wie relationalen Datenbankzugriff, XML-Analyse und prozessexterne Kommunikationsmethoden.
Die CLR und ihr Ansatz, viele Sprachen mit einer Plattform zu kombinieren, brachten es zwangsläufig mit sich, dass weitere dieser Sprachen ihren Weg in die Welt der Windows-Entwicklung fanden. Ebenso unvermeidlich war es, dass diese auch Einfluss auf praktizierende Programmierer ausüben würden. F# ist eine solche Sprache. In diesem Artikel sollen einige der zugrunde liegenden Konzepte sowie Vorteile von F# erläutert werden. Um Ihnen den Einstieg in F# zu erleichtern, erhalten Sie anschließend eine detaillierte Anleitung für die Installation und das Schreiben einfacher Programme.
Gründe für das Verwenden von F#
Ein kleiner Teil der .NET-Programmierer weiß, dass das Erlernen einer funktionalen Sprache für .NET Framework ein Schritt in die richtige Richtung für das Schreiben leistungsfähiger Software ist. Für die anderen liegen die Beweggründe, F# zu erlernen, im Verborgenen. Inwiefern profitieren Entwickler von F#?
In den letzten drei Jahren ist das Schreiben sicherer paralleler Programmen ein Hauptanliegen geworden, da Mehrkern-CPUs eine immer breitere Anwendung finden. Funktionale Sprachen helfen Entwicklern beim Unterstützen von Parallelität, indem unveränderbare Datenstrukturen unterstützt werden, die zwischen Threads und Computern übertragen werden können, ohne Probleme bei Threadsicherheit und atomarem Zugriff zu verursachen. Funktionale Sprachen erleichtern auch das Schreiben besserer parallelitätsfreundlicher Bibliotheken, wie z. B. asynchroner F#-Workflows, auf die unten genauer eingegangen wird.
Auch wenn Programmierer, die über beide Ohren in der objektorientierten Entwicklung stecken, möglicherweise eine andere Ansicht haben, sind funktionale Programme häufig für bestimmte Arten von Anwendungen einfacher zu schreiben und zu verwalten. Denken Sie zum Beispiel an das Schreiben eines Programms zum Konvertieren eines XML-Dokuments in ein anderes Datenformat. Sicherlich wäre es möglich, ein C#-Programm zu schreiben, dass das XML-Dokument analysiert und eine Vielzahl an Anweisungen zum Bestimmen der an unterschiedlichen Stellen im Dokument zu ergreifenden Aktionen anwendet. Ein weit besserer Ansatz besteht jedoch darin, die Transformation als XSLT-Programm (Extensible Stylesheet Language Transformations) zu schreiben. Es verwundert nicht, dass XSLT und SQL eine Vielzahl an Funktionen besitzen.
In F# wird dringend von der Verwendung von Nullwerten abgeraten, während die Verwendung unveränderbarer Datenstrukturen unterstützt wird. Diese Kombination kann die Häufigkeit von Programmierfehlern verringern, indem weniger Code für Sonderfälle benötigt wird.
In F# geschriebene Programme sind in der Regel auch kürzer. Er wird tatsächlich weniger getippt und typisiert: Es sind weniger Tastaturanschläge nötig, und es gibt weniger Orte, an denen der Compiler Anweisungen zum Typ der Variablen, der Argumente oder des Rückgabetyps erhalten muss. Dadurch kann die Menge an zu verwaltendem Code erheblich verringert werden.
F# besitzt eine ähnliches Leistungsprofil wie C#. Das Leistungsprofil von F# ist jedoch im Vergleich zu ähnlichen prägnanten Sprachen, insbesondere den dynamischen Sprachen und den Skriptsprachen, viel besser. Darüber hinaus enthält F#, wie viele dynamische Sprachen, die Tools, mit denen Sie Daten durch das Schreiben von Programmfragmenten und die interaktive Ausführung untersuchen können.
Installieren von F#
F# steht als kostenloser Download unter
research.microsoft.com/fsharp/fsharp.aspx zur Verfügung und installiert nicht nur alle Befehlszeilentools, sondern auch ein Visual Studio
®-Erweiterungspaket, das farbige Syntaxhervorhebung, Projekt- und Dateivorlagen (einschließlich eines sehr ausführlichen Beispiels für F# als Kurzanleitung) sowie IntelliSense
®-Unterstützung bietet. Es ist auch möglich, eine interaktive F#-Shell innerhalb von Visual Studio auszuführen. Auf diese Weise können Entwickler Ausdrücke aus Quelldateifenstern auswählen, sie ins interaktive Shellfenster einfügen und Ergebnisse aus dem Codeausschnitt sofort anzeigen, und zwar innerhalb eines Fensters, das mit einem erweiterten Direktfenster vergleichbar ist.
Beim Verfassen dieses Artikels wurde F# als externes Tool innerhalb von Visual Studio ausgeführt. Das bedeutet, dass die von C# oder Visual Basic gewohnte nahtlose Ausführung zum Teil fehlt. F# bietet u. A. auch keine Unterstützung für ASP.NET-Seitendesigner. (Dies bedeutet jedoch keinesfalls, dass F# nicht in ASP.NET verwendet werden kann. Die Visual Studio-Unterstützung für F# bietet nur nicht die gleiche Art standardmäßiger Drag & Drop-Entwicklungsfunktionalität für F# wie für C# und Visual Basic.)
Die aktuelle Version von F# kann jedoch überall verwendet werden, wo auch andere .NET-kompatible Sprachen verwendet werden können. Auf den nächsten Seiten sind einige Beispiele aufgeführt.
Hello, F#
Alle Sprachen werden über das universelle „Hello, World“-Programm vorgestellt. F# unterscheidet sich in dieser Hinsicht nicht:
Dieses Beispiel mag zwar eine kleine Enttäuschung sein, zeigt aber, dass F# zur Kategorie der Sprachen gehört, die keinen expliziten Einstiegspunkt benötigen (im Unterschied zu C#, Visual Basic und C++/CLI). Die Sprache verwendet die erste Zeile des Programms als Einstiegspunkt und wird von dort ausgeführt.
Für diese Ausführung stehen dem F#-Entwickler zwei Möglichkeiten zur Verfügung: kompiliert oder interpretiert. Das Ausführen innerhalb des F#-Interpreters (fsi.exe) ist ganz unkompliziert. Führen Sie einfach fsi.exe von der Befehlszeile aus, und geben Sie die obige Zeile in die sich ergebende Eingabeaufforderung ein (siehe Abbildung 1).
Abbildung 1 Ausführen von „Hello, World“ innerhalb des F#-Interpreters (Klicken Sie zum Vergrößern auf das Bild)
Beachten Sie, dass die Anweisung in der Shell durch zwei Semikolons beendet werden muss. Dies ist eine Besonderheit des interaktiven Modus und ist für kompilierte F#-Programme nicht erforderlich.
Führen Sie dieses Beispiel als standardmäßige ausführbare .NET-Datei aus, indem Sie Visual Studio wie gewöhnlich starten und ein neues F#-Projekt erstellen (das Sie unter „Other Project Types“ (Weitere Projekttypen) finden). Ein F#-Projekt besteht am Anfang aus einer einzelnen F#-Quelldatei namens „file1.fs“. Wenn Sie diese Datei öffnen, können Sie eine große Sammlung von Beispiel-F#-Code sehen. Werfen Sie ein Blick auf ihren Inhalt, um eine Vorstellung davon zu erhalten, wie die F#-Syntax aussieht. Ersetzen Sie danach die gesamte Datei durch den oben dargestellten „Hello, World!“-Code. Führen Sie die Anwendung aus. Wie zu erwarten, wird „Hello, World!“ in einem Konsolenanwendungsfenster angezeigt.
Wenn Sie lieber Befehlszeilen verwenden, können Sie den Code mithilfe des fsc.exe-Tools kompilieren. Dieses Tool befindet sich im Unterverzeichnis „\bin“ des F#-Installationsverzeichnisses. Beachten Sie, dass fsc.exe wie die meisten Befehlszeilencompiler funktioniert und mithilfe der Quelle in der Befehlszeile eine ausführbare Datei erstellt. Die meisten Befehlszeilenschalter sind dokumentiert. Viele kennen Sie sicherlich bereits aus der Arbeit mit csc.exe- oder cl.exe-Compilern. Im Bereich von MSBuild ist F# jedoch momentan noch nicht auf dem gleichen Entwicklungsstand. Es gibt keine direkte Unterstützung in der aktuellen Installation (1.9.3.7 zum derzeitigen Zeitpunkt) für MSBuild-gesteuerte Kompilierung.
Wenn Sie Ihr „Hello, World!“ optisch ein wenig ausgestalten möchten, bietet F# problemlos absolute Genauigkeit und Interoperabilität mit der zugrunde liegenden CLR-Plattform, einschließlich der Windows Forms-Bibliotheken. Probieren Sie Folgendes aus:
System.Windows.Forms.MessageBox.Show "Hello World"
Die F#-Sprache ist besonders für die mathematische und die naturwissenschaftliche Gemeinschaft attraktiv, die bereits funktionale Sprachen wie OCaml oder Haskell verwenden, sowie für die .NET-Entwickler in der ganzen Welt, weil die .NET Framework-Klassenbibliothek sowie die F#-Bibliotheken verwendet werden können.
Der let-Ausdruck
Betrachten Sie den folgenden F#-Code, der nicht so trivial wie der traditionelle „Hello, World!“-Code ist. Beachten Sie die folgenden Aspekte:
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
Ein interessantes Element in dieser F#-Syntax ist der let-Ausdruck. Er ist der wichtigste Ausdruck in allen Sprachen. Mit let können Sie formal einem Bezeichner einen Wert zuweisen. Die Versuchung für den Visual Basic- und C#-Entwickler besteht darin, dies als „Definition einer Variablen“ zu übersetzen, was wäre eine unwahre Annahme wäre. Bezeichner in F# verkörpern stattdessen zwei Prinzipien. Erstens darf ein einmal definierter Bezeichner niemals geändert werden. (Auf diese Weise können Programmierer mit F# sicher gleichzeitig ausführbare Programme erstellen, weil ein änderbarer Zustand verhindert wird.) Zweitens kann der Bezeichner nicht nur ein primitiver Typ oder ein Objekttyp, wie für C# und Visual Basic beschrieben, sein, sondern auch ein Funktionstyp, der mit dem in LINQ vergleichbar ist.
Beachten Sie ebenso, dass Bezeichner nie explizit als Besitzer eines Typs definiert werden. Der Ergebnisbezeichner wird zum Beispiel nie definiert, sondern von der rechten Seite des darauf folgenden Ausdrucks abgeleitet. Dies wird als Typrückschluss bezeichnet und beschreibt die Möglichkeit des Compilers, den Code zu analysieren und den Rückgabewert zu bestimmen sowie automatisch zu integrieren. (Hier besteht eine Parallele zu den neuen, von C# über das var-Schlüsselwort abgeleiteten Typausdrücken).
Der let-Ausdruck kann nicht nur mit Daten verwendet werden. Sie können mit ihm Funktionen definieren, die F# als erstklassige Konzepte erkennt. Im Folgenden wird beispielsweise eine add-Funktion mit zwei Parametern, a und b, definiert:
Die Implementierung verläuft erwartungsgemäß: Es werden a und b hinzugefügt, und das Ergebnis wird implizit an den Aufrufer zurückgegeben. Daher gibt jede Funktion in F# in technischer Hinsicht einen Wert zurück, selbst wenn dieser Wert kein Wert wie sonst üblich ist, der unter dem speziellen Namen „unit“ bekannt ist. Dadurch entstehen einige interessante Auswirkungen auf den F#-Code, insbesondere an der Schnittstelle zur .NET Framework-Klassenbibliothek. Zum jetzigen Zeitpunkt können C#- und Visual Basic-Entwickler unit jedoch als gleichwertig mit void betrachten.
Es kann vorkommen, dass eine Funktion einen übergebenen Parameter ignorieren muss. Verwenden Sie hierfür in F# einfach den Unterstrich als Platzhalter für den eigentlichen Parameter:
let return10 _ =
add 5 5
// 12 is effectively ignored, and ten is set to the resulting
// value of add 5 5
let ten = return10 12
printf "ten = %d\n" ten
Wie viele funktionale Sprachen erlaubt F# das Currying. Dabei kann die Anwendung einer Funktion nur teilweise definiert werden und erhält den Rest der Parameter beim Aufruf:
In gewisser Hinsicht ist dies mit dem Erstellen einer überladenen Methode vergleichbar, die einen anderen Parametersatz verwendet und eine andere Methode aufruft:
public class Adders {
public static int add(int a, int b) { return a + b; }
public static int add5(int a) { return add(a, 5); }
}
Es gibt jedoch auch einen feinen Unterschied. Beachten Sie, dass in der F#-Version keine Typen explizit definiert sind. Das bedeutet, dass der Compiler seine Typrückschlüsse durchführt, bestimmt, ob der Parameter für add5 typkompatibel mit dem Hinzufügen zum Ganzzahlliteral 5 ist, und entweder so die Kompilierung durchführt oder einen Fehler meldet. Ein großer Teil der F#-Sprache ist tatsächlich implizit typparametrisiert (und verwendet folglich Generika).
In Visual Studio zeigt das Positionieren des Zeigers über der vorher gezeigten Definition von ten an, dass der Typ folgendermaßen deklariert ist:
In F# bedeutet dies, dass ten ein Wert, eine Funktion ist, die einen Parameter eines beliebigen Typs benötigt und ein int-Ergebnis liefert. Die Strichsyntax entspricht etwa der <T>-Syntax in C#. Die geeignetste Übersetzung in eine C#-Funktion wäre die Beschreibung, dass ten wie eine Delegatinstanz für eine typparametrisierte Methode aussieht, dessen Typ Sie eigentlich ignorieren möchten (was aber unter den Regeln von C# nicht möglich ist):
delegate int Transformer<T>(T ignored);
public class App
{
public static int return10(object ignored) { return 5 + 5; }
static void Main()
{
Transformer<object> ten = return10;
System.Console.WriteLine("ten = {0}", return10(0));
}
}
Das for-Schlüsselwort
Sehen Sie sich das for-Schlüsselwort im ersten Beispiel an:
#light
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
Starten Sie oben im Code, und betrachten Sie die #light-Syntax. Dies ist ein Zugeständnis an Nicht-OCaml-Programmierer, die neu bei F# sind. Einige der Syntaxanforderungen der OCaml-Sprache wurden gelockert, und es wurden viele Leerzeichen beim Definieren von Codeblöcken verwendet. Dies ist zwar nicht erforderlich, erleichtert dem durchschnittlichen Entwickler mit C#- oder Visual Basic-Erfahrung aber das Analysieren der Syntax. Deshalb erscheint die Syntax häufig in F#-Beispielen und bereitgestellten Codeausschnitten und stellt für die F#-Programmierung schon beinahe einen Standard dar. (In einer zukünftigen Version von F# ist #light möglicherweise die Standardsyntax und nicht umgekehrt.)
Die unkompliziert erscheinende for-Schleife ist in Wirklichkeit alles andere als einfach. Offiziell handelt es sich um eine generierte Liste, also eigentlich um einen Codeblock, der als Ergebnis eine Liste erstellt.
Eine Liste ist ein primitives Konstrukt, das häufig in funktionalen Sprachen gefunden wird. In dieser Hinsicht ähnelt es auf vielfache Weise einem Array. Eine Liste erlaubt jedoch keinen positionsbasierten Zugriff (wie z. B. die traditionelle a[i]-Syntax in C#). Listen tauchen an verschiedenen Stellen der funktionalen Programmierung auf. Sie können zum größten Teil als F#-Äquivalent zur .NET Framework-Liste<T> mit ein paar erweiterten Funktionen verstanden werden.
Eine Liste ist immer ein bestimmter Typ. In diesem Fall liefert der Bezeichner als Ergebnis eine Liste von Tupeln, insbesondere den Tupeltyp, der von F# als Typ (int * int) identifiziert wird. Dieses Konzept wird Ihnen nicht fremd sein, wenn Sie sich eine Liste mit Tupeln als ein Spaltenpaar vorstellen, das von einer SELECT-Anweisung in SQL zurückgegeben wird. In dem Beispiel wird daher im Grunde eine Liste von Ganzzahlpaaren erstellt, die 100 Elemente enthält.
Normalerweise werden in funktionalen Sprachen Funktionsdefinitionen überall dort verwendet, wo der eigentliche Code erscheinen kann. Wenn Sie nun das vorherige Beispiel erweitern möchten, können Sie Folgendes schreiben:
let compute2 x = (x, x*x)
let compute3 x = (x, x*x, x*x*x)
let results2 = [ for i in 0 .. 100 -> compute2 i ]
let results3 = [ for i in 0 .. 100 -> compute3 i ]
Die Vorstellung eines Schleifendurchlaufs durch eine Liste (oder ein Array oder ein anderes Konstrukt, das durchlaufen werden kann) ist eine so häufige Aufgabe in funktionalen Sprachen, dass sie als grundlegender Methodenaufruf verallgemeinert wurde: List.iter. Es wird einfach eine Funktion für jedes Element der Liste aufgerufen. Andere ähnliche Bibliotheksfunktionen bieten nützliche Fähigkeiten. Zum Beispiel verwendet List.map eine Funktion als Argument und wendet die Funktion auf jedes Element der Liste an. Dabei wird eine neue Liste im Prozess zurückgegeben.
Die Pipeline
Es soll noch ein weiteres Konstrukt in F# untersucht werden: der Pipelineoperator. Er verwendet die Ergebnisse einer Funktion ähnlich wie Pipes aus Befehlsshells (wie Windows PowerShell®) als Eingabe für eine Folgefunktion. Betrachten Sie den Ausschnitt aus F# in Abbildung 2. Dieser Code verwendet den System.Net-Namespace, um eine Verbindung zu einem HTTP-Server herzustellen, nimmt das entsprechende HTML auf und analysiert die Ergebnisse.

Figure 2 Abrufen und Analysieren von HTML
/// Get the contents of the URL via a web request
let http(url: string) =
let req = System.Net.WebRequest.Create(url)
let resp = req.GetResponse()
let stream = resp.GetResponseStream()
let reader = new System.IO.StreamReader(stream)
let html = reader.ReadToEnd()
resp.Close()
html
let getWords s = String.split [ ' '; '\n'; '\t'; '<'; '>'; '=' ] s
let getStats site =
let url = "http://" + site
let html = http url
let words = html |> getWords
let hrefs = html |> getWords |> List.filter (fun s -> s = "href")
(site,html.Length, words.Length, hrefs.Length)
Beachten Sie den Wortbezeichner in der Definition von getStats. Er verwendet den HTML-Wert aus der URL und wendet die getWords-Funktion auf ihn an. Ich hätte die Definition auch folgendermaßen schreiben können:
let words = getWords html
Beide sind identisch. Der hrefs-Bezeichner zeigt jedoch die Leistung des Pipelineoperators, sodass Sie eine willkürliche Anzahl von Anwendungen aneinanderreihen können. In diesem Beispiel wird die Ergebnisliste der Wörter auf die List.filter-Funktion übertragen, die mithilfe einer anonymen Funktion nach dem Wort „href“ sucht und es zurückgibt, wenn der Ausdruck zutrifft. Überdies stellen die Ergebnisse des getStats-Aufrufs ein anderes Tupel dar, dieses Mal ein (string * int * int * int). Um dies mit C# zu schreiben, werden weit mehr als 15 Codezeilen benötigt.
In dem Beispiel in Abbildung 2 werden darüber hinaus weitere Aspekte der Kompatibilität von F# mit .NET Framework gezeigt. Dieses Thema setzt sich hier fort:
open System.Collections.Generic
let capitals = Dictionary<string, string>()
capitals.["Great Britain"] <- "London"
capitals.["France"] <- "Paris"
capitals.ContainsKey("France")
Hierbei handelt es sich lediglich um das Ausführen des Dictionary<K,V>-Typs. Es wird jedoch demonstriert, wie Generika in F# spezifiziert werden (mithilfe von spitzen Klammern wie in C#), wie Indexer in F# verwendet werden (mithilfe der eckigen Klammern wie in C#) und wie .NET-Methoden ausgeführt werden (mithilfe des „Punkts“ und der Klammern wie in C#). Das einzig Neue ist die Zuweisung änderbarer Werte, wofür der Pfeil-nach-links-Operator verwendet wird. Dies ist notwendig, weil F#, wie die meisten funktionalen Sprachen, die Verwendung des Gleichheitszeichens für den Vergleich reserviert. Dies beruht auf dem mathematischen Verständnis, dass, wenn x = y, x und y den gleichen Wert haben, statt dass x der Wert y zugewiesen wird. (Echte Mathematiker können sich über die Vorstellung amüsieren, dass x = x + 1 in irgendeinem Universum, einem realen oder einem Fantasieuniversum, wahr sein könnte).
Erstellen von Objekten in F#
Selbstverständlich sind nicht alle .NET-Entwickler, die neu bei F# sind, von der Vorstellung funktionaler Konzepte begeistert. Die meisten F#-Entwickler mit C#- oder Visual Basic-Hintergrund möchten die Bestätigung erhalten, dass sie in F# ohne gravierende Auswirkungen alte Gewohnheiten pflegen können. In einem gewissen Maß ist dies möglich.
Denken Sie zum Beispiel an die Klassendefinition für den zweidimensionalen Vektor oben in Abbildung 3. Einige interessante Ideen entstehen daraus. Beachten Sie zunächst, dass es keinen expliziten Konstruktortext gibt. Die Parameter in der ersten Zeile zeigen die Parameter an, über die Benutzer Vector2D-Instanzen erstellen, die hauptsächlich als Konstruktor dienen. Der Längenbezeichner sowie die dx- und dy-Bezeichner werden innerhalb des Vector2D-Typs zu privaten Elementen, während das member-Schlüsselwort auf Member hinweist, die außerhalb von Vector2D über einen standardmäßigen .NET-Eigenschaftenzugriff verfügbar sein sollten. Grundsätzlich deklariert dieser F#-Code das, was Sie am unteren Rand von Abbildung 3 sehen (wie von Reflector gemeldet).

Figure 3 Vektorvarianten in F# und C#
VECTOR2D IN F#
type Vector2D(dx:float,dy:float) =
let length = sqrt(dx*dx + dy*dy)
member obj.Length = length
member obj.DX = dx
member obj.DY = dy
member obj.Move(dx2,dy2) = Vector2D(dx+dx2,dy+dy2)
VECTOR2D IN C# (REFLECTOR>
[Serializable, CompilationMapping(SourceLevelConstruct.ObjectType)]
public class Vector2D
{
// Fields
internal double _dx@48;
internal double _dy@48;
internal double _length@49;
// Methods
public Vector2D(double dx, double dy)
{
Hello.Vector2D @this = this;
@this._dx@48 = dx;
@this._dy@48 = dy;
double d = (@this._dx@48 * @this._dx@48) +
(@this._dy@48 * @this._dy@48);
@this._length@49 = Math.Sqrt(d);
}
public Hello.Vector2D Move(double dx2, double dy2)
{
return new Hello.Vector2D(this._dx@48 + dx2, this._dy@48 + dy2);
}
// Properties
public double DX
{
get
{
return this._dx@48;
}
}
public double DY
{
get
{
return this._dy@48;
}
}
public double Length
{
get
{
return this._length@49;
}
}
}
Denken Sie daran, dass F#, wie die meisten funktionalen Sprachen, unveränderbare Werte und Status unterstützt. Dies ist leicht am Code in Abbildung 3 zu erkennen, da alle Eigenschaften schreibgeschützt sind. Der Move-Member ändert nicht das vorhandene Vector2D, sondern erstellt stattdessen aus dem aktuellen Vector2D ein neues und wendet die Änderungswerte vor der Rückgabe darauf an.
Beachten Sie auch, dass die F#-Version nicht nur vollständig threadsicher ist, sondern in ihrer Gesamtheit über traditionellen C#- oder Visual Basic-Code zugänglich ist. Dadurch wird eine einfache Methode für den Einstieg in F# bereitgestellt: die Verwendung zum Definieren von Geschäftsobjekten oder anderen Typen, die threadsicher und unveränderbar sein wollen oder müssen. Während es sicherlich möglich ist, Typen in F# zu erstellen, die den üblichen Satz änderbarer Vorgänge bieten (festgelegte Eigenschaften usw.), ist der Aufwand dabei höher und erfordert die Verwendung änderbarer Schlüsselwörter. In einer Welt, in der gleichzeitige Abläufe zum Tagesgeschehen gehören, entspricht dies genau den Anforderungen vieler Benutzer: standardmäßig unveränderbar und bei Bedarf änderbar.
Das Erstellen von Typen in F# ist interessant, aber es ist auch möglich, F# für die Aufgaben von traditionellem C#- oder Visual Basic-Code zu verwenden, wie das Erstellen einer einfachen Windows Forms-Anwendung und das Erfassen von Benutzereingaben (siehe Abbildung 4).

Figure 4 Windows Forms mit F#
#light
open System
open System.IO
open System.Windows.Forms
open Printf
let form = new Form(Text="My First F# Form", Visible=true)
let menu = form.Menu <- new MainMenu()
let mnuFile = form.Menu.MenuItems.Add("&File")
let filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
let mnuiOpen =
new MenuItem("&Open...",
new EventHandler(fun _ _ ->
let dialog =
new OpenFileDialog(InitialDirectory="c:\\",
Filter=filter;
FilterIndex=2,
RestoreDirectory=true)
if dialog.ShowDialog() = DialogResult.OK then
match dialog.OpenFile() with
| null -> printf "Could not read the file...\n"
| s ->
let r = new StreamReader(s)
printf "First line is: %s!\n" (r.ReadLine());
s.Close();
),
Shortcut.CtrlO)
mnuFile.MenuItems.Add(mnuiOpen)
[<STAThread>]
do Application.Run(form)
Alle Entwickler, die mit Windows Forms vertraut sind, erkennen schnell die folgenden Vorgänge: Ein einfaches Formular wird erstellt, einige Eigenschaften werden aufgefüllt, ein Ereignishandler wird gefüllt, und die Anwendung erhält die Anweisung, die Ausführung erst anzuhalten, wenn der Benutzer auf die Schaltfläche „Close“ in der rechten oberen Ecke klickt. Standard für .NET-Anwendungen, was es ermöglicht, sich ganz auf die F#-Syntax zu konzentrieren.
Die open-Anweisung funktioniert auf ähnliche Weise wie eine using-Anweisung in C#, indem hauptsächlich ein .NET-Namespace für die Verwendung ohne formale Qualifizierer geöffnet wird. Der Printf-Namespace ist ein F#-Original, vom technischen Standpunkt her ist er ein Port des OCaml-Moduls mit dem gleichen Namen. F# stimmt nicht nur mit der .NET Framework-Klassenbibliothek voll überein, sondern bietet auch einen sehr einfachen Port von OCaml-Bibliotheken, wodurch Programmierern, die mit dieser Sprache vertraut sind, der Einstieg in .NET Framework erleichtert wird. (Printf befindet sich übrigens innerhalb der FSharp.Core.dll-Assembly.) Ihnen steht die Verwendung von System.Console.WriteLine frei, wenn Ihnen dies in ästhetischer Hinsicht zusagt.
Beim Erstellen von Formularbezeichnern werden F#-benannte Parameter verwendet. Dies entspricht dem Instanziieren des Objekts und einer Reihe anschließender Aufrufe von Eigenschaftsätzen, um die Eigenschaften mit Werten zu füllen. Der einige Zeilen darunter erstellte Dialogbezeichner erfährt die gleiche Behandlung.
Die Definition des mnuiOpen-Bezeichners enthält ein interessantes Konstrukt. Entwicklern, die anonyme Delegaten aus .NET Framework 2.0 oder Lambda-Ausdrücke aus .NET Framework 3.5 kennen, wird es bekannt vorkommen. In der mit Open MenuItem verbundenen Konstruktion von EventHandler befindet sich eine anonyme Funktion, die mithilfe der folgenden Syntax definiert wird:
Wie bei anonymen Delegaten wird eine Funktion erstellt, die nach der Auswahl des Menüelements aufgerufen wird. Jedoch ist die Syntax ein wenig kompliziert.
Die Definition des EventHandler-Abschnitts der MenuItem-Definition ist eine anonyme Funktion, die die zwei übergebenen Parameter ignoriert. Diese entsprechen genau den Sender- und Ereignisargumenten im standardmäßigen EventHandler-Delegattyp. Die Funktion legt fest, dass ein neuer OpenFileDialog angezeigt und beim Klicken auf „OK“ die Ergebnisse (irgendwie) untersucht werden sollen:
if dialog.ShowDialog() = DialogResult.OK then
match dialog.OpenFile() with
| null -> printf "Could not read the file...\n"
| s ->
let r = new StreamReader(s) in
printf "First line is: %s!\n" (r.ReadLine());
s.Close();
Die Ergebnisse werden mithilfe des Mustervergleichs untersucht. Hierbei handelt es sich um ein leistungsfähiges Feature aus der Welt der funktionalen Sprachen. Auf den ersten Blick ähnelt der Mustervergleich einem switch/case aus C# und besitzt genau die Eigenschaften, die sein Name impliziert: Er vergleicht einen Wert mit unterschiedlichen Mustern, von denen nicht alle konstante Werte sein müssen, und führt den passenden Codeblock aus. Deshalb wird beispielsweise in dem hier dargestellten entsprechenden Block das Ergebnis von OpenFile in Bezug auf zwei mögliche Werte abgestimmt: Null bedeutet, dass keine Datei geöffnet werden konnte, und „s“ bedeutet, dass ein beliebiger Wert ungleich null zugewiesen und anschließend von StreamReader als Konstruktor zum Öffnen und Lesen der ersten Zeile der jeweiligen Textdatei verwendet wird.
Der Mustervergleich spielt in den meisten funktionalen Sprachen eine große Rolle und soll daher hier erläutert werden. Am häufigsten wird er in Verbindung mit einem besonderen union-Typ verwendet, der vage an einen Aufzählungstyp aus C# oder Visual Basic erinnert:
// Declaration of the 'Expr' type
type Expr =
| Binary of string * Expr * Expr
| Variable of string
| Constant of int
// Create a value 'v' representing 'x + 10'
let v = Binary("+", Variable "x", Constant 10)
Er wird häufig in funktionalen Sprachen zum Erstellen zentraler Darstellungen domänenspezifischer Sprachen verwendet, mit denen Entwickler kompliziertere und leistungsfähigere Konstrukte schreiben können. Zum Beispiel könnte diese Syntax erweitert werden, um eine vollständige Computersprache zu erstellen, die problemlos erweitert werden kann, indem dem Expr-Typ neue Elemente hinzugefügt werden. Beachten Sie jedoch Folgendes: Die Syntax mit dem *-Zeichen weist nicht auf eine Multiplikation hin, sondern steht für eine Standardmethode funktionaler Sprachen zum Anzeigen eines Typs, der aus mehreren Teilen besteht.
Funktionale Sprachen werden tatsächlich häufig zum Schreiben von sprachorientierten Programmiertools wie Interpretern und Compilern verwendet. Dabei wird der Expr-Typ schließlich zum vollständigen Satz an Sprachausdruckstypen. In F# wird dies noch einfacher. Hierfür werden zwei Tools, fslex und fsyacc, eingefügt, die speziell für die Aufnahme traditioneller Spracheingaben – lex- und yacc-Dateien – entworfen wurden. Anschließend werden sie zur leichteren Bearbeitung in F#-Code kompiliert. Laden Sie bei Interesse das F#-Installationsprogramm herunter, um diesen Aspekt weiter zu untersuchen. Das Analysebeispiel im standardmäßigen F#-Paket bietet eine praktische grundlegende Struktur für den Einstieg.
Der spezielle union-Typ ist nur einer der Vorteile des Mustervergleichs. Der zweite Vorteil besteht in der Ausführung von Ausdrücken, wie Sie in Abbildung 5 erkennen können. Das rec in der Definition von eval wird benötigt, um dem F#-Compiler mitzuteilen, dass eval rekursiv innerhalb des Definitionstexts aufgerufen wird. Ohne rec erwartet F# das Vorhandensein einer lokalen, verschachtelten Funktion namens „eval“. Ich verwende die getVarValue-Funktion für die Rückgabe einiger vordefinierter Werte für die Variablen. In einer echten Anwendung würde getVarValue wahrscheinlich ein Dictionary auf die zurückzugebenden Werte prüfen, wie es zum Zeitpunkt der Erstellung der Variablen festgelegt wurde.

Figure 5 Ausführung von Ausdrücken
let getVarValue v =
match v with
| "x" -> 25
| "y" -> 12
| _ -> 0
let rec eval x =
match x with
| Binary(op, l, r) ->
let (lv, rv) = (eval l, eval r) in
if (op = "+") then lv + rv
elif (op = "-") then lv - rv
else failwith "E_UNSUPPORTED"
| Variable(var) ->
getVarValue var
| Constant(n) ->
n
do printf "Results = %d\n" (eval v)
Beim Aufrufen von eval wird der Wert v erfasst, und es festgestellt, dass es sich um einen Binärwert handelt. Dies wird mit dem ersten Unterausdruck verglichen, der wiederum den Wert (lv, rv) an die evaluierten Ergebnisse der linken und rechten Teile des soeben untersuchten Binärwerts bindet. Der unbenannte Wert (lv, rv) ist ein Tupel. Dabei handelt es sich im Grunde um einen einzelnen Wert aus mehreren Teilen, der mit einer relationalen Satz- oder einem C struct vergleichbar ist.
Wenn eval l zum ersten Mal aufgerufen wird, ist l von der Binärinstanz ein Variablentyp. Folglich wird der rekursive Aufruf von eval mit dem Zweig des Mustervergleichsblocks verglichen. Dadurch wird wiederum getVarValue aufgerufen, das ein hartcodiertes 25 zurückgibt, das letzten Endes an den Wert lv gebunden wird. Die gleiche Sequenz wird für r ausgeführt, eine Konstante, die den Wert 10 enthält und an rv gebunden wird. Dann wird der Rest des Blocks, ein if/else-if/else-Block, ausgeführt. Er kann von einem Entwickler, der mit C#, Visual Basic oder C++ vertraut ist, leicht verstanden werden.
Am wichtigsten ist hier zu erkennen, dass wieder jeder Ausdruck einen Wert zurückgibt, sogar innerhalb des Mustervergleichsblocks. In diesem Fall ist der Rückgabewert ein Ganzzahlwert, und zwar entweder der Wert des Vorgangs, der von der Variablen abgerufene Wert oder die Konstante selbst. Allein diese Tatsache kann mehr als alles andere Entwickler aus dem Konzept bringen, die eher an objektorientiertes oder imperatives Programmieren gewöhnt sind. Das liegt daran, dass in C#, Visual Basic oder C++ Rückgabewerte optional sind und selbst dann ignoriert werden können, wenn sie angegeben werden. In funktionalen Sprachen wie F# wird für das Ignorieren eines Rückgabewerts ein expliziter Codierausdruck benötigt. In solchen Fällen können Programmierer die Ergebnisse in eine Funktion mit dem Namen „ignore“ eingeben, die genau das tut, was ihr Name besagt.
Asynchrones F#
Bisher hat die Darstellung der F#-Syntax eine von zwei Formen angenommen: Eine Form basiert auf relativ einfachen funktionalen Konstrukten. Die zweite Form sieht wie eine seltsamere und kürzere Version traditioneller, objektorientierter, .NET-kompatibler Sprachen (C#, Visual Basic oder C ++/CLI) aus. Beide bieten kaum überzeugende Argumente für die Übernahme von F# im Unternehmen.
Betrachten Sie jedoch Abbildung 6. Diese Darstellung passt sicherlich in keine der beiden beschriebenen Kategorien. Außer den !-Zeichen, die an einigen Stellen angezeigt werden, und der Verwendung des asynch-Modifizierers sieht dies nach relativ einfachem Code aus: Laden einer Quellgrafikdatei, Extrahieren seiner Daten, Übergabe der Daten an eine eigenständige Funktion zur Bearbeitung (z. B. Drehen oder Verzerren) und Schreiben der Daten zurück in eine Ausgabedatei.

Figure 6 Ändern eines Bilds
let TransformImage pixels i =
// Some kind of graphic manipulation of images
let ProcessImage(i) =
async { use inStream = File.OpenRead(sprintf "source%d.jpg" i)
let! pixels = inStream.ReadAsync(1024*1024)
let pixels' = TransformImage(pixels,i)
use outStream = File.OpenWrite(sprintf "result%d.jpg" i)
do! outStream.WriteAsync(pixels')
do Console.WriteLine "done!" }
let ProcessImages() =
Async.Run (Async.Parallel
[ for i in 1 .. numImages -> ProcessImage(i) ])
Weniger deutlich zu erkennen ist die Tatsache, dass dieser Code aufgrund der Verwendung des async-Modifizierers zu einem asynchronen Workflow in F# wird (ohne Bezug zu Windows Workflow Foundation). Das bedeutet, dass alle Lade-/Verarbeitungs-/Speicherschritte auf parallelen Threads eines .NET-Threadpools stattfinden.
Sehen Sie sich einfach den Code in Abbildung 7 an. In dieser Sequenz werden asynchrone Workflows leicht verständlich dargestellt. Bei evals handelt es sich im Wesentlichen um ein Array auszuführender Funktionen. Sie befinden sich in der Warteschlange im Threadpool zur Ausführung durch den Async.Parallel-Aufruf. Während der Ausführung wird deutlich, dass sich die Funktionen innerhalb von evals eigentlich auf einem anderen Thread als die Funktion in awr befinden. (Aufgrund der Beschaffenheit des .NET-Systemthreadpools können einige oder alle evals-Funktionen auf dem gleichen Thread ausgeführt werden.)

Figure 7 Asynchrones Ausführen von Funktionen
#light
open System.Threading
let printWithThread str =
printfn "[ThreadId = %d] %s" Thread.CurrentThread.ManagedThreadId str
let evals =
let z = 4.0
[ async { do printWithThread "Computing z*z\n"
return z * z };
async { do printWithThread "Computing sin(z)\n"
return (sin z) };
async { do printWithThread "Computing log(z)\n"
return (log z) } ]
let awr =
async { let! vs = Async.Parallel evals
do printWithThread "Computing v1+v2+v3\n"
return (Array.fold_left (fun a b -> a + b) 0.0 vs) }
let R = Async.Run awr
printf "Result = %f\n" R
Die Tatsache, dass sie aus dem .NET-Threadpool ausgeführt werden, zeigt erneut, wie gut die F#-Sprache Interoperabilität mit der zugrunde liegenden Laufzeit unterstützt. Da eine Abhängigkeit von der .NET Framework-Klassenbibliothek sogar in Bereichen besteht, die traditionell der spezialisierten Implementierung (wie z. B. Threading) in funktionalen Sprachen vorbehalten ist, können C#-Programmierer F#-Bibliotheken oder -Module genauso nutzen, wie F#-Entwickler C#-Bibliotheken nutzen können. In Zukunft werden F#-Features wie asynchrone Aufgaben sogar neue .NET Framework-Bibliotheken nutzen können, wie z. B. die Aufgabenverarbeitungsbibliothek in der Bibliothek für parallele Erweiterungen.
Anpassung an F#
Ich denke, es liegt auf der Hand, dass es noch viel mehr über die F#-Sprache zu sagen gäbe, als in diesem Artikel behandelt werden kann. Angesichts der neuen Syntax und der vollständig neuen Denkweise (funktional im Gegensatz zu imperativ) kann es einige Zeit dauern, bis ein durchschnittlicher objektorientierter Entwickler, der an C# oder Visual Basic gewöhnt ist, F# beherrscht. Glücklicherweise bleibt F# vollständig interoperabel mit dem übrigen .NET-Umfeld. Das bedeutet, dass Sie einen großen Teil Ihrer Kenntnisse sowie viele vorhandene Tools nutzen können, um F# in Ihr Programmierarsenal zu integrieren.
F#-Entwickler haben vollständigen Zugriff auf alle Foundation-Bibliotheken. Da F# zudem einige Aspekte von imperativer Entwicklung und Objektentwicklung unterstützt, kann der interaktive Modus von F# durchaus als Methode zum Erlernen der F#-Syntax sowie der Details von Windows Presentation Foundation, Windows Communication Foundation oder Windows Workflow Foundation betrachtet werden, ohne dass für das Kompilieren von Zyklen pausiert werden muss.
Wie bereits erwähnt, können Entwickler Geschäftsobjekte in F# für die Verwendung durch andere Teile ihres Anwendungscodes schreiben. Da das F#-Typkonstrukt Klassen erzeugt, die zum größten Teil mit ihren C#- oder Visual Basic-Entsprechungen identisch sind, werden Persistenzbibliotheken wie NHibernate F#-Typen ohne Problem beibehalten. Auf diese Weise wird eine nahtlose Integration von F# in funktionierende Geschäftsanwendungen ermöglicht.
Schon allein das Erlernen von F# wird Ihnen dabei helfen, viele Features und zukünftige Versionen von C# und Visual Basic zu verstehen, da viele dieser Ideen und Konzepte – einschließlich Generika, Iteratoren (das yield-Schlüsselwort in C#) und LINQ – funktionale Wurzeln haben und außerdem die Erforschung vom F#-Team durchgeführt wurde. Ganz gleich, wie Sie es sehen, die funktionale Programmierung existiert und wird bleiben.
Ted Neward ist ein unabhängiger Berater, der sich auf umfassende Unternehmenssysteme spezialisiert hat. Er ist Autor und Mitautor verschiedener Bücher, Microsoft MVP-Architekt, technischer Direktor bei BEA, Sprecher bei INETA und Ausbilder bei Pluralsight. Sie erreichen Ted Neward über
ted@tedneward.com und seinen Blog unter
blogs.tedneward.com besuchen.