© IR Stone/shutterstock.com, © S&S Media
Die Vorteile von F#

Entwicklung leicht gemacht mit F#


Wer mit .NET entwickelt, macht das wahrscheinlich mit C#. Doch gerade F# hat großes Potenzial, die tägliche Arbeit zu erleichtern. Denn alles, was C# kann, geht auch mit F#, und das oft leichtgewichtiger und weniger fehleranfällig.

Ich arbeite als Softwarearchitekt und -entwickler in einem kleinen Team von vier Entwicklern. Wir entwickeln und betreiben ein Produkt zur Zeiterfassung – DevOps sozusagen. Wir können diese Aufgabe nur stemmen, wenn unser Source Code simpel ist. Simpel bedeutet, dass der Code einfach zu lesen und zu verstehen ist. Zudem muss er einfach zu refaktorisieren und anzupassen sein. Nach fast 20 Jahren Entwicklung mit C# haben wir den Wechsel zu F# gewagt. In diesem Artikel berichte ich von unseren Erfahrungen, wo uns F# unterstützt, den Code noch simpler zu gestalten, als es mit C# möglich wäre.

Vorteile von C# gegenüber F#

  • Werkzeuge und IDE-Unterstützung sind ausgereifter

  • Compilergeschwindigkeit ist höher

  • Task-Runtime-Performanz höher bei starker IO-Verwendung

  • Debugging-Unterstützung ist besser

  • Low-level-Programmierung wird besser unterstützt

  • Programmierung von WinForms und WPF

  • Besser geeignet, wenn viele Objektableitungen verwendet werden

Alles ist eine Expression

In C# gibt es Expressions und Statements, in F# ausschließlich Expressions. Als Beispiel zeigt Listing 1 ein einfaches Statement in C#. Diese Methode hat drei Probleme. Erstens brauchen wir einen Initialwert, der an sich unnötig ist (var result = 0). Zweitens kann der Inhalt eines Zweigs des if nicht einfach extrahiert werden, da die Zuweisung bestehen bleiben muss. Das hindert das einfache Zusammensetzen von bestehenden Teilen zu etwas Größerem. Und drittens hilft mir der Compiler nicht, zu bemerken, wenn ich die Zuweisung result = 17; vergesse.

Listing 1: C# Statement

public int Compute(bool b) { var result = 0; if (b) { result = 42; } else { result = 17; } return result; }

Wenn wir uns den gleichen Code als Expression in C# anschauen, dann sehen wir, dass diese drei Probleme nicht existieren. Es gibt keinen Initialwert, die Zweige bestehen selbst wieder aus einer Expression und der Compiler würde motzen, wenn wir die 17 weglassen würden:

public int Compute(bool b) { return b ? 42 : 17; }

Der analoge F#-Code sieht wie folgt aus. F# erkennt die Typen meist selbst, weshalb sie nicht explizit hingeschrieben werden müssen. Für die Einrückung wird Whitespace statt Klammern verwendet:

let compute b = if b then 42 else 17

Da Expressions immer einen Rückgabewert haben, gibt es in F# den speziellen Wert unit für Funktionen, die keinen Wert brauchen oder zurückgeben. Die folgende Funktion nimmt „nichts“ als Argument und gibt „nichts“ zurück: let compute () = ().

Expressions erleichtern ebenfalls das Refaktorisieren des Codes. Der Compiler ist viel besser in der Lage, uns zu unterstützen.

Records – Einfachheit statt Spezialvarianten

Seit Version 9 kennt auch C# Records. Sie können dort auf unterschiedliche Weise definiert werden. Listing 2 zeigt zwei Varianten für Immutable Records.

Listing 2: C# Records

public record A(string Name, int Id); public record B { public string Name { get; init; } public int Id { get; init; } } public void Instantiate() { var a = new A("Hugo", 42); var b = new B { Name = "Hugo2", Id = 42 } }

Die Weise, wie ein Record in C# instanziiert werden muss, hängt davon ab, wie er definiert wurde. Das ist mühsam und ein Ausdruck davon, dass C# versucht, möglichst flexibel zu sein, dafür aber Einfachheit opfert.

F# Records werden so definiert und instanziiert, wie in Listing 3 zu sehen ist. Es gibt zwei Schreibweisen (mehrzeilig und einzeilig), die Definition ist aber identisch.

Listing 3: F# Records

type A = { Name : string Id : int } type A = { Name : string ; Id : int } // einzeilige Schreibweise let a = { Name = "Name" Id = 42 }

Equality for free

In C# werden Objekte standardmäßig per Referenz verglichen – außer Records, die eine Implementierung von Equals direkt mitbringen. Quizfrage: Was ist das Resultat der Methode in Listing 4?

Listing 4: C# Equality

public record Data(string Name, string[] Values); public bool Compute() { var a = new Data( "Charles", new[] {"1", "2"}); var b = new Data( "Charles", new[] {"1", "2"}); return a == b; }

Leider ist das Resultat false. Der Grund dafür ist, dass die beiden Arrays in C# standardmäßig mittels Referenz verglichen werden. Das ist meiner Meinung nach eine große Stolperfalle von C# Records.

Alle F#-Datentypen haben standardmäßig ein strukturelles Equality eingebaut. Somit gibt die Funktion in Listing 5 true zurück.

Listing 5: F# Equality

type Data = { Name : string ; Values : string[] } let compute () = let a = { Name = "Charles" Values = [| "1" ; "2" |] // [| |] initialisiert ein Array in F# } let b = { Name = "Charles" Values = [| "1" ; "2" |] } a = b // in F# ist = keine Zuweisung, // sondern ein Vergleich

Aufpassen muss man lediglich, wenn das F#-Typsystem verlassen wird, z. B. bei Interop mit C#. Durch das standardmäßige Vergleichen von Werten anhand der beinhalteten Unterwerte gibt es in F# keine unbeabsichtigten Vergleiche mittels Referenz. In C# lauern leider immer wieder Fallen, vor allem bei der Verwendung von LINQ (Contains, ContainsKey, GroupBy, Union, Distinct). Diese Methoden verwenden intern alle Equals und somit standardmäßig einen Vergleich der Referenz bei Referenztypen. Da wir nicht abschätzen können, ob eine von uns verwendete Library intern eine dieser Methoden verwendet, müssen wir entweder bei all unseren Klassen Equals korrekt überschreiben oder in Tests investieren, die die korrekte Verarbeitung überprüfen, so gut es geht. Beides ist sehr aufwendig und fehleranfällig.

Für uns bedeutet die Verwendung von F# weniger Fehler durch falsche Vergleiche ohne Zusatzaufwand.

Das eine oder das andere, nicht „sonst“

Beim Entwerfen eines Domänenmodells haben wir oft den Fall, dass wir verschiedene Varianten von etwas haben. Wenn wir zum Beispiel eine Temperatur modellieren, dann könnten wir das folgendermaßen tun; ein Interface mit zwei Implementierungen für die Varianten:

public interface ...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang