Die Vorteile funktionaler Programmierung (im speziellen F#)

TL;DR: Weniger Code mit FP und F#.

Was ist es denn genau, was in der Funktionalen Programmierung mit F# den Code, also die Problemlösung, so vereinfacht und verkürzt?

Zu meinem Blog-Post "Less Code and Less Bugs : with Functional Programming languages"
http://functionalsoftware.net/less-code-and-less-bugs-with-functional-programming-languages-589/
bei dem es darum geht, das "weniger Code auch weniger Fehler" hat,
möchte ich hier aufzeigen mit welchen speziellen Sprachkonstrukten F#
  • das Code Verständnis erhöht und

  • dabei prägnanten Code ermöglicht

    (prägnant = knapp, treffend, gehaltvoll.)

    (concise = giving a lot of information clearly and in a few words; brief but comprehensive.)


Es folgt eine Auflistung von ausgewählten F# Features, die in etwa in dieser Reihenfolge aufeinander aufbauen für eine Problemlösungs-Formulierung, und massgeblich für das Code Verständnis und Code Korrektheit dienlich sind.

Was das Code Verständnis und Code Korrektheit am meisten fördert, sind:
  • Funktionen

    Input -> Output : den Output an einem klar definierten Ort im Code und die exakte Type Information des Output.

  • Die Pipe |>

    und Arrow >> Syntax ermöglicht den Daten/Program-Fluss in eine Richtung (ähnlich einem visuellen Flow-Diagram), die der natürlichen Leserichtung von links nach rechts (in unserer Kultur) folgt. Dies erleichtert auch das Naming, weil nicht für jedes Zwischenergebnis das nur einmal weiter gegeben wird ein Name/Bezeichner gefunden werden muss. Je nach verwendeter Namenskonventionen kann dies sehr mühsam zu lesen sein (Wortlängeneffekt, Erinnerungsleistung, Speed-Reading). Statt benannte Zwischenergebnisse Schritt für Schritt anzuwenden x = f() ; y = g(x) ; z = y das definieren was es ist z = f() |> g

  • Higher Order Functions (HOF) https://en.wikipedia.org/wiki/Higher-order_function

    sind Funktionen die Funktionen transformieren und mit minimalem syntaktischen Aufwand codiert werden. Man braucht nur den Funktionsnamen zu übergeben und die Interface-Typen werden von der Type-Inference automatisch ermittelt. Man braucht so nichts zu deklarieren (keine Func, Action, delegate, Interface), einfach nur zusammenbauen.

  • Expressions

    statt Statements (Referential Transparency, Pure Functions) - kurz : das WAS statt das WIE

  • Immutability

    als default und Mutation als Ausnahme.

  • Type Inference https://de.wikipedia.org/wiki/Typinferenz

    nicht weil man weniger Typen Signaturen schreiben muss, sondern weil es "fast" nebensächlich ist und es einfach passen muss, was die Type Inference im Editor während des Code schreibens prüft und signalisiert.

  • Algebraic Data Types (ADT) https://en.wikipedia.org/wiki/Algebraic_data_type

    Prägnanter exakter Daten Design mit Summen- und Produkt-Typen, die auch rekursiv zusammen gestellt (Composition) werden können.

  • Pattern Matching https://msdn.microsoft.com/en-us/library/dd547125.aspx

    ein vom Compiler überwachter Verzweigungs-Control-Flow damit kein (Spezial)-Fall vergessen wird. Man stelle sich das als super intelligentes switch-case Konstrukt vor, das mit den Algebraic Data Types perfekt harmoniert.

  • Computation Expression (CE) https://msdn.microsoft.com/en-us/library/dd233182.aspx

    ermöglicht Code-Plumbing hinter den Kulissen zu erledigen. So ist nur die Essenz in der Computation Expression ersichtlich. Die Logik dahinter, der Computation Builder, kann wiederverwendet werden. Mittels dieser Computation Expression lassen sich elegant z.B. Monaden usw. implementieren.

  • Units of Measure https://msdn.microsoft.com/en-us/library/dd233243.aspx

    vom Compiler geprüfte und berechnete Masseinheiten.

  • Domain Specific Language (DSL), Domänenspezifische Sprache https://de.wikipedia.org/wiki/Dom%C3%A4nenspezifische_Sprache

    F# eignet sich hervorragend um embedded-DSL's zu implementieren, denn man braucht dazu keine Lexer/Parser zu schreiben und kann die DSL in der Entwicklungsumgebung anwenden. Dies ist einfach möglich, da F# nebst kompliliertem Code auch als Scripting verwendet werden kann, das dann wiederum Kompiliert werden kann. Somit hat man für die eigene Domain Specific Language die Spracheigenschaften von F#, wie strict-static-typing und die Entwicklungsumgebung mit samt dem Debugger (wenn nötig). Ein bekanntes und viel angewandtes Beispiel einer embedded-DSL ist FAKE, ein F# Open Source Build Automatisierungs Scripting Tool ("make").


Hier einige Referenzen mit Beispielen die zeigen wie prägnant F# Code sein kann, im Vergleich mit C# Code.

"An F# rewrite of a fully refactored C# Clean Code example"
http://functionalsoftware.net/fsharp-rewrite-of-a-fully-refactored-csharp-clean-code-example-612/

"Analyzing Government Data"
in C# #csharp http://blogs.msdn.com/b/dave_crooks_dev_blog/archive/2015/04/20/intro-to-c-and-analyzing-government-data.aspx
und F# #fsharp http://fssnip.net/qE

"Does the Language You Use Make a Difference (revisited)?"
http://simontylercousins.net/does-the-language-you-use-make-a-difference-revisited/

"F# means less code"
http://fpbridge.co.uk/why-fsharp.html#conciseness

Weniger Softwarefehler durch Klarheit – null ist Vergangenheit in F#

Die null ist leider die Quelle vieler Laufzeitfehler.

Die null-en befüllen die Code-Basis und machen sie weniger gut lesbar und wartbar.

Ein typisches Codefragment in C#:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
// C#
if (x != null) 
{
    return UseALanguageWithoutNull( x );
}
else
{
    return null;
}

Wie es ohne null aussieht

1: 
2: 
// F#
UseALanguageWithoutNull x

Null vs. Option.None?

Wir wissen dass in F# alle Werte initialisiert sind und niemals mit null. Mit der Ausnahme, dass von einer .Net Komponente die nicht in F# geschrieben ist, ein Rückgabewert oder ein Out Parameter als null daherkommt.

Was ist nun, wenn man den Zustand von nicht vorhanden in F# abbilden will?

Dazu dient der Option Type. Dieser kann None oder Some 'T sein. Wobei ‘T ein beliebiger Type sein kann, also z.B. ein int, ein String oder ein Array.

Jetzt könnte man ja sagen es ist genau das selbe?

1: 
2: 
null    vs.     None
obj     vs.     Some 'T

Die Unterschiede sind:

  • Es ist nicht umständlicher einen Wert in einen Option type zu packen. Auch wenn es im ersten Augenblick so aussehen mag. Der Nutzen überwiegt!
  • Der Type ‘T von Some ist bekannt, und auch von None, also welcher Type der keinen Wert hat. Dies ermöglicht die exakte statische Type Überprüfung durch den Compiler, was zu sicherem Code führt. Es wird also nicht zur Laufzeit implizit ge-castet.
  • Elimination der Mehrfachbedeutung von null. Wie oben schon erwähnt: Bedeutet null nun, nicht initialisiert oder nicht zugewiesen oder nicht vorhanden oder kein Ergebnis oder Fehler ? Da jede Variable auch null sein kann, muss der Programmierer immer mitdenken, was für Zustände die Variable haben könnte und was diese im Kontext nun bedeutet. Vergisst er es oder missinterpretiert er es, hilft ihm der Compiler nicht, erst zur Laufzeit unter bestimmten Bedingungen (Zustände, Daten) kann es dann zu null-Exceptions kommen. Im Gegensatz dazu, mit dem Option Type ist es absolut klar und der Compiler weiss es auch und hilft!
union case Option.None: Option<'T>
type obj = System.Object

Full name: Microsoft.FSharp.Core.obj
union case Option.Some: Value: 'T -> Option<'T>