Grundlagen zu F#

Einführung in die funktionale Programmierung für .NET-Entwickler

Chris Marinos

Beispielcode herunterladen.

Es ist gut möglich, dass Sie bereits von F# gehört haben, der neuesten Ergänzung der Microsoft Visual Studio-Sprachfamilie. Es gibt viele gute Gründe, F# zu erlernen – es verfügt über eine saubere Syntax, leistungsstarke Multithreadingfunktionen und vollständige Interoperabilität mit anderen Microsoft .NET Framework-Sprachen. F# umfasst jedoch einige wichtige neue Konzepte, die Sie verstehen sollten, bevor Sie die Features nutzen können.

Eine Blitztour ist eine gute Möglichkeit, mit dem Erlernen einer anderen objektorientierten Sprache oder sogar einer dynamischen Sprache wie Ruby oder Python zu beginnen. Das liegt daran, dass Sie die meisten Begriffe bereits kennen und nur die neue Syntax lernen müssen. F# ist jedoch anders. F# ist eine funktionale Programmiersprache, die mehr neue Begriffe als erwartet mit sich bringt. Darüber hinaus wurden funktionale Sprachen traditionell in akademischen Umgebungen verwendet, daher sind die Definitionen für die neuen Begriffe manchmal schwer zu verstehen.

Zum Glück ist F# nicht als akademische Sprache konzipiert. Die Syntax ermöglicht die Verwendung funktionaler Techniken zur Lösung von Problemen auf neuen, besseren Wegen, während weiterhin der objektorientierte und imperative Stil unterstützt wird, den Sie als .NET-Entwickler gewohnt sind. Im Gegensatz zu anderen .NET-Sprachen bedeutet die Multi-Paradigma-Struktur von F#, dass es Ihnen freisteht, den besten Programmierstil für das Problem zu finden, das Sie zu lösen versuchen. Funktionales Programmieren in F# bezieht sich auf das Schreiben von präzisem, leistungsfähigem Code zur Lösung praktischer Softwareprobleme. Es ermöglicht die Verwendung von Techniken wie Funktionen höherer Ordnung und Funktionskompositionen, um leistungsstarkes, einfach zu verstehendes Verhalten zu erreichen. Es zielt außerdem darauf ab, Ihren Code leichter verständlich zu machen, zu testen und zu parallelisieren, indem verborgene Komplexitäten entfernt werden.

Doch um all diese fantastischen Features von F# nutzen zu können, müssen Sie die Grundlagen verstehen. In diesem Artikel erläutere ich diese Konzepte mit Begriffen, die Sie als .NET-Entwickler bereits kennen. Ich zeige Ihnen außerdem einige funktionale Programmierungstechniken, die Sie auf den vorhandenen Code anwenden können, sowie einige Arten von funktionaler Programmierung, die Sie bereits verwenden. Am Ende des Artikels wissen Sie genug über funktionale Programmierung, dass Sie sofort mit F# in Visual Studio 2010 loslegen können.

Grundlagen der funktionalen Programmierung

Für die meisten .NET-Entwickler ist es einfach zu verstehen, was funktionale Programmierung ist, indem sie erfahren, was es nicht ist. Imperative Programmierung ist eine Art der Programmierung, die als das Gegenteil der funktionalen Programmierung betrachtet wird. Dies ist wahrscheinlich die Art von Programmierung, mit der Sie vertraut sind, da die meisten gängigen Programmiersprachen imperativ sind.

Funktionale Programmierung und imperative Programmierung unterscheiden sich grundlegend, und Sie können dies an einem einfachen Code erkennen:

int number = 0;
number++;

Damit wird offensichtlich eine Variable inkrementell um Eins erhöht. Das ist nicht besonders aufregend, aber denken Sie nach, wie Sie dieses Problem noch lösen könnten:

const int number = 0;
const int result = number + 1;

Die Zahl wird immer noch um Eins erhöht, aber sie wird nicht vor Ort geändert. Stattdessen wird das Ergebnis als eine weitere Konstante gespeichert, da der Compiler nicht zulässt, dass der Wert einer Konstante geändert wird. Man könnte sagen, dass Konstanten unveränderlich sind, da ihr Wert nicht mehr geändert werden kann, nachdem sie definiert wurden. Im Gegensatz dazu ist die Zahlenvariable aus dem ersten Beispiel veränderlich, da ihr Wert geändert werden kann. Diese beiden Vorgehensweisen zeigen den grundlegenden Unterschied zwischen imperativer Programmierung und funktionaler Programmierung. Imperative Programmierung betont die Verwendung veränderlicher Variablen, während funktionale Programmierung unveränderliche Variablen verwendet.

Die meisten .NET-Entwickler würden sagen, dass die Zahl und das Ergebnis in dem vorhergehenden Beispiel Variablen sind, doch als funktionaler Programmierer müssen Sie dies sorgfältiger unterscheiden. Die Idee einer konstanten Variable ist allenfalls verwirrend. Stattdessen sagen funktionale Programmierer, dass Zahl und Ergebnis Werte sind. Reservieren Sie den Begriff Variable für veränderliche Objekte. Diese Begriffe werden zwar nicht exklusiv für die funktionale Programmierung verwendet, aber sie sind bei der Programmierung im funktionalen Stil sehr viel wichtiger.

Dies ist scheinbar nur ein kleiner Unterschied, er bildet aber die Grundlage für eine Menge der Konzepte, die funktionale Programmierung so leistungsfähig machen. Veränderliche Variablen sind die Hauptursache für viele lästige Fehler. Wie Sie unten sehen können, führen sie zu impliziten Abhängigkeiten zwischen verschiedenen Teilen des Codes, was häufig zu Problemen führt, besonders in Bezug auf Parallelität. Im Gegensatz dazu bringen unveränderliche Variablen deutlich weniger Komplexität mit sich. Sie ermöglichen funktionale Techniken wie die Verwendung von Funktionen als Werte und zusammengesetzte Programmierung, die ich später noch ausführlicher erläutern werde.

Wenn Sie jetzt funktionale Programmierung eher skeptisch betrachten, machen Sie sich keine Sorgen! Das ist ganz normal. Die meisten imperativen Programmierer glauben, dass man mit unveränderlichen Werten nicht sinnvoll arbeiten kann. Doch betrachten Sie folgendes Beispiel:

string stringValue = "world!";
string result = stringValue.Insert(0, "hello ");

Die Insert-Funktion hat die Zeichenfolge „Hello World" erstellt, aber Sie wissen, dass „Insert“ nicht den Wert der Quellzeichenfolge verändert. Das liegt daran, dass Zeichenfolgen in .NET unveränderlich sind. Die Entwickler von .NET Framework verwendeten eine funktionale Herangehensweise, da es mit Zeichenfolgen einfacher war, besseren Code zu schreiben. Da Zeichenfolgen die am häufigsten verwendeten Datentypen in .NET Framework sind (zusammen mit anderen Basistypen wie Integer, DateTime usw.), ist es gut möglich, dass Sie schon mehr sinnvolle funktionale Programmierung vorgenommen haben, als Sie wissen.

Einsatz von F#

F# wird mit Visual Studio 2010 geliefert, und Sie finden die aktuellste Version unter msdn.microsoft.com/vstudio. Wenn Sie mit Visual Studio 2008 arbeiten, können Sie ein F#-Add-In aus dem F# Developer Center unter msdn.microsoft.com/fsharp herunterladen, wo Sie auch die Installationsanweisungen für Mono finden.

F# fügt ein neues Fenster namens F# Interactive zu Visual Studio hinzu, in dem Sie F#-Code interaktiv ausführen können. Betrachten Sie es als leistungsfähigere Version des Direktfensters, das Sie auch aufrufen können, wenn Sie sich nicht im Debugmodus befinden. Wenn Sie mit Ruby oder Python vertraut sind, werden Sie feststellen, dass F# Interactive eine Ausführen-Auswerten-Ausgeben-Schleife (Read-Evaluate-Print-Loop; REPL) ist, die sich als nützliches Tool beim Erlernen von F# und dem Experimentieren mit Code erweist.

Ich verwende F# Interactive in diesem Artikel, um Ihnen zu zeigen, was passiert, wenn Beispielcode kompiliert und ausgeführt wird. Wenn Sie Code in Visual Studio markieren und Alt+Eingabe drücken, senden Sie den Code an F# Interactive. Hier ist ein einfaches Additionsbeispiel in F#:

let number = 0
let result = number + 1

Wenn Sie diesen Code in F# Interactive ausführen, erhalten Sie Folgendes:

val number : int = 0
val result : int = 1

Sie können sich aufgrund des Begriffs „val“ wahrscheinlich denken, dass Zahl und Ergebnis beides unveränderliche Werte sind, keine veränderlichen Variablen. Dies sehen Sie, wenn Sie <- verwenden, den F#-Zuweisungsoperator:

> number <- 15;;

  number <- 15;;
  ^^^^^^^^^^^^

stdin(3,1): error FS0027: This value is not mutable
>

Da Sie wissen, dass funktionale Programmierung auf Unveränderlichkeit basiert, sollte dieser Fehler Sinn machen. Das Schlüsselwort „let“ wird verwendet, um unveränderliche Bindungen zwischen Namen und Werten zu erstellen. In C#-Begriffen ausgedrückt, ist in F# alles standardmäßig konstant. Sie können bei Bedarf eine veränderliche Variable erstellen, aber Sie müssen dies explizit angeben. Die Vorgaben sind genau das Gegenteil von dem, was Sie von imperativen Sprachen gewöhnt sind:

let mutable myVariable = 0
myVariable <- 15

Typrückschluss und Berücksichtigung von Leerraum

Sie können in F# Variablen und Werte deklarieren, ohne ihren Typ anzugeben, was den Schluss zulässt, dass F# eine dynamische Sprache ist, aber das trifft nicht zu. Beachten Sie unbedingt, dass F# eine statische Sprache wie C# oder C++ ist. F# hat jedoch ein leistungsfähiges Typrückschluss-System, aufgrund dessen Sie häufig nicht den Objekttyp angeben müssen. Dies ermöglicht eine einfache und prägnante Syntax, während gleichzeitig die Typsicherheit einer statischen Sprache geboten wird.

Obwohl Typrückschluss-Systeme wie dieses nicht wirklich in imperativen Sprachen vorhanden sind, ist Typrückschluss nicht direkt mit funktionaler Programmierung verknüpft. Typrückschluss ist jedoch ein wichtiges Features, das Sie verstehen sollten, wenn Sie F# erlernen möchten. Wenn Sie C#-Entwickler sind, ist es sogar möglich, dass Sie anhand des var-Schlüsselworts bereits mit grundlegenden Typrückschlüssen vertraut sind:

// Here, the type is explictily given
Dictionary<string, string> dictionary = 
  new Dictionary<string, string>();

// but here, the type is inferred
var dictionary = new Dictionary<string, string>();

Beide Zeilen des C#-Codes erstellen neue Variablen, die statisch als „Dictionary<string, string>“ eingegeben werden, doch das var-Schlüsselwort teilt dem Compiler mit, dass er den Typ der Variable ableiten soll. F# erweitert dieses Konzept auf die nächste Stufe. Hier sehen Sie ein Beispiel für eine Add-Funktion in F#:

let add x y =
    x + y
    
let four = add 2 2

In dem Code oben gibt es nicht eine einzige Typangabe, aber F# Interactive verrät die statische Eingabe:

val add : int -> int -> int
val four : int = 4

Ich werde die Pfeile später noch ausführlicher erläutern; für den Moment können Sie sie so auslegen, dass „add“ definiert wurde, um zwei int-Argumente zu übernehmen, und dass „four“ ein int-Wert ist. Der F#-Compiler war in der Lage, dies basierend auf der Definition von „add“ und „four“ abzuleiten. Die Regeln, die der Compiler dafür anwendet, sprengen den Rahmen dieses Artikels, Sie können aber bei Interesse im F# Developer Center mehr darüber erfahren.

Typrückschluss ist eine Methode, mit der F# überflüssigen Code reduziert, aber beachten Sie, dass es keine geschweiften Klammern oder Schlüsselwörter gibt, um den Inhalt oder den Rückgabewert der add-Funktion zu kennzeichnen. Das liegt daran, dass F# standardmäßig Leerräume berücksichtigt. In F# wird der Inhalt einer Funktion durch Einrücken angegeben, und der Rückgabewert steht in der letzten Zeile der Funktion. Wie schon der Typrückschluss hat auch die Berücksichtigung von Leerraum keine direkte Beziehung zur funktionalen Programmierung, aber Sie müssen das Konzept kennen, um F# verwenden zu können.

Nebeneffekte

Sie wissen nun, dass funktionale Programmierung anders als imperative Programmierung funktioniert, da sie auf unveränderlichen Werten statt auf veränderlichen Variablen beruht, aber diese Tatsache allein ist noch nicht sehr hilfreich. Der nächste Schritt besteht darin, die Nebeneffekte kennen zu lernen.

Bei der imperativen Programmierung hängt die Ausgabe einer Funktion von den Eingabeargumenten und dem aktuellen Status des Programms ab. Bei der funktionalen Programmierung sind die Funktionen nur von ihren Eingabeargumenten abhängig. Anders ausgedrückt, wenn Sie eine Funktion mehrmals mit demselben Eingabewert aufrufen, erhalten Sie immer denselben Ausgabewert. Dies ist bei der imperativen Programmierung aufgrund der Nebeneffekte anders, wie Sie in Abbildung 1 sehen können.

Abbildung 1 Nebeneffekte veränderlicher Variablen

public MemoryStream GetStream() {
  var stream = new MemoryStream();
  var writer = new StreamWriter(stream);
  writer.WriteLine("line one");
  writer.WriteLine("line two");
  writer.WriteLine("line three");
  writer.Flush();
  stream.Position = 0;
  return stream;
}

[TestMethod]
public void CausingASideEffect() {
  using (var reader = new StreamReader(GetStream())) {
    var line1 = reader.ReadLine();
    var line2 = reader.ReadLine();

    Assert.AreNotEqual(line1, line2);
  }
}

Beim ersten Aufruf von „ReadLine“ wird der Stream gelesen, bis eine neue Zeile kommt. Dann gibt „ReadLine“ den gesamten Text bis zur neuen Zeile zurück. Zwischen diesen Schritten wird eine veränderliche Variable, die die Streamposition darstellt, aktualisiert. Das ist der Nebeneffekt. Beim zweiten Aufruf von „ReadLine“ hat sich der Wert der veränderlichen Positionsvariable geändert, sodass „ReadLine“ einen anderen Wert zurückgibt.

Lassen Sie uns nun einen Blick auf die wichtigsten Konsequenzen aus der Verwendung der Nebeneffekte werfen. Betrachten wir zuerst eine einfache Klasse, „PiggyBank“, und einige Methoden, um damit zu arbeiten (siehe Abbildung 2).

Abbildung 2 PiggyBank - veränderlich

public class PiggyBank{
  public PiggyBank(int coins){
    Coins = coins;
  }

  public int Coins { get; set; }
}

private void DepositCoins(PiggyBank piggyBank){
  piggyBank.Coins += 10;
}

private void BuyCandy(PiggyBank piggyBank){
  if (piggyBank.Coins < 7)
    throw new ArgumentException(
      "Not enough money for candy!", "piggyBank");

  piggyBank.Coins -= 7;
}

Wenn Sie ein Sparschwein („PiggyBank“) mit 5 Geldstücken („Coins“) darin haben, können Sie „DepositCoins“ (Geld entnehmen) vor „BuyCandy“ (Bonbons kaufen) aufrufen, aber bei der umgekehrten Verwendung wird ein Fehler verursacht:

// this works fine
var piggyBank = new PiggyBank(5);

DepositCoins(piggyBank);
BuyCandy(piggyBank);

// but this raises an ArgumentException
var piggyBank = new PiggyBank(5);

BuyCandy(piggyBank);
DepositCoins(piggyBank);

Die Funktion „BuyCandy“ und die Funktion „DepositCoins“ aktualisieren beide den Status von „PiggyBank“ durch die Verwendung eines Nebeneffekts. Daher hängt das Verhalten jeder Funktion vom „PiggyBank“-Status ab. Da die Anzahl von „Coins“ veränderlich ist, ist die Reihenfolge der Funktionsausführung wichtig. Anders gesagt, es gibt eine implizite zeitliche Abhängigkeit zwischen diesen beiden Methoden.

Lassen Sie uns nun die Anzahl von „Coins“ mit einem Schreibschutz versehen, um eine unveränderliche Datenstruktur zu simulieren. Abbildung 3 zeigt, dass „BuyCandy“ und „DepositCoins“ nun neue PiggyBank-Objekte zurückgeben, statt eine vorhandene „PiggyBank“ zu aktualisieren.

Abbildung 3 PiggyBank - unveränderlich

