F# - Einstieg und praktische Anwendung

Wichtige Sprachstrukturen in F#
Kommentare

In diesem Kapitel finden Sie all die grundlegenden Informationen, die Sie zur Struktur und zum syntaktischen Aufbau von F#-Programmen brauchen, um den Beispielen komfortabel folgen zu können. Nehmen Sie sich die Zeit, selbst ein wenig mit der Sprache zu spielen, zum Beispiel mit der interaktiven Umgebung FSI. Die meisten Programmierer können sich alles Neue wesentlich besser merken, wenn sie es schon einmal selbst ausprobiert haben.

Werte und Variablen

So wie die meisten Programmiersprachen, kennt auch F# die Idee des „benannten Wertes“. In vielen Sprachen spricht man von „Variablen“, die „Werte“ speichern. Solche Variablen haben gewöhnlich einen Typ, der den Datentyp des Wertes bestimmt, der in ihnen gespeichert werden kann. Es ist ebenso üblich, dass der Wert einer Variablen sich während des Programmablaufs beliebig und beliebig häufig verändern kann − daher stammt die Bezeichnung „Variable“. Variablen im beschriebenen Sinne gibt es in F# auch, allerdings werden Sie stattdessen meistens mit unveränderbaren Werten arbeiten. Die Idee, Werte nach ihrer anfänglichen Zuweisung nicht mehr zu ändern (englisch: „Immutability“), wird von vielen funktionalen Programmiersprachen umgesetzt und stellt auch in F# den Standard dar. Hier sehen Sie einige typische Wertzuweisungen:

let intVal = 10
let strVal = "hi there"
let boolVal = true
let floatVal = 12.3
let decimalVal = 47.99m 

Wie Sie sehen, beginnt jede Wertzuweisung mit dem Schlüsselwort let. Nur der Klarheit halber: die Namen der Werte sind intVal, strVal, boolValfloatVal und decimalVal, die Werte selbst sind 10, „hi there“, true, 12.3 und 47.99 m. Man bezeichnet oft die direkt im Quelltext erscheinenden Werte als „Literals“. 

F# ist eine statisch typisierte Sprache. Daraus ergibt sich unter anderem, dass alle Werte Typen haben, die bereits zur Zeit des Compiler-Laufs feststehen. Auf den ersten Blick scheinen im obigen Code die Werte allerdings typlos zu sein. Der Grund hierfür ist, dass F#, ganz in der Tradition funktionaler Programmiersprachen, sehr stark auf die automatische Herleitung von Typen (englisch: „type inference“) durch den Compiler setzt. Auch in anderen .NET-Sprachen hat diese Technik mittlerweile Einzug gehalten, allerdings in relativ stark eingeschränkter Form. In F# ist die Umsetzung so leistungsfähig, dass eine explizite Erwähnung eines Typs im Code eine seltene Ausnahme darstellt.

Abbildung 1.7: Typherleitung in IntelliSense

Um Ihrem Verständnis der Typherleitung etwas zu helfen, bietet Visual Studio die Möglichkeit, direkt im Editor mit der Maus auf einen Wertnamen zu zeigen. Daraufhin erscheint ein Popup, in dem die Details zum hergeleiteten Typ angezeigt werden. Sie werden im Laufe der Zeit sehen, dass dies speziell bei komplexeren Typen eine wertvolle Hilfe ist. Wie bereits erwähnt, können Sie einen Wert, nachdem er anfänglich zugewiesen wurde, nicht mehr ändern. Sollten Sie versuchen, diese Aussage in FSI zu überprüfen, werden Sie überrascht sein:

> let intVal = 10;;
val intVal : int = 10
> let intVal = 30;;
val intVal : int = 30
> 

In seiner interaktiven Umgebung erlaubt es FSI, Werte neu zu definieren. Wenn Sie hingegen dieselben beiden Zuweisungen in einem kompilierten F#-Programm auszuführen versuchen, hindert der Compiler Sie daran mit der Fehlermeldung Duplicate definition of value ‚intVal‘. Der Vollständigkeit halber sei an dieser Stelle erwähnt, dass F# auch den Umgang mit änderbaren Werten ermöglicht, was hin und wieder besonders für den Umgang mit APIs des .NET Frameworks erforderlich ist. Mehr dazu finden Sie im Kapitel „Umgang mit Daten“.

[ header = Funktionen ]

