Interpretation der F# Ausdrücke

 

In F# ist alles mit Ausdrücken (expressions) aufgebaut, wo andere Sprachen Statements (statements) verwenden. Diese Ausdrücke werden in folgender Reihenfolge ausgewertet:

von links nach rechts

von oben nach unten

Also nichts ungewöhnliches, genau wie die Deutsche Sprache geschrieben wird. D.h. alles was verwendet wird ist oberhalb oder in anderen Modulen (oder Assemblies) definiert.

Die einzige Ausnahme ist bei type member, also Klassen Member, deren Reihenfolge willkürlich sein kann. Wieder mit der Ausnahme, dass der namenlose Konstruktor (also der, der nicht New heisst) als erstes kommt.

In einem Projekt sind die Sourcefiles auch strikt hierarchisch geordnet. Also das unterste kann die oberen mit open (analog C# using) einbinden, nicht umgekehrt.

Was macht man, wenn es scheinbar nicht möglich ist so strikt zu strukturieren?

Wie geht das bei zirkulären Abhängigkeiten (circular dependency)?

Für Typen- type und Funktions- let Definitionen innerhalb eines Moduls gibt es das and Konstrukt, das jeweils die abhängigen type oder let Schlüsselworte ersetzt. Dies erlaubt es wechselseitig Anhängige Definitionen zu bilden. So sind zirkuläre Definitionen möglich (ohne Vorausdeklarationen und Mutation).

Wieso ist das so strikt und stur?

Einerseits ist es so, dass es für den Compiler und vor allem für die Type Inferenz so sicher einfacher ist. Das könnte man als Nachteil sehen, dass man gezwungen wird den Aufbau so zu machen. Die Erkenntnis ist aber, dass ein so aufgebautes Software System sehr klar ist. Hierarchisch strikt durchschaubar. Keine Spagetti-hafte Kreuz Abhängigkeiten zwischen den Modulen. Was hier detailliert analysiert wird.

Zirkuläre Abhängigkeit von Komponenten verhindert die Schichten-Bildung (Layering) und ist somit schlechter testbar, wartbar, erweiterbar und wiederverwendbar.

Abhilfe für zirkuläre Abhängigkeiten

Die Tricks sind:

  • Die Datentypen von den Funktionen trennen. Was genau die Umkehrung vom Objektorientierten Design ist, also die Memberfunktionen aus den Klassen reissen und die herausfallenden mutierenden Zustände vermeiden. Diese Funktionen generisch machen und so deren Wiederverwendbarkeit ermöglichen. Was bei Funktionaler Programmierung typischerweise der Fall ist, weil die Daten immutable sind, funktioniert das einfach so.

Daten sind Programmiersprachen unabhängig, Objekt Klassen nicht. Man denke an die Speicherung.

  • Abstrahieren der Datentypen, möglichst alle Funktionen für generische Typen auslegen. Was in F# automatisch passiert, wenn man keinen Type spezifiziert.

Werte sind die Besten Interfaces.

Nicht aufgeben

Also wenn man etwas versucht das mit der strikten Hierarchischen Struktur nicht klar kommt, dann sollte man sich genau überlegen was es ist, das nicht in diese Form passt. Meist zeigt sich, dass es auch anders geht. Und so kommt es automatisch zu guten einfachen Designs.

Der Vorteil ist, dass man den Code so wie ein Buch lesen kann und die Reihenfolge ist logisch aufbauend auf schon bekanntem. Man muss nicht im Code oder den Komponenten herum springen um ihn zu lesen und zu verstehen.

Man kann es vom Kleinen zum Grossen lesen, oder umgekehrt beim Grossen beginnen um sich einen Überblick zu verschaffen und dann in die Details gehen. Man weiss wo man starten kann. Dies ermöglicht einem neuen Teammitglied den schnellen Einstieg in eine vorhandene Code-Basis. Man muss nicht erst Design Dokumentation lesen, falls solche vorhanden ist und sich mit dieser anfreunden oder erlernen.

Beispiel zu wechselseitig Anhängigen Typen

Die in diesem Beispiel voneinander abhängigen Typen sind Person und Ortschaft, da eine Person ein Wohnsitz hat und eine Ortschaft Einwohner hat.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
// Datentypen (Records)
type Person =
    { name      : string
      wohnort   : Ortschaft  
    }
and Ortschaft =
    { name      : string
      einwohner : Person list
    }

// Funktionen
let getPerson name ort =
    { Person.name = name; 
      wohnort = { Ortschaft.name = ort; einwohner = [] } 
    }

let lebtInOrtschaft ortschaftName person = 
    person.wohnort.name = ortschaftName

let einwohnerVon ortschaftName personen =
    personen |> List.filter (lebtInOrtschaft ortschaftName) 

let getEinwohnerZahl (ort : Ortschaft) =
    ort.einwohner.Length

let getOrtschaft ort personen =
    { name      = ort
      einwohner = personen |> einwohnerVon ort 
    }

// Aufbau der Datenstruktur
let Fritz  = getPerson "Fritz" "Zürich"
let Hans   = getPerson "Hans"  "Genf"  
let Max    = getPerson "Max"   "Genf"  

let Personen = [ Fritz; Hans; Max ] 

let Genf   = getOrtschaft "Genf"    Personen
let Zurich = getOrtschaft "Zürich"  Personen

let Ortschaften = [Genf; Zurich]

// Anwendung
List.map getEinwohnerZahl Ortschaften   // = [2; 1]

Aufgabe : Max zieht um von Genf nach Zürich. Wie bekommt man das hin, weil ja alles unveränderlich (immutable) ist?

Lösung : Alle Veränderungen werden in diesem Beispiel hier mit angehängtem ' an den Wertenamen markiert. Das ist reine Dekoration, man könnte auch immer eine 2 anhängen. Wichtig ist, es entsteht ein neuer Wert, hier mit neuem Namen.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let Max'         = { Max with wohnort = Zurich }
let Personen'    = [ Fritz; Hans; Max' ] 
let Genf'        =  getOrtschaft "Genf"   Personen'
let Zurich'      =  getOrtschaft "Zürich" Personen'
let Ortschaften' = [ Genf'; Zurich' ]

List.map getEinwohnerZahl Ortschaften' // = [1; 2]

Jetzt könnte man meinen dies sei Speicherverschwendung, immer alles neu aufzubauen?

Der Code spezifiziert Was gemacht wird, nicht das Wie.
Es wird intern nicht alles neu Aufgebaut, nur die Änderungen. Die Liste ist eine spezielle Funktionale Datenstruktur, die weil ja alles immutable ist, intern auf die bereits existenten unveränderlichen Daten verweist. Somit ist die Liste natürlich auch Thread-Safe.

Siehe auch Vorteile Funktionale Datenstruktur und Chris Okasaki’s 1998 Buch “Purely functional data structures” und Chris Okasaki’s Blog

Grob gesagt nur die neue Mutation, also der Max' benötigt zusätzlichen Speicher. Ein Vorteil ist so auch, dass man die ganze History immer vorhanden ist. Gewisse Anwendungsbereiche leben davon, wie z.B. Source Control, Bankkonten. Da wird die ganze Geschichte aufbewahrt und nicht einfach die Speicherstelle Kontostand verändert.

Dieses Beispiel konkurriert natürlich nicht mit speziell dafür (was immer genau die Anforderungen sind?) ausgelegten Datenstrukturen. Und F# kann auch Daten ändern, siehe ref und mutable, dazu später mehr.

Erinnern wir uns, das Beispiel zeigt wie man wechselseitig abhängige Typen definiert, erzeugt und handhabt.

type Person =
{name: string;
wohnort: Ortschaft;}Full name: InterpretationderFAusdrücke.Person

Person.name: string
Multiple items
val string : value:’T -> stringFull name: Microsoft.FSharp.Core.Operators.string

——————–
type string = System.String

Full name: Microsoft.FSharp.Core.string

Person.wohnort: Ortschaft
type Ortschaft =
{name: string;
einwohner: Person list;}Full name: InterpretationderFAusdrücke.Ortschaft

Ortschaft.name: string
Ortschaft.einwohner: Person list
type ‘T list = List<‘T>Full name: Microsoft.FSharp.Collections.list<_>

val getPerson : name:string -> ort:string -> PersonFull name: InterpretationderFAusdrücke.getPerson

val name : string
val ort : string
val lebtInOrtschaft : ortschaftName:string -> person:Person -> boolFull name: InterpretationderFAusdrücke.lebtInOrtschaft

val ortschaftName : string
val person : Person
val einwohnerVon : ortschaftName:string -> personen:Person list -> Person listFull name: InterpretationderFAusdrücke.einwohnerVon

val personen : Person list
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 list

Full name: Microsoft.FSharp.Collections.List<_>

val filter : predicate:(‘T -> bool) -> list:’T list -> ‘T listFull name: Microsoft.FSharp.Collections.List.filter

val getEinwohnerZahl : ort:Ortschaft -> intFull name: InterpretationderFAusdrücke.getEinwohnerZahl

val ort : Ortschaft
property List.Length: int
val getOrtschaft : ort:string -> personen:Person list -> OrtschaftFull name: InterpretationderFAusdrücke.getOrtschaft

val Fritz : PersonFull name: InterpretationderFAusdrücke.Fritz

val Hans : PersonFull name: InterpretationderFAusdrücke.Hans

val Max : PersonFull name: InterpretationderFAusdrücke.Max

val Personen : Person listFull name: InterpretationderFAusdrücke.Personen

val Genf : OrtschaftFull name: InterpretationderFAusdrücke.Genf

val Zurich : OrtschaftFull name: InterpretationderFAusdrücke.Zurich

val Ortschaften : Ortschaft listFull name: InterpretationderFAusdrücke.Ortschaften

val map : mapping:(‘T -> ‘U) -> list:’T list -> ‘U listFull name: Microsoft.FSharp.Collections.List.map

val Max’ : PersonFull name: InterpretationderFAusdrücke.Max’

val Personen’ : Person listFull name: InterpretationderFAusdrücke.Personen’

val Genf’ : OrtschaftFull name: InterpretationderFAusdrücke.Genf’

val Zurich’ : OrtschaftFull name: InterpretationderFAusdrücke.Zurich’

val Ortschaften’ : Ortschaft listFull name: InterpretationderFAusdrücke.Ortschaften’