F# - Funktionales Programmieren unter .NET

Informatik Seminar WS09/10 von Sören Militzer


Seminarthemen WS09/10 :: Einleitung | Grundlagen Funktionaler Programmierung | Datenstrukturen | Imperative Aspekte | Objektorientierte Aspekte


Imperative Aspekte

Veränderliche Datenstrukturen

Wie im zweiten Kapitel angesprochen, sind alle Datenstrukturen sowie Symbole in F# per se unveränderlich, ein wichtiger Garant für die Freiheit der Funktionen von Seiteneffekten. Jedoch muss der Programmierer auch in der Lage sein, Variablen und veränderliche Datenstrukturen deklarieren zu können, besonders da aus F# die restlichen Komponenten der .NET Welt verwendet werden könnnen. Die Deklaration einer Variablen unterscheidet sich nur marginal von der Definition eines Symbols in der Hinsicht, dass die Deklaration einer Variablen mit dem Schlüsselwort "mutable" markiert werden muss. Der lesende Zugriff auf eine Variable geschieht gleichermaßen wie der auf ein Symbol, nur das Zuweisen eines Wertes auf einer Variable benötigt die Benutzung des Zuweisungs-Operators "<-".

let mutable myVar = 1

myVar <- 2

Records können sogar partiell veränderlich sein, da bei der Definition jede Einzelkomponente seperat als mutable markiert werden muss. Die Einzelkomponenten eines Records behalten auch außerhalb von F# ihre Veränderlichkeit bei, da Records als Klasse implementiert werden, auf deren Einzelkomponenten über schreibgeschützte bzw. auch beschreibbare Eigenschaften zugegriffen werden kann:

type Website = { name : string; mutable visits : int }

let newWebsite aName =
    { name = aName; visits = 0 }

let visitWebsite site =
    site.visits <- site.visits + 1
    site.name <- "kein Name" // diese Zeile Code wird vom Compiler als Fehler markiert!

In F# werden alle Parameter für Funktionen per "Call by Value" übergeben, bei Werte-Datentypen die Werte direkt als Kopie, ansonsten werden bei Objekt-Datentypen die Referenzen auf die Objekte in Kopie übergeben. Um Paramter für eine Funktion als "Call by Reference" übergeben zu können, muss ein Wrapper für diese Werte in Form eines Records definiert werden.

type 'a ref = { mutable contents : 'a }

let (!) r = r.contents
let (:=) r v = r.contents <- v
let ref v = { contents = v }

type 'a ref =
  {mutable contents: 'a;}


val ( ! ) : 'a ref -> 'a
val ( := ) : 'a ref -> 'a -> unit
val ref : 'a -> 'a ref

let useRef x =
    let useRefIntern y =
        y := 2 * !y

    let myRef = ref x
    useRefIntern myRef
    printfn "myRef: %i" !myRef

useRef 2

myRef: 4

Mittels diesen kleinen Tricks kann man also Parameter als Referenzen übergeben. Mit der "ref"-Funktion wird aus einem Wert eine Referenz erzeugt, mittels !-Operators wird der Wert dieser Referenz ausgelesen und per ":="-Operator der Wert dieser Referenz geändert.


Iteration

Natürlich dürfen in einer imperativen Programmiersprache die Kontrollstrukturen für eine Iteration nicht fehlen. Als Erstes sei die zählergesteuerte Iteration mittels For-Schleife an einem Beispielen demonstriert. Diese Schleife zählt von x bis y hoch und akkumuliert die Zählvariable.

let SumFromTo x y =
    let mutable result = 0
    for i in x..y do
        result <- result + i

    result

Das zweite Schleifen Konstrukt ist die kopfgesteuerte While-Schleife, die solange den Code ausführt, solange die Abbruchbedingung unwahr ist. Auch diese Funktion zählt von x bis y hoch und akkumuliert die Zählvariable.

let SumFromTo x y =
    let mutable i = x
    let mutable result = 0
    while (i <= y) do
        result <- result + i
        i <- i + 1

    result

Ein fussgesteuertes Schleifenkonstrukt, die es aus vielen imperativen Programmiersprachen bekannt ist existiert nicht in F#.


Arrays

Arrays sind eine weitere wichtige Datenstruktur in der F# Welt, die selten verwendet werden solange man sich ausschließlich in der F# aufhält, jedoch sprunghaft an Relevanz gewinnen sobald man in einem F# Programm auf Funktionalität aus dem Rest der .NET Welt zugreifen möchte. Arrays sind im Gegensatz zu Listen bei der Anzahl der enthaltenen Elemente unveränderlich, einmal in einer gewissen Größe initialisiert können keine weiteren Elemente hinzugefügt werden. Auch sind im Gegensatz zu Listen die im Array enthaltenen Elemente schreibbar und können mittels "<-"-Operator verändert werden. Da alle Elemente eines Arrays hintereinander als Block im Speicher liegen, ist die Komplexität des zufälligen Zugriffes auf einzelne Elemente in der Klasse O(1). Dies wird jedoch mit der Tatsache erkauft, das zur Erweiterung des Arrays alle Elemente in ein neues Array kopiert werden müssen.

Wie auch bei den Listen existiert ein Modul in F#, über dessen Funktionen auf alle alle wichtigen Operationen für Arrays zugegriffen werden kann. Bis auf die Typensignaturen sind die Funktionen aus dem Array Modul größtenteils vergleichbar mit dem aus dem List Modul, daher verzichte ich hier auf weitere Details.