Die Strukturierung von F# Quellcode

Eine neue Seite zum Thema der Strukturierung von F# Code ist dazugekommen. Es geht um: – warum F# Code so kompakt ist – wie einem die Type-Inferenz behilflich ist sich auf das wesentliche zu Konzentrieren - und wie man dadurch weniger Fehler macht weiterlesen…..

Die Strukturierung von F# Quellcode

F# Code ist sehr kompakt, weil unnötiges und redundantes entfällt. Kurz gesagt, keine ; { } und keine Typen Deklaration. So, nun im Detail alles der Reihe nach:

Keine ;

Das Ende einer Code-Zeile mit ; abzuschliessen ist in F# nicht nötig. Man kann, sieht dann aber aus wie umgewandelter C# oder C++ Code. ; werden verwendet um Aufzählungen von Records, Listen, Arrays etc. zu trennen. Wobei man auch hier, bei nur einem Element pro Zeile den ; nicht zu schreiben braucht.
1: 
2: 
3: 
4: 
5: 
6: 
7: 
let xs = [ 1; 2; 3 ] 
let xs = 
       [ 
        1
        2
        3
       ]

Keine {}

F# Code kommt ohne Code Blöcke aus die in geschweifte Klammern { } gefasst sind. Die Struktur ergibt sich aus der sauberen Einrückungen mittels Tab oder Space (white-space aware code). Die meisten Codierrichtlinen geben ja heutzutage genau eine solche Code Formatierung vor, zusätzlich zu den {} Klammern was redundant ist. Also wieso nicht die Klammer weglassen. Mag sein, dass das für manchen am Anfang etwas komisch aussieht, aber man gewöhnt sich so schnell daran. Kommen in F# doch { } vor, dann handelt sich um einen Record Type oder Computation Expressions

Wie geht denn das ohne { } Code-Blocks ?

Bei gut gestaltetem Code achtet man darauf, dass die Code-Blocks korrekt eingerückt sind. Also z.B.
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
// C#
void Bar()
{
    bool b = Foo();
    if b 
    {
        FooTrue1();
        FooTrue2();
    }
    else
    {
        FooFalse();
    }
}
Das selbe in F# ist durch weglassen von ; und { } zu erreichen, bei Einhaltung der korrekten Einrückungen.
1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
// F#   
let bar() =
    let b = foo()
    if b then
        FooTrue1()  // Die Einrückung gilt für mehrere Code Zeilen. 
        FooTrue2()       
    else
        FooFalse()
 

Keine Typen Deklaration

Es ist sicher schon aufgefallen, dass bei Funktionen keine Typen angegeben wurden.

Fehlt da dann nicht etwas, wenn im Code die Typen fehlen?

Wie geht das ohne Typen zu deklarieren? Die sogenannte Type-Inferenz findet selbst heraus um welchen Type es sich handeln muss. Man stelle sich vor, man ruft eine API-Funktion auf die data als Parameter entgegennimmt. Diese Funktion verarbeitet int array, somit ist data vom Type int array, also muss man data nicht extra deklarieren. In Visual-Studio oder auch im Blog Code hier, zeigt ein Tooltip über dem jeweiligen Wert oder Funktion dessen Type an, wenn man überprüfen will was es denn nun ist.
Der Code wird dadurch schlanker zu lesen und auch zu schreiben. Wobei Code ja mehr gelesen (Code Review) als geschrieben wird, ist das sehr nützlich, denn Kompakter Code ist schneller zu verstehen!
Kompakter Code, nicht im Sinne von alles auf eine Zeile quetschen, ist schneller zu verstehen, als wenn man Seitenweise lesen muss und sich immer die Typen Deklaration mitdenken muss. Überlassen wir die Typen dem “Compiler”, der prüft diese, und zwar schon vor dem Compilieren, die TypeInference die das erledigt läuft im Editor mit. Passt etwas nicht, wird die Stelle mit der bekannten rote Wellenlinie unterstrichen. Im Tooltip dazu wird erklärt was da nicht passt und was das Problem lösen kann.
Wir kümmern uns darum WAS gemacht wird. Ob alles passt, darum kümmert sich die TypeInference. Es ermöglicht ein abstrakteres Denken, wenn man sich nicht um die Typen Details kümmern muss und spart Entwicklungszeit.
Dies führt zum oft erwähnten positiven Effekt, den man dabei erlebt, dass wenn alles passt, der Code auch gleich erfolgreich kompiliert und meist auch prompt funktioniert. Diese Erfolge motivieren und man will TypeInference nicht mehr missen.

Generische Typen

Selber denkt man eher generisch, also mit generischen Datentypen. Es ist in F# so:
Wenn die Type-Inferenz nicht herausfindet, was es für ein Type sein muss, dann ist der Code automatisch generisch!
Super, man muss also dafür nicht extra überall <T> hinschreiben, das ist dann automatisch so!
1: 
2: 
3: 
4: 
5: 
// C#
void Swap<T>(List<T> list1, List<T> list2)
{
    // code to swap items
}
Und das equivalente generische swap in F#:
1: 
2: 
// F#
let swap (x,y) = (y,x)  // val swap : x:'a * y:'b -> 'b * 'a
und das funktioniert dann auch z.B. mit int List
1: 
2: 
3: 
4: 
let xs = [1; 3]
let ys = [4; 5; 6]
swap (xs,ys)
//val it : int list * int list = ([4; 5], [1; 3])
Auch das Refactoring wird dadurch vereinfacht, man muss die Typen nicht mitschleppen, also lesen und schreiben, umkopieren.
Weil es so einfach ist, braucht man dazu auch keine Refactoringtools! Viele glauben es zu beginn nicht, aber es ist so! Ihr werdet es merken ;-)
Etwas krass ausgedrückt werden Refactoringtool vielfach dazu verwendet, die “nebensächlichen” Teile des Codes zu erzeugen, die man nicht schreiben will. Steht der Code und man muss umbauen, dann müssen all die “nebensächlichen” Teile syntaktisch korrekt verpflanzt werden (Code-Plumbing wie man es auch nennt). Man kann es so sehen:
Die F# Syntax ist so ausgelegt, dass vieles nebensächliche gar nicht vorkommt und so das WAS gemacht wird sofort und klar ersichtlich ist.
Wie z.B.
  • keine “;” am Ende der Codezeile
  • keine { } Code-Blocks
  • keine Typen Deklaration
  • keine Generische Typen Deklaration
Positives Fazit:
Wer weniger schreibt, macht weniger Fehler. Wer weniger lesen muss, findet Fehler eher. Wer weniger lesen muss, arbeitet sich schneller ein. Weniger Code, weniger Bugs. Weniger Code, weniger Code Review. Weniger Code, Entwickler schneller eingearbeitet.
Ja und es geht nicht hauptsächlich darum : “Wer weniger schreibt, ist schneller fertig.” … es ist nett wenn das auch noch so ist ;-)
val xs : int listFull name: StrukturierungvonFQuellcode.xs
type bool = System.BooleanFull name: Microsoft.FSharp.Core.bool
Multiple items module Listfrom Microsoft.FSharp.Collections——————– type List<‘T> = | ( [] ) | ( :: ) of Head: ‘T * Tail: ‘T list interface IEnumerable interface IEnumerable<‘T> member Head : ‘T member IsEmpty : bool member Item : index:int -> ‘T with get member Length : int member Tail : ‘T list static member Cons : head:’T * tail:’T list -> ‘T list static member Empty : ‘T listFull name: Microsoft.FSharp.Collections.List<_>