Tiefe Einblicke in CLR

Grundlagen von F#

Luke Hoban

Bei F# handelt es sich um eine neue, funktionale und objektorientierte Programmiersprache für Microsoft .NET Framework, die in die diesjährige Version von Microsoft Visual Studio 2010 integriert ist. F# vereint eine einfache und prägnante Syntax mit einer leistungsstarken statischen Typisierung und kann von der einfachen untersuchenden Programmierung in F# Interactive bis hin zur groß angelegten, auf .NET Framework basierenden Komponentenentwicklung mit Visual Studio skaliert werden.

F# wurde von Grund auf für CLR entwickelt. Als eine auf .NET Framework basierende Sprache nutzt F# die umfangreichen Bibliotheken, die in der .NET Framework-Plattform verfügbar sind, und es kann zum Erstellen von .NET-Bibliotheken oder zur Implementierung von .NET-Schnittstellen verwendet werden. F# nutzt auch viele der CLR-Hauptfunktionen, einschließlich Generika, Garbage Collection, endständiger Aufrufanweisugen, sowie das grundlegende CLI-Typsystem (Common Language Infrastructure).

Dieser Artikel befasst sich mit einigen grundlegenden Konzepten der Sprache F# und deren Implementierung in der CLR.

Ein schneller Blick auf F#

Beginnen wir mit einem kurzen Überblick über einige der zentralen Sprachfunktionen in F#. Weitere Informationen zu diesen Funktionen und den zahlreichen anderen interessanten Konzepten in der Sprache F# finden Sie in der über das F# Developer Center verfügbaren Dokumentation unter fsharp.net.

Die wichtigste Funktion von F# ist das Schlüsselwort „let“, das einen Wert an einen Namen bindet. Das Schlüsselwort „let“ kann zur Bindung von Daten- und Funktionswerten und auch für Bindungen auf höchster Ebene bzw. für lokale Bindungen verwendet werden:

let data = 12
 
let f x = 
    let sum = x + 1 
    let g y = sum + y*y 
    g x

F# umfasst einige wichtige Datentypen sowie eine Sprachsyntax zum Arbeiten mit strukturierten Daten, einschließlich Listen, typisierten optionalen Werten und Tupeln:

let list1 = ["Bob"; "Jom"]

let option1 = Some("Bob")
let option2 = None

let tuple1 = (1, "one", '1')

Diese strukturierten Daten und auch andere Daten können mithilfe von F#-Mustervergleichsausdrücken abgeglichen werden. Der Mustervergleich ist der Verwendung von Switch-Anweisungen in C-ähnlichen Sprachen ähnlich, bietet jedoch umfassendere Möglichkeiten zum Vergleichen und Extrahieren von Teilen von verglichenen Ausdrücken, etwa so wie reguläre Ausdrücke für Mustervergleichszeichenfolgen verwendet werden:

let person = Some ("Bob", 32)

match person with
| Some(name,age) -> printfn "We got %s, age %d" name age
| None -> printfn "Nope, got nobody"

F# verwendet die .NET Framework-Bibliotheken für eine Vielzahl von Aufgaben, z. B. das Zugreifen auf Daten von einer Vielzahl von Datenquellen aus. ..NET-Bibliotheken können von F# in der gleichen Weise wie von anderen .NET-Sprachen verwendet werden:

let http url = 
    let req = WebRequest.Create(new Uri(url))
    let resp = req.GetResponse()
    let stream = resp.GetResponseStream()
    let reader = new StreamReader(stream)
    reader.ReadToEnd()

F# ist auch eine objektorientierte Sprache, in der beliebige .NET-Klassen oder -Strukturen definiert werden können, ähnlich wie bei C# oder Visual Basic:

type Point2D(x,y) = 
    member this.X = x
    member this.Y = y
    member this.Magnitude = 
        x*x + y*y
    member this.Translate(dx, dy) = 
        new Point2D(x + dx, y + dy)

Außerdem unterstützt F# zwei besondere Arten von Typen: Datensätze und Unterscheidungs-Unions. Datensätze sind eine einfache Darstellung von Datenwerten mit benannten Feldern; bei Unterscheidungs-Unions handelt es sich um eine Möglichkeit zur anschaulichen Darstellung von Datentypen, die viele unterschiedliche Arten von Werten haben können. Jede Art weist dabei unterschiedliche zugewiesene Daten auf:

type Person = 
    { Name : string;
      HomeTown : string;
      BirthDate : System.DateTime }

type Tree = 
    | Branch of Tree * Tree
    | Leaf of int

F# in der CLR

F# ist in vielerlei Hinsicht eine Sprache auf einer höheren Ebene als C#, denn Typsystem, Syntax und Sprachkonstrukte sind weiter von den Metadaten und der Zwischensprache (IL) der CLR entfernt. Dies bringt einige interessante Auswirkungen mit sich. Insbesondere bedeutet dies aber, dass F#-Entwickler häufig Probleme lösen und über ihre Programme auf einer höheren Ebene nachdenken können, die näher am Ursprung des bestehenden Problems liegt. Es bedeutet jedoch auch, dass der F#-Compiler mehr Arbeit beim Zuordnen des F#-Codes zur CLR leisten muss und dass die Bindung weniger direkt erfolgt.

Der C# 1.0-Compiler und die CLR wurden zur gleichen Zeit entwickelt, und die Funktionen wurden eng aneinander ausgerichtet. Fast alle C# 1.0-Sprachkonstrukte haben eine sehr direkte Darstellung im CLR-Typsystem und in CIL. Dies ist in den späteren C#-Versionen immer weniger der Fall, da sich die C#-Sprache schneller entwickelt hat als die CLR selbst. Iteratoren und anonyme Methoden waren grundlegende Funktionen der C# 2.0-Sprache, die keine direkten Entsprechungen in CLR besaßen. Abfrageausdrücke und anonyme Typen folgten diesem Trend in C# 3.0.

F# geht einen Schritt weiter. Viele der Sprachkonstrukte haben keine direkten IL-Entsprechungen; Funktionen wie Mustervergleichsausdrücke werden daher in einem umfangreichen Satz von IL-Anweisungen kompiliert, die einen effizienten Mustervergleich ermöglichen. F#-Typen, z. B. Datensätze und Unions, generieren automatisch einen Großteil der erforderlichen Elemente.

Beachten Sie jedoch, dass wir die Kompilierungstechniken des aktuellen F#-Compilers besprechen. Viele dieser Implementierungsdetails sind für den F#-Entwickler nicht direkt sichtbar und könnten in künftigen Versionen des F#-Compilers zur Leistungsoptimierung oder für neue Funktionen geändert werden.

Standardmäßig unveränderlich

Die einfache let-Bindung in F# ist, bis auf einen bedeutenden Unterschied, var in C# ähnlich: Sie können den Wert eines mit let gebundenen Namens später nicht ändern. Das heißt, dass Werte in F# standardmäßig nicht verändert werden können:

let x = 5
x <- 6 // error: This value is not mutable

Die Unveränderlichkeit hat große Vorteile für die Parallelität, da Sie sich keine Gedanken über Sperren machen müssen, wenn Sie den unveränderlichen Zustand verwenden – auf diesen kann sicher von mehreren Threads aus zugegriffen werden. Durch Unveränderlichkeit wird auch die Kopplung zwischen Komponenten verringert. Die einzige Möglichkeit der Einflussnahme einer Komponente auf eine andere besteht darin, dass ein expliziter Aufruf aller Komponenten durchgeführt wird.

Die Veränderlichkeit kann in F# integriert werden und wird häufig beim Aufrufen anderer .NET-Bibliotheken oder zum Optimieren von bestimmten Codepfaden verwendet:

let mutable y = 5
y <- 6

In ähnlicher Weise sind Typen in F# standardmäßig unveränderlich:

let bob = { Name = "Bob"; 
            HomeTown = "Seattle" }
// error: This field is not mutable
bob.HomeTown <- "New York" 

let bobJr = { bob with HomeTown = "Seattle" }

In diesem Beispiel wird, wenn die Veränderung nicht verfügbar ist, stattdessen der Befehl zum Kopieren und Aktualisieren verwendet, um eine neue Kopie aus einer alten Kopie zu erstellen und dabei eines oder mehrere Felder zu ändern. Es wird zwar ein neues Objekt erstellt, aber es verwendet viele Stücke gemeinsam mit dem Original. In diesem Beispiel ist nur eine einzige Zeichenfolge erforderlich, und zwar „Bob“. Diese gemeinsame Verwendung ist ein wichtiger Teil der Leistung der Unveränderlichkeit.

Eine gemeinsame Verwendung ist auch in F#-Sammlungen vorhanden. Der F#-Listentyp ist beispielsweise eine Datenstruktur mit verknüpfter Liste, der ein „tail“ mit anderen Listen gemeinsam verwenden kann:

let list1 = [1;2;3]
let list2 = 0 :: list1
let list3 = List.tail list1

Durch das Kopieren und Aktualisieren und gemeinsame Verwenden, das mit der Programmierung mit unveränderlichen Objekten einhergeht, unterscheidet sich das Leistungsprofil dieser Programme häufig wesentlich von typischen imperativen Programmen.

Die CLR spielt in diesem Zusammenhang eine wichtige Rolle. Bei der Programmierung mit unveränderlichen Objekten werden als Ergebnis der Umwandlung von Daten mehr kurzlebige Objekte erstellt anstatt dass diese direkt geändert werden. Der Garbage Collector (GC) der CLR kann diese problemlos verarbeiten. Kurzlebige kleine Objekte sind aufgrund des generationsbasierten Mark-and-Sweep-Schemas, das vom Garbage Collector der CLR verwendet wird, verhältnismäßig günstig.

Funktionen

F# ist eine funktionale Sprache; Funktionen spielen also logischerweise eine wichtige Rolle in der gesamten Sprache. Funktionen sind der wichtigste Teil des F#-Typsystems. Der Typ „char -> int“ stellt beispielsweise F#-Funktionen dar, die „char“ verwenden und „int“ zurückgeben.

F#-Funktionen sind .NET-Delegaten sehr ähnlich, es gibt aber zwei bedeutende Unterschiede. F#-Funktionen sind nicht nominal. Alle Funktionen, die „char“ verwenden und „int“ zurückgeben, weisen den Typ „char -> int“ auf, wohingegen mehrere Delegaten mit unterschiedlichen Namen verwendet werden können, um Funktionen dieser Signatur darzustellen; diese sind nicht austauschbar.

Außerdem sind F#-Funktionen so konzipiert, dass sie die teilweise oder vollständige Anwendung unterstützen. Von einer teilweisen Anwendung wird gesprochen, wenn eine Funktion mit mehreren Parametern nur eine Teilmenge der Parameter erhält, was zu einer neuen Funktion führt, die die übrigen Parameter verwendet.

let add x y = x + y

let add3a = add 3
let add3b y = add 3 y
let add3c = fun y -> add 3 y

Alle F#-Funktionswerte der ersten Klasse sind Instanzen vom Typ „FSharpFunc<, >“, wie in der F#-Laufzeitbibliothek „Fsharp.Core.dll“ definiert. Wenn eine F#-Bibliothek aus C# verwendet wird, ist dies der Typ, den alle F#-Funktionswerte haben werden, die als Parameter verwendet oder von Methoden zurückgegeben werden. Diese Klasse sieht in etwa wie folgt aus (wenn sie in C# definiert würde):

public abstract class FSharpFunc<T, TResult> {
    public abstract TResult Invoke(T arg);
}

Beachten Sie insbesondere, dass alle F#-Funktionen im Grunde ein einziges Argument verwenden und ein einziges Ergebnis generieren. Dadurch wird das Konzept der teilweisen Anwendung erfasst – eine F#-Funktion mit mehreren Parametern ist tatsächlich eine Instanz des folgenden Typs:

FSharpFunc<int, FSharpFunc<char, bool>>

Das heißt, eine Funktion, die „int“ verwendet und eine andere Funktion zurückgibt, die selbst „char“verwendet und „bool“ zurückgibt. Der häufige Fall einer vollständigen Anwendung erfolgt schnell über Verwendung einer Gruppe von Hilfsprogrammtypen in der F#-Kernbibliothek.

Wenn ein F#-Funktionswert mithilfe eines Lambda-Ausdrucks (fun-Schlüsselwort) oder als Ergebnis einer teilweisen Anwendung einer anderen Funktion (wie im Fall add3a weiter oben beschrieben) erstellt wird, generiert der F#-Compiler eine Closureklasse:

internal class Add3Closure : FSharpFunc<int, int> {
    public override int Invoke(int arg) {
        return arg + 3;
    }
}