Funktionen

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

let square x = x * x 

Diese Zeile deklariert eine Funktion namens square, mit einem Parameter x und einem Rückgabewert, der berechnet wird aus x multipliziert mit sich selbst. Die Benennung einfacher Funktionen in Kleinbuchstaben, oder zumindest mit einem kleinen Buchstaben am Anfang, ist in F# üblich. Für längere Bezeichner wählen die meisten Programmierer camel casing, zum Beispiel: calculateThisAndThat. Weil F# als Programmiersprache auf der .NET-Plattform basiert, verwenden manche Programmierer auch Benennungen, die sich in anderen .NET-Sprachen durchgesetzt haben. Die Konvention ist hier nicht ganz eindeutig, und Sie sollten sich selbst mit Ihren Teamkollegen einigen.

Abbildung 1.8: Typherleitung für eine Funktion

Wenn Sie sich in Visual Studio die Informationen zur Typherleitung für die neue Funktion anschauen, wird Ihnen folgender Typ angezeigt: int ➝ int. Man liest diesen Typ als int goes to int. Die Bedeutung ist einfach: Die Funktionen nimmt einen Parameter vom Typ int entgegen und erzeugt ihrerseits einen Rückgabewert, der ebenfalls vom Typ int ist. Dieses Prinzip lässt sich auch auf komplexere Funktionsdeklarationen übertragen.
Hier ist eine Funktion mit zwei Parametern:

let add x y =
x + y 

Der Typ der Funktion add wird vom Compiler als int ➝ intint, gesprochen int goes to int goes to int hergeleitet. Die technischen Hintergründe zu dieser Verkettung werden in einem späteren Kapitel noch im Detail beleuchtet, aber der Zusammenhang mit den Parameter- und Rückgabetypen ist offensichtlich.

Zwei weitere Details zu dieser Funktionsdeklaration sind Ihnen vielleicht bereits aufgefallen. Als Erstes ist da der Rückgabewert, der ohne explizites return erzeugt wird. Die zugrunde liegende Annahme ist, dass Funktionen immer einen Rückgabewert haben − dafür ist deswegen keine besondere Syntax erforderlich. Der Wert des letzten Ausdrucks in einer Funktion ist automatisch auch der Wert, der zurückgegeben wird. In der Funktion square ist der letzte Ausdruck x * x, in der Funktion add ist es x + y. Das Resultat dieser Rechenoperationen wird damit jeweils automatisch aus der Funktion zurückgegeben.

Das zweite Detail ist, dass die Funktion add, anders als square, nicht in einer einzelnen Zeile deklariert wird. Es ist zwar üblich, Funktionen kurz zu deklarieren und sich auch der syntaktischen Fähigkeiten der Sprache zu bedienen, um Code kurz und prägnant zu gestalten, aber die Sprache bietet auch alle notwendigen Mittel, um komplexe Funktionen übersichtlich zu gestalten und zu formatieren. Wichtig ist allerdings, dass es keine besonderen Schlüsselwörter oder syntaktische Elemente zur Blockbildung gibt. Stattdessen werden Blöcke einzig durch die Einrückung des Codes erzeugt.

Es wäre also ungültig, die Funktion add so zu definieren:

let add x y =
x + y 

Andererseits dürfen Sie natürlich die bereits gezeigten Beispiele kombinieren. Eine alternative Implementierung der Funktion add könnte etwa wie folgt aussehen:

let add' x y =
let result =
x + y
result 

Beachten Sie, dass diese alternative Funktion jetzt den Namen add‘ − gesprochen: add Strich − trägt. In funktionalen Sprachen ist diese an die Mathematik angelehnte Notation, wenn es um mehrere sehr ähnliche oder alternativ verwendbare Varianten einer Funktion geht, nicht ungewöhnlich. Allerdings könnten Sie eine solche Funktion natürlich ebenso gut mit einem völlig anderen Namen versehen oder, wie etwa in C# üblich, add_ verwenden.

Zurück zur Implementierung für einen Augenblick. In der Variante add‘ erstreckt sich die Funktion nunmehr über vier Zeilen. Innerhalb der Funktion wird ein Wert namens result deklariert, dem das Ergebnis der Berechnung zunächst zugewiesen wird. Durch die Nennung des Wertnamens result in der letzten Zeile der Funktion wird dieser Wert zum Aufrufer zurückgegeben. Die Einrücktiefe der einzelnen Zeilen der Funktion bestimmt die Blockbildung.