public class PiggyBank{
  public PiggyBank(int coins){
    Coins = coins;
  }

  public int Coins { get; private set; }
}

private PiggyBank DepositCoins(PiggyBank piggyBank){
  return new PiggyBank(piggyBank.Coins + 10);
}

private PiggyBank BuyCandy(PiggyBank piggyBank){
  if (piggyBank.Coins < 7)
    throw new ArgumentException(
      "Not enough money for candy!", "piggyBank");

  return new PiggyBank(piggyBank.Coins - 7);
}

Wie schon zuvor erhalten Sie einen Argument-Ausnahmefehler, wenn Sie versuchen, „BuyCandy“ vor „DepositCoins“ aufzurufen:

// still raises an ArgumentException
var piggyBank = new PiggyBank(5);

BuyCandy(piggyBank);
DepositCoins(piggyBank);

Doch nun erhalten Sie dasselbe Ergebnis auch dann, wenn Sie die Reihenfolge umkehren:

// now this raises an ArgumentException,  too!
var piggyBank = new PiggyBank(5);

DepositCoins(piggyBank);
BuyCandy(piggyBank);

In diesem Fall sind „BuyCandy“ und „DepositCoins“ nur von ihrem Eingabeargument abhängig, da die Coin-Anzahl unveränderlich ist. Sie können die Funktionen in einer beliebigen Reihenfolge ausführen, das Ergebnis bleibt gleich. Die implizite zeitliche Abhängigkeit ist nicht mehr vorhanden. Da Sie jedoch wahrscheinlich wollen, dass „BuyCandy“ gelingt, müssen Sie das Ergebnis von „BuyCandy“ von der „DepositCoins“-Ausgabe abhängig machen. Machen Sie also die Abhängigkeit explizit:

var piggyBank = new PiggyBank(5);
BuyCandy(DepositCoins(piggyBank));

Dies ist ein kleiner Unterschied mit weit reichenden Konsequenzen. Gemeinsame veränderliche Zustände und implizite Abhängigkeiten sind die Ursache für die schlimmsten Fehler in imperativem Code, und sie sind der Grund dafür, dass Multithreading in imperativen Sprachen so schwierig ist. Wenn die Reihenfolge, in der die Funktionen ausgeführt werden, wichtig ist, müssen Sie sich auf umständliche Sperrmechanismen verlassen, um nichts durcheinander zu bringen. Rein funktionale Programme haben keine Nebeneffekte und implizite zeitliche Abhängigkeiten, daher ist die Reihenfolge, in der die Funktionen ausgeführt werden, unerheblich. Das heißt, Sie müssen sich keine Gedanken über Sperrmechanismen und andere fehleranfällige Multithreading-Techniken machen.

Einfacheres Multithreading ist einer der Hauptgründe dafür, dass funktionaler Programmierung in letzter Zeit mehr Aufmerksamkeit gewidmet wird, aber es gibt noch mehr Vorteile bei der Programmierung im funktionalen Stil. Funktionen ohne Nebeneffekte können leichter getestet werden, da jede Funktion ausschließlich von den Eingabeargumenten abhängig ist. Sie sind einfacher zu verwalten, da sie nicht implizit auf Logik aus anderen Setup-Funktionen basieren. Funktionen ohne Nebeneffekte sind außerdem meist kürzer und können besser kombiniert werden. Auf den letzten Punkt werde ich gleich noch im Detail eingehen.

In F# liegt der Schwerpunkt auf der Bewertung von Funktionen nach ihren Ergebniswerten statt nach ihren Nebeneffekten. In imperativen Sprachen wird eine Funktion häufig aufgerufen, um etwas auszuführen; in funktionalen Sprachen werden Funktionen aufgerufen, um ein Ergebnis zu liefern. Dies sehen Sie in F#, wenn Sie sich die if-Anweisung anschauen:

let isEven x =
    if x % 2 = 0 then
        "yes"
    else
        "no"

Sie wissen, dass in F# die letzte Zeile einer Funktion der Rückgabewert ist, doch in diesem Beispiel ist die letzte Zeile der Funktion die if-Anweisung. Dies ist kein Compiler-Trick. In F# dienen selbst if-Anweisungen zur Rückgabe von Werten:

let isEven2 x =
    let result = 
        if x % 2 = 0 then
            "yes"
        else
            "no"
    result

Der Ergebniswert hat den Typ „String" und wird der if-Anweisung direkt zugewiesen. Dies funktioniert ähnlich wie der bedingte Operator in C#:

string result = x % 2 == 0 ? "yes" : "no";

Der bedingte Operator ist hauptsächlich auf die Wertrückgabe, nicht auf den Nebeneffekt ausgerichtet. Dies ist ein eher funktionaler Ansatz. Im Gegensatz dazu ist die if-Anweisung in C# eher imperativ, da sie kein Ergebnis zurückgibt. Sie verursacht lediglich Nebeneffekte.

Zusammengesetzte Funktionen

Nachdem Sie nun einige der Vorteile der Funktionen ohne Nebeneffekte kennen gelernt haben, können Sie das ganze Potenzial der Funktionen in F# nutzen. Lassen Sie uns mit einem C#-Code beginnen, um das Quadrat der Zahlen von 0 bis 10 zu bilden:

IList<int> values = 0.Through(10).ToList();

IList<int> squaredValues = new List<int>();

for (int i = 0; i < values.Count; i++) {
  squaredValues.Add(Square(values[i]));
}

Abgesehen von den Hilfsmethoden „Through“ und „Square“ ist dieser Code mehr oder weniger C#-Standard. Gute C#-Entwickler sind wahrscheinlich über die Verwendung einer For-Schleife statt einer Foreach-Schleife verwundert, und sie haben recht. Moderne Sprachen wie C# bieten Foreach-Schleifen als Abstraktion, um das Durchlaufen von Enumerationen zu erleichtern, indem sie explizite Indexer überflüssig machen. Sie erreichen dieses Ziel, doch betrachten Sie den Code in Abbildung 4.

Abbildung 4 Verwendung von Foreach-Schleifen

IList<int> values = 0.Through(10).ToList();

// square a list
IList<int> squaredValues = new List<int>();

foreach (int value in values) {
  squaredValues.Add(Square(value));
}

// filter out the even values in a list
IList<int> evens = new List<int>();

foreach(int value in values) {
  if (IsEven(value)) {
    evens.Add(value);
  }
}

// take the square of the even values
IList<int> results = new List<int>();

foreach (int value in values) {
  if (IsEven(value)) {
    results.Add(Square(value));
  }
}

Die Foreach-Schleifen in diesem Beispiel sind ähnlich, doch jeder Schleifeninhalt führt einen leicht abweichenden Vorgang durch. Imperative Programmierer haben sich bisher nicht an dieser Codeduplizierung gestört, da sie ihn als idiomatischen Code angesehen haben.

Funktionale Programmierer gehen hier anders heran. Statt Abstraktionen wie Foreach-Schleifen zu erstellen, um Listen zu durchlaufen, verwenden sie Funktionen ohne Nebeneffekte:

let numbers = {0..10}
let squaredValues = Seq.map Square numbers

Dieser F#-Code bildet ebenfalls das Quadrat einer Folge von Zahlen, jedoch unter Verwendung einer Funktion höherer Ordnung. Funktionen höherer Ordnung sind Funktionen, die eine andere Funktion als Eingabeargument akzeptieren. In diesen Fall akzeptiert die Funktion „Seq.map“ die Square-Funktion als Argument. Sie wendet diese Funktion auf jede Zahl in der Zahlenfolge an und gibt die Folge der Quadratzahlen zurück. Diese Funktionen höherer Ordnung sind der Grund, warum viele Leute sagen, dass funktionale Programmierung Funktionen als Daten verwendet. Das bedeutet ganz einfach, dass Funktionen als Parameter verwendet werden oder einem Wert bzw. einer Variable zugeordnet werden können, wie eine Ganzzahl oder eine Zeichenfolge. In C# ausgedrückt entspricht dies in etwa den Konzepten von Delegaten und Lambda-Ausdrücken.

Funktionen höherer Ordnung sind eine der Techniken, die funktionale Programmierung so leistungsfähig machen. Sie können Funktionen höherer Ordnung verwenden, um den duplizierten Code in Foreach-Schleifen zu isolieren und in eigenständige, Nebeneffekt-freie Funktionen zu kapseln. Diese Funktionen führen jeweils einen kleinen Vorgang aus, den der Code in der Foreach-Schleife bearbeitet hätte. Da es keine Nebeneffekte gibt, können Sie diese Funktionen kombinieren, um besser lesbaren, einfacher zu verwaltenden Code zu erstellen, der das Gleiche wie Foreach-Schleifen erzielt:

let squareOfEvens = 
    numbers
    |> Seq.filter IsEven
    |> Seq.map Square

Das einzig Verwirrende an diesem Code ist der Operator |>. Dieser Operator dient dazu, den Code besser lesbar zu machen, indem Sie die Argumente für eine Funktion neu sortieren können, sodass Sie das letzte Argument als Erstes lesen. Die Definition ist sehr einfach:

let (|>) x f = f x

Ohne den Operator |> sieht der Code für „squareOfEvens“ wie folgt aus:

let squareOfEvens2 = 
  Seq.map Square (Seq.filter IsEven numbers)

Wenn Sie LINQ verwenden, sollte Ihnen die Verwendung von Funktionen höherer Ordnung auf diese Weise vertraut sein. Das liegt daran, dass LINQ tief in der funktionalen Programmierung verwurzelt ist. Genau genommen können Sie die Lösung für „squareOfEvens“ mit LINQ-Methoden in C# übersetzen:

var squareOfEvens =
  numbers
  .Where(IsEven)
  .Select(Square);

Dies wird in die folgende LINQ-Abfragesyntax übersetzt:

var squareOfEvens = from number in numbers
  where IsEven(number)
  select Square(number);

Wenn Sie LINQ in C#- oder Visual Basic-Code verwenden, können Sie einige Vorteile der funktionalen Programmierung im alltäglichen Gebrauch erkunden. Dies ist eine hervorragende Möglichkeit, funktionale Programmierungstechniken zu erlernen.

Wenn Sie anfangen, Funktionen höherer Ordnung regelmäßig zu verwenden, stoßen Sie irgendwann auf eine Situation, in der Sie eine kleine, sehr spezifische Funktion an eine Funktionen höherer Ordnung übergeben möchten. Funktionale Programmierer verwenden Lambda-Funktionen, um dieses Problem zu lösen. Lambda-Funktionen sind einfache Funktionen, die Sie definieren, ohne ihnen einen Namen zu geben. Sie sind normalerweise kurz und haben eine spezifische Verwendung. Hier ist beispielsweise eine weitere Möglichkeit, wie Sie die Quadrate von geraden Zahlen mit Lambda berechnen können:

let withLambdas =
    numbers
    |> Seq.filter (fun x -> x % 2 = 0)
    |> Seq.map (fun x -> x * x)

Der einzige Unterschied zwischen diesem und dem vorhergehenden Code besteht darin, dass „Square“ und „IsEven“ als Lambdas definiert sind. In F# deklarieren Sie eine Lambda-Funktion mit dem fun-Schlüsselwort. Sie sollten Lambdas nur verwenden, um Funktionen zur einmaligen Verwendung zu deklarieren, da diese nicht einfach außerhalb ihres definierten Kontexts verwendet werden können. Aus diesem Grund sind „Square“ und „IsEven“ nicht gut für Lambda-Funktionen geeignet, da sie in vielen Situationen nützlich sind.

Currying und partielle Anwendung

Sie wissen nun fast alles über die Grundlagen, die Sie für die ersten Schritte mit F# benötigen, doch es gibt noch ein weiteres Konzept, das Sie kennen sollten. Im vorhergehenden Beispiel sind der Operator |> und die Pfeile in Typsignaturen von F# Interactive an ein Konzept gebunden, das so genannte Currying.

Currying bedeutet das Aufschlüsseln einer Funktion mit vielen Argumenten in eine Reihe von Funktionen, die jeweils ein Argument übernehmen und letztendlich dasselbe Ergebnis wie die ursprüngliche Funktion erzielen. Currying ist wahrscheinlich das herausforderndste Thema in diesem Artikel für .NET-Entwickler, besonders da es häufig mit partieller Anwendung verwechselt wird. In diesem Beispiel sehen Sie beide im Einsatz:

let multiply x y =
    x * y
    
let double = multiply 2
let ten = double 5

Sie sehen wahrscheinlich sofort, dass das Verhalten anders als bei den meisten imperativen Sprachen ist. Die zweite Anweisung erstellt eine neue Funktion, genannt „double“, indem sie ein Argument an eine Funktion übergibt, die zwei übernimmt. Das Ergebnis ist eine Funktion, die ein int-Argument akzeptiert und dasselbe Ergebnis erzielt, als wenn Sie mit x = 2 und y = Argument multipliziert hätten. Was das Verhalten angeht, stellt dieser Code dasselbe dar:

let double2 z = multiply 2 z

Häufig sagen Benutzer fälschlicherweise, dass Currying auf „multiply“ angewendet wird, um „double“ zu erhalten. Doch das stimmt nur teilweise. Es wurde zwar Currying auf die Funktion „multiply“ angewendet, doch das erfolgte bereits bei der Definition, da Funktionen in F# standardmäßig immer mit Currying verarbeitet werden. Beim Erstellen der Funktion „double“ ist es richtiger zu sagen, dass die Funktion „multiply“ partiell angewendet wird.

Lassen Sie uns diese Schritte ausführlicher betrachten. Beim Currying wird eine Funktion mit vielen Argumenten in eine Reihe von Funktionen aufgeschlüsselt, die jeweils ein Argument übernehmen und letztendlich dasselbe Ergebnis wie die ursprüngliche Funktion erzielen. Die Funktion „multiply“ hat laut F# Interactive die folgende Typsignatur:

val multiply : int -> int -> int

Bis zu diesem Punkt haben Sie dies so entschlüsselt, dass „multiply“ eine Funktion ist, die ein int-Argument übernimmt und ein int-Ergebnis zurückgibt. Ich werde nun erklären, was wirklich passiert. Die Funktion „multiply“ ist eigentlich eine Serie aus zwei Funktionen. Die erste Funktion übernimmt ein int-Argument und gibt eine andere Funktion zurück, sodass x effektiv an einen bestimmten Wert gebunden wird. Diese Funktion akzeptiert außerdem ein int-Argument, das Sie als den Wert, der an y gebunden wird, betrachten können. Nach dem Aufruf dieser zweiten Funktion sind x und y beide gebunden, daher ist das Ergebnis das Produkt aus x und y, wie im Inhalt von „double“ definiert ist.

Zum Erstellen von „double“ wird die erste Funktion in der Kette der „multiply“-Funktionen so ausgewertet, dass „multiply“ partiell angewendet wird. Die resultierende Funktion erhält den Namen „double“. Bei der Auswertung von „double“ wird dessen Argument zusammen mit dem partiell angewendeten Wert verwendet, um das Ergebnis zu erstellen.

Verwenden von F# und funktionaler Programmierung

Nachdem Sie nun genug Begriffe kennen gelernt haben, um mit F# und funktionaler Programmierung beginnen zu können, stehen Ihnen zahlreiche Möglichkeiten für die nächsten Schritte offen.

Mit F# Interactive können Sie F#-Code erkunden und F#-Skripts schnell erstellen. Es ist außerdem gut geeignet, um alltägliche Fragen über das Verhalten von .NET-Bibliotheksfunktionen zu überprüfen, ohne die Hilfedateien oder die Websuche zu Rate zu ziehen.

Mit F# lassen sich besonders komplizierte Algorithmen gut ausdrücken, daher können Sie diese Teile Ihrer Anwendung in F#-Bibliotheken kapseln, die dann von anderen .NET-Sprachen aufgerufen werden können. Dies ist besonders bei Multithread- oder Konstruktionsanwendungen hilfreich.

Darüber hinaus können Sie funktionale Programmiertechniken in der alltäglichen .NET-Entwicklung anwenden, ohne extra F#-Code zu schreiben. Verwenden Sie LINQ statt For- oder Foreach-Schleifen. Probieren Sie die Verwendung von Delegaten zum Erstellen von Funktionen höherer Ordnung aus. Schränken Sie die Verwendung von Veränderlichkeit und Nebeneffekten in Ihrer imperativen Programmierung ein. Nachdem Sie einmal begonnen haben, Code im funktionalen Stil zu schreiben, werden Sie schnell feststellen, dass Sie gern mehr F#-Code schreiben würden.                     

Chris Marinos ist Softwareberater bei SRT Solutions in Ann Arbor, Michigan, USA. Sie können seine Vorträge zu F#, funktionaler Programmierung und anderen interessanten Themen bei Veranstaltungen in der Nähe von Ann Arbor hören oder seinen Blog unter srtsolutions.com/blogs/chrismarinos lesen.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Luke Hoban