Diese Closures sind im Hinblick auf ihre Lambda-Ausdruckskonstrukte Closures ähnlich, die von den C#- und Visual Basic-Compilern erstellt werden. Bei Closures handelt es sich um die am häufigsten verwendeten, von Compilern generierten Konstrukte in der .NET Framework-Plattform, die nicht über direkte Unterstützung auf CLR-Ebene verfügen. Sie sind in nahezu allen .NET-Programmiersprachen vorhanden und werden besonders häufig von F# verwendet.

Funktionsobjekte sind sehr geläufig in F#, der F#-Compiler verwendet daher eine Reihe von Optimierungstechniken, damit diese Closures nicht zugewiesen werden müssen. Der interne, vom F#-Compiler generierte Code, der, wenn möglich, Inlining, Lambda-Anhebung und eine direkte Darstellung als .NET-Methoden verwendet, sieht häufig etwas anders als hier beschrieben aus.

Typrückschluss und Generika

Ein bemerkenswertes Merkmal aller bisherigen Codebeispiele ist das Fehlen einer Typangabe. F# ist zwar eine statisch typisierte Programmiersprache, Angaben für explizite Typen sind aber häufig nicht erforderlich, da in F# der Typrückschluss umfassend genutzt wird.

C#- und Visual Basic-Entwickler werden mit dem Typrückschluss vertraut sein, der wie im folgenden C# 3.0-Code für lokale Variablen verwendet wird:

var name = "John";

Das let-Schlüsselwort in F# ist ähnlich, aber der Typrückschluss in F# geht wesentlich weiter und gilt auch für Felder, Parameter und Rückgabetypen. Im folgenden Beispiel werden die beiden Felder x und y vom Typ „int“ abgeleitet, was dem Standardwert für die +- und *-Operatoren entspricht, die in diesen Werten im Text der Typdefinition verwendet werden. Die Translate-Methode wird vom Typ „Translate: int * int -> Point2D“ abgeleitet:

type Point2D(x,y) = 
    member this.X = x
    member this.Y = y
    member this.Magnitude = 
        x*x + y*y
    member this.Translate(dx, dy) = 
        new Point2D(x + dx, y + dy)

Typangaben können natürlich bei Bedarf verwendet werden, um den F#-Compiler anzuweisen, welcher Typ tatsächlich für einen bestimmten Wert, ein bestimmtes Feld oder einen bestimmten Parameter erwartet wird. Diese Informationen werden dann für den Typrückschluss verwendet. Sie können beispielsweise die Definition von Point2D so ändern, dass „float“ anstelle von „int“ verwendet wird, indem Sie einfach ein paar Typangaben hinzufügen:

type Point2D(x : float,y : float) = 
    member this.X = x
    member this.Y = y
    member this.Magnitude = 
        x*x + y*y
    member this.Translate(dx, dy) = 
        new Point2D(x + dx, y + dy)

Eines der wichtigsten Ergebnisse des Typrückschlusses besteht darin, dass Funktionen, die nicht an einen bestimmten Typ gebunden sind, automatisch in generische Funktionen verallgemeinert werden. Ihr Code wird daher so allgemein wie möglich sein, und Sie müssen nicht alle generischen Typen explizit angeben. Daher spielen Generika in F# eine wichtige Rolle. Der zusammengesetzte Stil der funktionalen Programmierung mit F# unterstützt auch kleine wiederverwendbare Funktionalitätsstücke, die stark davon profitieren, dass sie so allgemein wie möglich gehalten werden. Die Möglichkeit des Erstellens generischer Funktionen ohne komplexe Typangaben ist eine wichtige Funktion von F#.

Die folgende map-Funktion durchläuft beispielsweise eine Liste von Werten und generiert eine neue Liste, indem ihre Argumentfunktion f auf jedes Element angewendet wird:

let rec map f values = 
    match values with
    | [] -> []
    | x :: rest -> (f x) :: (map f rest)

Beachten Sie, dass keine Typangaben erforderlich sind, der abgeleitete Typ für „map“ lautet jedoch „map: (‘a -> ‘b) -> list<’a>  -> list<’b>“. F# kann aus der Verwendung des Mustervergleichs und aus der Verwendung des Parameters f als Funktion einen Rückschluss darauf ziehen, dass die Typen der beiden Parameter eine bestimmte Form haben, aber nicht vollständig fixiert sind. F# gestaltet die Funktion daher so allgemein wie möglich, weist aber dennoch die für die Implementierung erforderlichen Typen auf. Beachten Sie, dass generische Parameter in F# durch das vorangestellte Zeichen ‘ gekennzeichnet sind, um sie syntaktisch von anderen Namen zu unterscheiden.