Zum Umgang mit Funktionen fehlt bisher noch ein wichtiges Element: das Aufrufen von Funktionen. Hier sind zwei Beispiele für den Aufruf der Funktionen square und add

let square11 = square 11
let add5and3 = add 5 3 

Wie Sie sehen, werden die Parameter für die Funktionsaufrufe jeweils einfach durch Leerzeichen getrennt hinter dem Namen der Funktion notiert. Es gibt keine weiteren syntaktischen Elemente für die Parameterliste selbst, wie etwa Klammern oder trennende Kommata. Der Rückgabewert eines Funktionsaufrufs kann natürlich wiederum in einem benannten Wert abgelegt werden.

Interessanter wird es, wenn Funktionsaufrufe kombiniert werden. Sie haben bereits im Einführungskapitel die Hilfsfunktion printfn kennen gelernt, mit der Informationen auf der Konsole ausgegeben werden können. Die folgende Zeile verwendet diese Hilfsfunktion, um das Resultat eines anderen Funktionsaufrufs direkt auszugeben.

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

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

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

[ header = Module, Namespaces, Eintrittspunkte und Kompilierreihenfolge]

Module, Namespaces, Eintrittspunkte und Kompilierreihenfolge

Die meisten Programmiersprachen bieten Möglichkeiten, Funktionen, Methoden oder ähnliche Bausteine der Implementierung strukturiert aufzuteilen, sodass nicht alle derartigen Elemente sich zu jeder Zeit gleichermaßen im Zugriff befinden. In fast jedem echten Programm wissen Programmierer diese Möglichkeiten zu schätzen, da das Volumen der erzeugten Codeelemente sehr schnell unübersichtlich wird und es zu Konflikten in der Namensgebung kommt. Stellen Sie sich einmal vor, Sie müssten in einem größeren Programm jede Funktion oder Methode mit einem global eindeutigen Namen versehen! Das ist glücklicherweise nicht notwendig, eben wegen der angesprochenen Funktionalität der strukturierten Kapselung.

Als hybride Programmiersprache bietet F# eine Reihe unterschiedlicher Mechanismen, die zum Zweck der Kapselung verwendet werden können. Dazu zählen auch die aus der objektorientierten Programmierung bekannten Klassen, mit deren Hilfe eine Modellierung von Zusammenhängen aus der wirklichen Welt und eine Strukturierung von Code und Datenhaltung gleichermaßen möglich ist. Unabhängig von objektorientierten Einflüssen jedoch gibt es auch bereits leistungsfähige Strukturierungsmöglichkeiten

in Form der Module, kombiniert mit den in .NET generell verwendeten Namespaces (manchmal im Deutschen als „Namensräume“ übersetzt). Module sind Strukturen, die Funktionen gliedern können, vergleichbar etwa mit statischen Klassen in C#. Der Compiler erzeugt auf IL-Ebene tatsächlich statische Klassen für die F#-Module.

Wenn Sie in einer F#-Quelltextdatei nicht explizit ein Modul deklarieren, erzeugt der Compiler automatisch eines, das den Namen der Datei als Modulnamen verwendet. Dieser Mechanismus ermöglicht es, wie Sie es in den bisherigen Beispielen bereits gesehen haben, ohne weitere syntaktische Strukturen z. B. Wertezuweisungen oder Funktionsdeklarationen in eine Quelltextdatei zu schreiben. Wenn Sie möchten, ist es allerdings auch möglich, am Beginn einer Quelltextdatei den Namen des dafür zuerzeugenden Moduls explizit zu nennen:

module Calculator
let square x = x * x
let add x y = x + y
let mult x y = x * y

Auf diese Weise ist es möglich, die Datei anders zu nennen als das darin enthaltene Modul − aus Gründen der Wartbarkeit ist das allerdings nicht empfehlenswert.

Nachdem Sie Ihrem ersten F#-Programm ein neues Modul hinzugefügt haben, werden Sie zunächst sehen, dass der Compiler mit der Projektstruktur nun nicht mehr zufrieden ist. Um den Einstieg in die Programmiersprache oder das schnelle und einfache Erzeugen von kleinen Programmen so einfach wie möglich zu gestalten, verwendet der Compiler eine Reihe von automatischen Komfortmechanismen. Dazu zählt z. B. die automatische Erzeugung eines Moduls, solange Sie noch keines selbst angegeben haben. Sobald Sie jedoch beginnen, Ihre eigenen zusätzlichen Quelltextdateien und Module zum Projekt hinzuzufügen, müssen Sie auch andere notwendige Teile der Struktur selbst in die Hand nehmen. So ist es notwendig, nachdem Sie ein neues Modul zu ihrem Programm hinzugefügt haben, auch in der Standarddatei Program.fs explizit eine module-Instruktion zu notieren.

Auch dieser Schritt führt allerdings nicht sofort zum Erfolg. Der Compiler wird Ihren Code nun verarbeiten, aber auch eine Warnung ausgeben mit dem Hinweis, dass Ihr Programm bei Ausführung nichts tun wird, da keine Markierung für den Programmeinstiegspunkt gefunden werden konnte. Um den Compiler wissen zu lassen, wie Ihr Programm genau gestartet werden soll, erzeugen Sie eine Funktion mit einer bestimmten Signatur und markieren diese mit dem Attribut EntryPoint. Die Signatur der Startfunktion ist string [] ➝ int − mit anderen Worten, es wird ein Eingabeparameter vom Typ eines String Arrays erwartet und ein Rückgabewert vom Typ int.

Um ein einfaches Testprogramm nach Hinzufügen des Moduls Calculator wieder korrekt kompilieren zu lassen, müsste Ihr Hauptprogramm etwa so aussehen:

module Program
[<EntryPoint>]
let main args =
printfn "Square 11: %d" (Calculator.square 11)
printfn "Add 5 and 3: %d" (Calculator.add 5 3)
printfn "Add 5 and the square of 11: %d"
(Calculator.add 5 (Calculator.square 11))
0

Nach all dieser strukturellen Arbeit erwarten Sie nun sicherlich, dass Ihr Programm endlich korrekt ausführbar ist. Es gibt allerdings noch immer ein Problem, das eventuell beim Compiler-Durchlauf auftreten könnte. Dieses Problem kann sich entweder durch Meldungen äußern, die sich auf ein unbekanntes Modul Calculator beziehen, oder durch die folgende Fehlermeldung:

A function labeled with the ‚EntryPointAttribute‘ attribute must be the last declaration in the last file in the compilation sequence, and can only be used when compiling to a .exe

Diese neuen Schwierigkeiten können auftreten, weil in F# die Reihenfolge der Quelltextdateien beim Compiler-Lauf relevant ist. Das ist ein Unterschied zu anderen .NET- Programmiersprachen, aber eine Gemeinsamkeit mit manchen anderen funktionalen Sprachen außerhalb der .NET-Welt. In einem Projekt mit mehreren Quelltextdateien bekommen Sie deshalb Fehlermeldungen wie die oben erwähnten, wenn Sie die Reihenfolge nicht beachtet haben. Wenn Sie an der Kommandozeile den Compiler manuell starten, ist die Bedeutung der Reihenfolge natürlich offensichtlich: Es ist die Reihenfolge der Argumente, die Sie an der Kommandozeile übergeben. Wenn Sie hingegen mit Visual Studio arbeiten, müssen Sie im Solution Explorer die Dateien Ihres Projekts in der Reihenfolge der Kompilierung auflisten. Indem Sie mit der rechten Maustaste auf eine Datei Ihres Projekts klicken und aus dem Kontextmenü die Einträge MOVE DOWN und MOVE UP aufrufen, ändern Sie die Reihenfolge.

Es gibt im Menü auch Einträge für das Hinzufügen neuer Dateien an einer bestimmten Stelle in der Liste, die statt des allgemeinen Menüpunkts im Kontextmenü des Projekts selbst verwendet werden sollten.

Abbildung 1.9: Kontextmenüeinträge zur Änderung der Kompilierreihenfolge

Ein Modul muss immer vollständig innerhalb einer Datei deklariert sein. Sie werden später in diesem Buch noch sehen, dass Sie Typen, die wiederum Bestandteil von Modulen sein können, in gewisser Weise „von außen“ erweitern können − für Module selbst besteht diese Möglichkeit jedoch nicht. Sie haben allerdings die Wahl, in einer Quelltextdatei mehrere Module zu definieren. In diesem Fall müssen Sie sich einer etwas anderen Syntax bedienen als in den vorherigen Beispielen: An die Stelle des einen globalen module-Blocks treten nun mehrere, wobei jede module– Zeile mit einem = endet und die zu dem Modul gehörenden Elemente eingerückt sein müssen. Hier ein kurzes Beispiel:

namespace global
module Adder =
let add x y = x + y
let add5 x = x + 5
module Multiplier =
let mult x y = x * y
let square x = x * x
let times3 x = x * 3

In diesem Beispiel kommt auch zum ersten Mal die Anweisung namespace vor, die Sie in der ersten Zeile finden. Es ist tatsächlich so, dass der Compiler die spezifische Angabe eines Namespace erwartet, wenn Sie eine Quelltextdatei mit mehreren voneinander unabhängigen Moduldefinitionen füllen, wie in dem Beispiel. Namespaces sind eine weitere Art von Blöcken, innerhalb derer sich Codeelemente strukturieren lassen. Der Name „Namespace“, grob übersetzt „Namensraum“, deutet auf die Tatsache hin, dass die Benennung von Codeelementen nur innerhalb eines bestimmten Namespace eindeutig sein muss.

Im Beispiel aus Listing 1.17 wird ein besonderer Namespace verwendet, der über den Bezeichner global angesprochen werden kann. Dabei handelt es sich um die Wurzel des hierarchischen Baums von Namensräumen, die keinen eigenen Namen hat. Für eine echte Anwendung ist es eine gute Empfehlung, Namespaces sorgfältig zu strukturieren und sie in die Planung von Typen, Modulen und deren jeweiliger Funktionalität mit einzubeziehen. .NET hat von niedrigster Ebene aufwärts Unterstützung für Namespaces, und wenn Sie Informationen und allgemeine Rat schläge zum Thema Namespaces und deren Strukturierung benötigen, bietet Microsoft auf den MSDN-Webseiten viel Hilfestellung dazu an. Sie werden in vielen Beispielen dieses Buches mit Namespaces konfrontiert werden. Ein letzter Hinweis noch zum kombinierten Thema von Modulen und Namespaces: Bei der Verwendung der globalen Moduldefinition, also bei einem Modul pro Quelltextdatei, haben Sie die Möglichkeit, das Modul direkt in einen bestimmten Namespace einzusortieren, indem sie den Namespace einfach dem Modulnamen voranstellen. Das kann zum Beispiel so aussehen:

module Sturm.Demo.Calculator
let square x = x * x
let add x y = x + y
let mult x y = x * y

Bemerkenswert ist allerdings, dass die Moduldefinition mit anschließendem = diese Möglichkeit nicht kennt. Der Compiler fordert also, wie bereits oben beschrieben, in diesem Fall unbedingt ein explizites namespace, und ist auch mit einem Modulnamen mit Namespace-Präfix nicht zufriedenzustellen. Mit folgendem Code werden die beiden zuvor gezeigten Module Adder und Multiplier Bestandteil des Namespace Sturm.Demo:

namespace Sturm.Demo
module Adder =
let add x y = x + y
let add5 x = x + 5
module Multiplier =
let mult x y = x * y
let square x = x * x
let times3 x = x * 3

[ header = Testen ]

Zuletzt fehlt noch ein wichtiges Detail zum Thema Namespaces. Es wurde bereits erwähnt, dass Namen von Typen und ähnlichen Elementen nur innerhalb eines einzelnen Namespace eindeutig sein müssen. Wenn Sie also davon ausgehen müssen, dass ein Modul oder ein Typ eines bestimmten Namens in unterschiedlichen Namespaces mehrfach existieren kann, dann folgt logischerweise, dass Sie dem Compiler jeweils genau sagen müssen, mit welchem dieser Module Sie gerade arbeiten möchten. Sie haben dazu die Wahl, entweder bei jeder Verwendung des Modulnamens den Namespace voranzustellen, oder mithilfe des Schlüsselwortes open einen bestimmten Namespace für die kontinuierliche Verwendung ohne besonderes Präfix verfügbar zu machen. Die folgenden beiden Codeblöcke sind also gleichermaßen gültig:

printfn "Square 11: %d" (Sturm.Demo.Calculator.square 11)
// --
open Sturm.Demo
[<EntryPoint>]
let main args =
// ...
printfn "Square 11: %d" (Calculator.square 11)
// ...