Don Syme, der Entwickler von F#, war zuvor führender Forscher und Entwickler für die Implementierung von Generika in .NET Framework 2.0. Das Konzept einer Sprache wie F# ist stark von Generika in der Laufzeit abhängig; Symes Interesse an F# entstand teilweise dadurch, dass er diese CLR-Funktion optimal nutzen wollte. F# nutzt die .NET-Generika in großem Maße; die Implementierung des F#-Compilers selbst weist über 9.000 generische Typparameter auf.

Letztendlich ist der Typrückschluss nur eine Funktion zur Kompilierzeit, und jeder Teil des F#-Codes erhält einen abgeleiteten Typ, der in den CLR-Metadaten für eine F#-Assembly codiert wird.

Endständige Aufrufe

Durch Unveränderlichkeit und funktionale Programmierung wird die Verwendung der Rekursion als Datenverarbeitungstool in F# unterstützt. Mithilfe eines einfachen rekursiven F#-Codeabschnitts kann beispielsweise eine F#-Liste durchlaufen und die Summe der Quadrate der Werte in der Liste erfasst werden:

let rec sumOfSquares nums =
    match nums with
    | [] -> 0
    | n :: rest -> (n*n) + sumOfSquares rest

Die Rekursion ist zwar häufig sehr praktisch, kann aber eine große Menge Arbeitsspeicher in dem Anrufstapel in Anspruch nehmen, da jede Iteration einen neuen Stapelrahmen hinzufügt. Bei ausreichend großen Eingaben kann dies sogar zu Stapelüberlaufausnahmen führen. Um dieses Stapelwachstum zu verhindern, kann rekursiver Code endrekursiv geschrieben werden; dies bedeutet, dass rekursive Aufrufe immer als letztes ausgeführt werden, kurz bevor die Funktion Folgendes zurückgibt:

let rec sumOfSquaresAcc nums acc = 
    match nums with 
    | [] -> acc
    | n :: rest -> sumOfSquaresAcc rest (acc + n*n)

Der F#-Compiler implementiert endrekursive Funktionen mithilfe von zwei Techniken, durch die sichergestellt werden soll, dass der Stapel nicht wächst. Für direkte endständige Aufrufe derselben Funktion, die definiert wird, beispielsweise der Aufruf von sumOfSquaresAcc, wandelt der F#-Compiler den rekursiven Aufruf in eine while-Schleife um, wodurch verhindert wird, dass ein Anruf erfolgt und Code generiert wird, der einer imperativen Implementierung derselben Funktion sehr ähnlich ist.

Die Endrekursion läuft nicht immer so einfach ab und kann stattdessen das Ergebnis mehrerer gegenseitig rekursiver Funktionen sein. In diesem Fall ist der F#-Compiler auf die systemeigene Unterstützung von endständigen Aufrufen von CLR angewiesen.

Die CLR verfügt über eine IL-Anweisung, die insbesondere bei der Endrekursion hilfreich ist: Das „tail“. IL-Präfix. Die tail.-Anweisung teilt der CLR mit, dass der Methodenstatus des Aufrufers verworfen werden kann, bevor der zugewiesene Aufruf ausgeführt wird. Das bedeutet, dass der Stapel bei diesem Aufruf nicht wächst. Dies bedeutet auch, dass die JIT, zumindest prinzipiell, den Aufruf effizient mithilfe einer einzigen jump-Anweisung ausführen kann. Dies ist hilfreich für F# und stellt sicher, dass die Endrekursion in fast allen Fällen sicher ist:

IL_0009:  tail.
IL_000b:  call    bool Program/SixThirtyEight::odd(int32)
IL_0010:  ret

In CLR 4.0 wurde einige wichtige Verbesserungen an der Behandlung von endständigen Aufrufen vorgenommen. Endständige Aufrufe wurden von der x64-JIT bisher sehr effizient implementiert, jedoch mithilfe einer Technik, die nicht auf alle Fälle angewendet werden konnte, in denen die tail.-Anweisung vorkam. Dies bedeutete, dass einige F#-Codeabschnitte, die erfolgreich auf x86-Plattformen ausgeführt wurden, auf x64-Plattformen mit einem Stapelüberlauf fehlschlugen. In CLR 4.0 wurde die effiziente Implementierung von endständigen Aufrufen durch die x64-JIT auf mehr Fälle erweitert, und es werden nun auch die aufwändigeren Mechanismen implementiert, die erforderlich sind, um sicherzustellen, dass endständige Aufrufe zu jeder Zeit angenommen werden, wie dies bei der x86 JIT der Fall ist.

Eine ausführliche Übersicht über die Verbesserungen für endständige Aufrufe in CLR 4.0 ist im CLR-Blog für die Codegenerierung (blogs.msdn.com/clrcodegeneration/archive/2009/05/11/tail-call-improvements-in-net-framework-4.aspx) verfügbar.

F# Interactive

F# Interactive ist ein Befehlszeilentool und Visual Studio-Toolfenster für das interaktive Ausführen von F#-Code (siehe Abbildung 1). Mit diesem Tool können ganz einfach mit Daten experimentiert, APIs untersucht und Anwendungslogik mithilfe von F# getestet werden. F# Interactive wird durch die Reflection.Emit-API von CLR ermöglicht. Diese API ermöglicht, dass ein Programm neue Typen und Member zur Laufzeit generiert und diesen neuen Code dynamisch aufruft. F# Interactive verwendet den F#-Compiler, um Code zu kompilieren, den der Benutzer an der Eingabeaufforderung eingibt, und verwendet dann Reflection.Emit, um die Typen, Funktionen und Member zu generieren, anstatt eine Assembly auf den Datenträger zu schreiben.

Executing Code in F# Interactive
Abbildung 1 Ausführen von Code in F# Interactive

Ein wichtiges Ergebnis dieses Ansatzes besteht darin, dass der ausgeführte Benutzercode vollständig kompiliert und vollständig in JIT ausgeführt wird, und keine interpretierte Version von F# ist, einschließlich aller nützlichen Optimierungen in beiden Schritten. Dies macht F# Interactive zu einer ausgezeichneten, leistungsstarken Umgebung zum Testen neuer Problemlösungsansätze und zum interaktiven Durchsuchen umfangreicher Datasets.

Tupel

Tupel in F# stellen eine einfache Möglichkeit zum Verpacken von Daten und Übergeben der Daten als Einheit dar, ohne dass neue benutzerdefinierte Typen definiert oder komplizierte Parameterschemas, wie z. B. Out-Parameter, zum Zurückgeben mehrerer Werte verwendet werden müssen.

let printPersonData (name, age) = 
    printfn "%s is %d years old" name age

let bob = ("Bob", 34)

printPersonData bob

    
let divMod n m = 
    n / m, n % m

let d,m = divMod 10 3

Tupel sind einfache Typen, die jedoch einige wichtige Eigenschaften in F# aufweisen. Die wichtigste Eigenschaft ist, dass sie unveränderlich sind. Nach der Erstellung können die Elemente eines Tupels nicht geändert werden. Daher können Tupel als bloße Kombination ihrer Elemente sicher behandelt werden. Dadurch wird auch eine weitere wichtige Eigenschaft von Tupeln ermöglicht: strukturelle Gleichheit. Tupel und andere F#-Typen, wie z. B. Listen, Optionen und benutzerdefinierte Datensätze und Unions, werden durch einen Vergleich ihrer Elemente auf Gleichheit geprüft.

In .NET Framework 4 sind Tupel nun ein wichtiger Datentyp, der in den Basisklassenbibliotheken definiert ist. Wenn Sie auf .NET Framework 4 abzielen, verwendet F# den System.Tuple-Typ zur Darstellung dieser Werte. Da dieser wichtige Typ in mscorlib unterstützt wird, können F#-Benutzer Tupel ganz einfach mit C#-APIs und umgekehrt gemeinsam nutzen.

Bei Tupeln handelt es sich zwar konzeptionell um einfache Typen, bei der Erstellung des System.Tuple-Typs gibt es jedoch viele interessante Entwurfsentscheidungen. Matt Ellis hat den Entwurfsprozess für Tupel in einer kürzlich veröffentlichten CLR Inside Out-Kolumne (msdn.microsoft.com/magazine/dd942829) beschrieben.

Optimierungen

Da F# weniger direkt in die CLR-Anweisungen übertragen wird, gibt es im F#-Compiler mehr Raum für Optimierungen, und die Abhängigkeit vom CLR-JIT-Compiler wird reduziert. Der F#-Compiler nutzt dies aus und implementiert wichtigere Optimierungen im Freigabemodus als der C#- und Visual Basic-Compiler.

Ein einfaches Beispiel ist die Zwischentupelbeseitigung. Tupel werden häufig zum Strukturieren von Daten verwendet, während diese verarbeitet werden. Tupel werden in der Regel erstellt und dann innerhalb eines einzigen Funktionstexts zerlegt. In diesem Fall entsteht eine unnötige Zuordnung eines Tupelobjekts. Da der F#-Compiler weiß, dass das Erstellen und Zerlegen eines Tupels keine nennenswerten Nebeneffekte haben kann, versucht er, das Zuordnen eines Zwischentupels zu verhindern.

In diesem Beispiel muss kein Tupelobjekt zugeordnet werden, da es nur durch Zerlegen in dem Mustervergleichsausdruck verwendet wird:

let getValueIfBothAreSame x y = 
    match (x,y) with
    | (Some a, Some b) when a = b -> Some a
    |_ -> None

Maßeinheiten

Maßeinheiten, z. B. Meter und Sekunden, werden in der Naturwissenschaft, im Ingenieurswesen und bei Simulationen häufig verwendet und sind im Wesentlichen ein Typsystem zum Arbeiten mit numerischen Mengen unterschiedlicher Arten. In F# werden Maßeinheiten direkt in das Typsystem der Sprache integriert, sodass numerische Mengen mit den entsprechenden Einheiten gekennzeichnet werden können. Diese Einheiten werden bei Berechnungen verwendet, und es werden Fehler gemeldet, wenn die Einheiten nicht übereinstimmen. Im folgenden Beispiel wird ein Fehler ausgegeben, wenn Sie versuchen, Kilogramm und Sekunden zu addieren. Es wird jedoch kein Fehler ausgegeben, wenn Sie Kilogramm durch Sekunden teilen.

[<Measure>] type kg
/// Seconds
[<Measure>] type s
    
let x = 3.0<kg>
//val x : float<kg>

let y = 2.5<s>
// val y : float<s>

let z = x / y
//val z : float<kg/s>

let w = x + y
// Error: "The unit of measure 's' 
// does not match the unit of measure 'kg'"

Maßeinheiten sind dank des F#-Typrückschlusses eine relativ einfache Ergänzung. Wenn der Typrückschluss verwendet wird, müssen vom Benutzer bereitgestellte Anmerkungen für Einheiten nur in Literalen und beim Akzeptieren von Daten von externen Quellen erscheinen. Der Typrückschluss verteilt diese dann über das Programm und überprüft, ob alle Berechnungen gemäß den verwendeten Einheiten korrekt durchgeführt werden.

Obwohl die Maßeinheiten Teil des F#-Typsystems sind, werden sie zur Kompilierzeit gelöscht. Dies bedeutet, dass die resultierende .NET-Assembly keine Informationen über Einheiten enthält, und die CLR behandelt die Werte in Einheiten als ihre zugrunde liegenden Typen. Auf diese Weise entsteht kein Leistungsaufwand. Dies steht im Gegensatz zu .NET-Generika, die zur Laufzeit voll verfügbar sind.

Wenn die CLR in Zukunft Maßeinheiten in das Haupt-CLR-Typsystem integrieren könnte, könnte F# die Informationen zu den Einheiten so verfügbar machen, dass sie für andere .NET-Programmiersprachen sichtbar sind.

Interaktive Verwendung von F#

Wie Sie gesehen haben, handelt es sich bei F# um eine ausdrucksstarke, funktionale, objektorientierte und untersuchende Programmiersprache für .NET Framework. Sie ist in Visual Studio 2010 integriert und umfasst auch die F# Interactive-Tools, mit deren Hilfe Sie sich gleich mit der Programmiersprache vertraut machen können.

Die Sprache und die Tools nutzen den vollen Umfang der CLR und führen einige Konzepte auf höherer Ebene ein, die den Metadaten und der IL der CLR zugeordnet sind. Bei F# handelt es sich dennoch nur um eine weitere .NET-Sprache, die dank des gemeinsamen Typsystems und der Laufzeit ganz einfach als eine Komponente neuer oder vorhandener .NET-Projekte integriert werden kann.    

Luke Hoban* ist Programmmanager für das F#-Team bei Microsoft. Bevor Luke zum F#-Team wechselte, war er der Programmmanager für den C#-Compiler und arbeitete an C# 3.0 und LINQ.*