Der zweite dieser beiden Wege ist vermutlich der häufiger genutzte, da der Zugriff auf das Modul Calculator so syntaktisch einfacher ist. Wenn Sie die Struktur ihrer Namespaces sorgfältig geplant und umgesetzt haben, sollte dieser Ansatz nur selten zu Kollisionen führen. Wenn dies dennoch passiert, haben Sie noch immer die Wahl, einen Ihrer Typen mit explizitem Namespace zu qualifizieren. Anders als etwa in C# ist es in F# auch möglich, mit einem open direkten Zugang zu den Funktionen innerhalb eines Moduls zu bekommen. Mit anderen Worten, Sie können nicht nur Namespaces „öffnen“, sondern auch Module. Das Beispiel oben könnten Sie, als dritte Variante, auch wie folgt umschreiben:

open Sturm.Demo.Calculator
[<EntryPoint>]
let main args =
// ...
printfn "Square 11: %d" (square 11)
// ...
Eine dritte Variante zu Listing 1.20

Verwenden Sie diese Möglichkeit vorsichtig! Mit dieser Variante von open bringen Sie neue Funktionen direkt in den aktuell verwendeten Namensraum, sodass Kollisionen recht leicht geschehen können.

Testen

Ohne automatische Tests wird heute nur noch selten programmiert − richtig so! Viele agile Entwicklungsmethoden schreiben die Erzeugung von Tests als Startpunkt jeder Entwicklung vor. F# kann als .NET-Sprache grundsätzlich mit den unterschiedlichsten Testumgebungen arbeiten, die für die Plattform verfügbar sind. Allerdings ist der Grad der Unterstützung letztlich nicht überall derselbe. In diesem Bereich ist viel in Bewegung, also sei geraten, den aktuellen Stand noch einmal selbst zu untersuchen. Um aber eine Grundlage für Tests in den folgenden Kapiteln zu haben, wird hier das Framework xUnit vorgestellt, das mit F# recht gut zusammenarbeitet.

Um xUnit zu verwenden, besorgen Sie sich zunächst von http://xunit.codeplex.com die neueste Version. Diese kommt in Form einer zip-Datei daher, die Sie an einem beliebigen Ort auf Ihrer Festplatte auspacken können. Darin finden Sie die verschiedenen DLLs des Pakets sowie einige ausführbare Programme. Es gibt auch einen Installer, dessen Ausführung optional ist − damit lässt sich die Integration von xUnit mit TestDriven. Net aktivieren, und es können einige Vorlagen für die Nutzung von xUnit mit ASP.NET MVC installiert werden.

Das Schreiben von Tests mithilfe von xUnit in F# ist nicht schwieriger als mit irgendeiner anderen Sprache. Sie machen sich dabei zu Nutze, dass xUnit Tests auch in statischen Klassen erkennen kann (Module werden nämlich vom Compiler als statische Klassen implementiert), und dass es recht tolerant ist in Hinsicht auf die Signatur von Methoden, die als Tests erkannt werden. Ein Testmodul, hier der Einfachheit halber im selben Projekt untergebracht wie der Produktionscode, kann etwa wie folgt aussehen:

module Sturm.Tests.CalculatorTests
open Sturm.Demo
open Xunit
[<Fact>]
let adder_add_should_return_8_for_5_plus_3() =
Assert.Equal(8, Adder.add 5 3)
[<Fact>]
let adder_add5_should_return_8_for_3() =
Assert.Equal(8, Adder.add5 3)

Möglicherweise wundern Sie sich über die spezielle Syntax des Aufrufs an die Methode Equal − hier werden offensichtlich die Parameter mit Klammern umfasst, wohingegen Sie bei bisherigen Beispielen zu Funktionsaufrufen in F# gesehen haben, dass keine Klammern um die Parameterliste gesetzt werden müssen. Das hat damit zu tun, dass die Klasse Assert und ihre Methode Equal nicht selbst in F# geschrieben worden sind. Deshalb geht der Compiler davon aus, dass die Parameter nicht getrennt werden dürfen, wie es in F# selbst möglich wäre. Die in Klammern erscheinenden, durch Kommata getrennten Werte formen einen kombinierten Wert, ein Tupel, und das muss wiederum mit einem Klammerpaar umschlossen werden. Es ist gewissermaßen ein willkommener Zufall, dass die entstehende Syntax an einen Methodenaufruf in C# oder ähnlichen Sprachen erinnert.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -