Einführung in F
Transcription
Einführung in F
Einführung in F# Inhaltsverzeichnis Einführung in F#...................................................................................................................................1 Einleitung.........................................................................................................................................2 Funktionale Programmierung..........................................................................................................2 Wozu F# lernen?..............................................................................................................................3 Erste Schritte....................................................................................................................................4 Das Typsystem.................................................................................................................................7 Funktionen.......................................................................................................................................9 Erste Funktionen.........................................................................................................................9 Generische Funktionen.............................................................................................................12 Higher-Order Functions............................................................................................................13 Anonyme Funktionen................................................................................................................13 Pipelines....................................................................................................................................14 Verzweigungen...............................................................................................................................15 If................................................................................................................................................15 Pattern-Matching.......................................................................................................................16 Rekursive Funktionen....................................................................................................................17 Einfache Rekursionen...............................................................................................................17 Endrekursion.............................................................................................................................18 Wechselseitige Rekursion.........................................................................................................19 Erste grundlegende Datentypen.....................................................................................................21 Tupel..........................................................................................................................................21 Typenaliase................................................................................................................................21 Records......................................................................................................................................21 .NET-Typen...............................................................................................................................22 Listen.............................................................................................................................................22 Listen erstellen..........................................................................................................................22 Manuelle Listenverarbeitung....................................................................................................23 List-Comprehensions................................................................................................................25 Listenfunktionen.......................................................................................................................25 Ein paar Beispiele.....................................................................................................................26 Arrays.............................................................................................................................................27 Sequenzen......................................................................................................................................28 Option types...................................................................................................................................30 Mengen und Wörterbücher............................................................................................................31 Imperative Elemente......................................................................................................................32 Variablen...................................................................................................................................32 Verzweigungen..........................................................................................................................32 Schleifen....................................................................................................................................32 Referenzzellen...........................................................................................................................33 Veränderliche .NET-Aufzählungsklassen..................................................................................34 Vergleich zu imperativen Programmiersprachen......................................................................34 Discriminated unions.....................................................................................................................35 Parameterisierte und generische Varianten...............................................................................36 Rekursive Typen........................................................................................................................37 Berechnungen mit Discriminated unions..................................................................................39 Objektorientierung.........................................................................................................................42 Bis jetzt und ein bisschen weiter...............................................................................................43 Erste Klasse...............................................................................................................................44 Große Klasse.............................................................................................................................45 Klassen in Langform.................................................................................................................45 Vererbung..................................................................................................................................45 Schnittstellen.............................................................................................................................45 Ausnahmebehandlung....................................................................................................................45 Code-Strukturierung......................................................................................................................45 Module......................................................................................................................................45 Namespaces...............................................................................................................................45 Language-Oriented Programming.................................................................................................45 Operatordefinition..........................................................................................................................46 Maßeinheiten.................................................................................................................................47 Aktive Muster................................................................................................................................47 Computation Expressions..............................................................................................................50 Einleitendes Beispiel.................................................................................................................50 Computation Expressions definieren........................................................................................51 Weitere Computation Expressions............................................................................................52 Zufallsgeneratoren....................................................................................................................53 Asynchrones Programmieren....................................................................................................57 Parserkombinatoren – Eigene Programmiersprachen schreiben...............................................63 Quellen und Links.....................................................................................................................63 Einleitung In letzter Zeit sind zahlreiche neue .NET-Sprachen entstanden. Waren zu Anfang nur C# und VB vertreten, so gibt es nun fast für jede Sprache schon eine .NET-Implementierung. Bei den meisten Sprachen läuft es letztendlich immer auf das Selbe hinaus, nur die Syntax unterscheidet sich. In diesem Tutorial möchte ich eine kleine Einführung in eine neue und ganz besondere .NET-Sprache geben, die völlig anders „tickt“ als ihre Verwandten: F# An wen richtet sich dieses Tutorial? Dieses Tutorial richtet sich an jeden, der sich einen Überblick über F# verschaffen möchte oder gerne einmal in die funktionale Programmierung hineinschnuppert. Damit dieser Artikel nicht jeglichen Rahmen sprengt, kann ich nicht alles ganz von Grund auf erklären; Ein paar Programmierkenntnisse in anderen Sprachen, Zeit und Experimentierfreude schaden also nicht – neue Konstruktionen werden aber immer aus sich heraus verständlich sein. Einige Themen möchte ich auch nur anreißen – Für tiefgreifendere Informationen genügt eine Anfrage bei Google oder im exzellenten englischen Wikibook zu F#. Vor Allem geht es aber darum, sich in F# einzulesen, weshalb ich sehr viel mit kleinen Codeschnipseln und Aufgaben arbeiten werde. Funktionale Programmierung Was macht F# nun so besonders? Die Antwort lautet: Es ist besonders, weil die „Denkweise“, das Prinzip hinter F# eine völlig andere ist, als bei den meisten anderen Sprachen. Visual Basic oder C# zählt man zu den sogenannten imperativen Programmiersprachen. Imperativ heißt: Ich gebe dem Computer Befehle und sage ihm Schritt für Schritt, wie er ein Problem zu lösen hat. F# ist hier anders – Es ist eine funktionale Programmiersprache. Historisch gesehen geht die funktionale Programmierung nicht auf Maschinensprachen wie dem heutigen Assembler zurück, sondern basiert auf dem mathematischen Konzept des sog. Lambda-Kalküls, einer formalen Sprache zum Untersuchen und Bearbeiten von Funktionen. Man hatte daraufhin festgestellt, dass dieses System den herkömmlichen Berechnungssystemen (Programmiersprachen) in Mächtigkeit um Nichts nach stand und bald entstand mit LISP die erste funktionale Sprache. Mit ML (Meta-Language) keimte im Weiteren eine ganze Familie von solchen Sprachen auf (Standard ML, OCaml), deren neuster Vertreter nun F# ist. Der Einfluss dieses Programmierparadigmas ist seit dem immer mehr gewachsen und hat bei nahezu allen modernen Sprachen seine Spuren hinterlassen. Funktionale Programmierung orientiert sich am Begriff der mathematischen Funktion – Ein Programm ist letztendlich eine einzige, komplexe Funktion. Man konzentriert sich hauptsächlich darauf, was ein Programm tun soll und überlässt das Wie dem Compiler. Der Vorteil, der sich hieraus ergibt, ist, dass oft viel knapper und näher an einem Problem formuliert werden kann. Funktionale Programme sind dadurch oft viel kürzer, verständlicher und fehlerunanfälliger als vergleichbare imperative Programme. Was genau aber funktionale Programmierung bedeutet werden wir im weiteren Verlauf klären. Wozu F# lernen? F# hat Zukunft – Als neues Mitglied in der .NET-Familie eignet es sich sehr gut, in bereits vorhandene Projekte integriert zu werden und ist damit, im Gegensatz zu mach anderen funktionalen Sprachen, in Projekten der „wirklichen Welt“ tatsächlich einsetzbar. Als vollwertige .NET-Sprache lässt sich mit F# zwar alles anstellen wie mit VB oder C# auch, allerdings hat es wie alle anderen Sprachen seine Stärken und Schwächen. Die folgende Liste soll einen kleinen Ausblick geben, wo und weshalb F# so eine mächtige Ergänzung in unserer Programmierlandschaft ist: 1. F# produziert hochgradig fehlerunanfälligen, präzisen und wartbaren Code. 2. F# hat ein außerordentlich weit entwickeltes, striktes Typsystem, das dem Programmierer viel Arbeit abnimmt anstatt abverlangt. Eine große Zahl von Fehlern kann bereits beim Kompilieren abgefangen werden. 3. F#-Code ist sehr leicht parallelisierbar – Programme skalierbar und effizient auf Mehrkernprozessoren auszuführen ist kein Problem 4. F# selbst ist syntaktisch sehr anpassungsfähig an spezielle Bedürfnisse eines Problemfeldes 5. F# verändert und bereichert durch seinen unkonventionellen Ansatz die Denkweise, mit der wie an Programmierung herangehen 6. und F# kann uneingeschränkt mit allen .NET-Sprachen und Bibliotheken interagieren und nahtlos in deren Programme integriert werden. Das prädestiniert es für folgende Aufgabenfelder: 1. Wissenschaftliches Arbeiten (Mathematik, Physik, Finanzen) 2. Problemlösung 3. Parallele Programmierung (Mehrkernprozessoren) / Netzwerke 4. Sicherheitskritische Aufgaben 5. Text/Datenverarbeitung 6. Entwicklung eigener Programmiersprachen (Domain-Specific Languages) Wer hingegen benutzeroberflächen-intensive Programme zu schreiben gedenkt, wird schnell feststellen, dass dies, obgleich natürlich möglich, einer funktionalen Sprache nicht sehr liegt. Statt dessen kann eine F#-Bibliothek im Hintergrund sinnvoller sein. Aber letztendlich ist vor Allem auch für „normale Hobbyprogrammierer“ F# unbedingt einen Blick wert, denn immerhin ermöglicht es, Aufgaben auf eine neue Art sehr schnell und präzise zu formulieren und zu lösen. Erste Schritte F#-Programme sehen auf den ersten Blick zunächst sehr viel anders aus, als man es gewohnt ist. Umso wichtiger ist es, direkt loszulegen und sich gleich mit der funktionalen Denkweise auseinanderzusetzen. Installation: Es gibt leider zur Zeit keine F#-ExpressEdition wie für VB. In den StandardEditionen des Visual Studio ist es integriert und kann wie jede andere Sprache auch verwendet werden (Screenshot aus der kostenlosen Beta des VS2010 Professional): Für alle, die keine der regulären VisualStudio-Editionen haben, müssen wir F# von Hand installieren. Es kann bei Microsoft kostenlos unter diesem Link heruntergeladen werden. Als kostenlose Entwicklungsumgebung mit (leider nicht sehr ausgereifter) F#-Unterstüztung kann #Develop oder die bessere Visual Studio Shell (isolated mode) von Microsoft verwendet werden. Nach der Installation von F# stehen uns zwei Programme zur Verfügung. Ein Kommandozeilen-Compiler, der aus unseren Quellcodes normale .NET-EXEDateien macht, und ein Konsolen-Interpreter namens FSharp Interactive (FSI). Im FSI werden wir unsere ersten Schritte mit F# machen . Wir können Anweisungen in das Konsolenfenster tippen und direkt das ausgeführte Ergebnis sehen. Das Erste, was wir eingeben, ist folgende Zeile. #light;; #light ist ein Kommando, das den Compiler veranlasst, bestimmte Vereinfachungen an der Syntax zu aktivieren und das in jedem Programm als erste Zeile stehen sollte. Die ;; terminieren im FSI eine Eingabe und werden in normalem F#-Code weggelassen. Man könnte jetzt theoretisch die Interaktivkonsole als eine Art Taschenrechner missbrauchen, oder aber wir beginnen mit unserem ersten F#-Programm: let x = "Hallo, Welt" Wie man sieht, erhalten wir prompt die Ausgabe val x : string Man ahnt, es wurde eine Art Variable angelegt. Wir können x jetzt anzeigen lassen: > x;; val it : string = "Hallo, Welt" Das Typsystem Let, das erste Schlüsselwort, das wir kennengelernt haben, ist eines der wichtigsten in F#. Und wie wir gesehen haben, dient es dazu, einen Wert unter einem Namen zu speichern. Wieso Wert und nicht Variable? Das x aus unserem Beispiel ist doch eine Variable? Nein! Das ist es nicht – und zwar, weil es in funktionalen Programmiersprachen keine Variablen gibt. Schockierend? Hier ist es sinnvoll sich vorzustellen, dass bei Berechnungen aus Werten neue Werte entstehen, ohne dass sich der eine dafür ändern muss. Betrachten wir eine Anweisung, die in fast allen Programmiersprachen permanent auftritt, einmal mathematisch. X = X + 1 X, z.B. eine Zählvariable, wird hier um eins erhöht. Was logisch aussieht ist mathematisch bedenklich, denn wenn man auf beiden Seiten X abzieht, kommt man zur hochsinnvollen Aussage. 0 = 1 Es gibt kein X, dass gleich seinem Nachfolger ist, auch wenn das von obiger Aussage vermittelt wird. Das ist ein Sachverhalt, an dem sich funktionale Programmierung stört. In bestimmten „Hardcore“-funktionalen (rein funktionalen) Sprachen wie Haskell ist dieser Gedanke so stark durchgezogen, so dass schon für eine einfache Bildschirmausgabe tief in die mathematische Trickkiste gegriffen werden muss. So extrem stehen die Dinge in F# nicht, immerhin muss die Sprache ja auch noch mit dem nicht funktionalen .NET-Framework interagieren können. Man kann konventionelle Variablen erzeugen (über einen speziellen mutableModifizierer), allerdings sind diese sehr selten und nicht als unbedingt schön oder F#-mäßig empfunden, weshalb man sich angewöhnen sollte, wo es geht nur mit Let zu arbeiten und seine Werte nicht zu ändern. Man kann so Fehlern vorbeugen und verschiedene funktionale Features besser nutzen. Die Aufforderung x = "Foo" ist keine Zuweisung, sondern nur ein Vergleich, der false ergibt. Noch ein zweiter Aspekt ergibt sich aus unserer ersten F#-Zeile: Die FSI-Ausgabe val x : string bedeutet: x ist ein Wert vom Typ string (Zeichenkette). Wenn wir die Eingabe betrachten steht da aber gar nichts von string, wir haben x doch nur „Hallo, Welt“ zugewiesen und doch weiß der Compiler, dass x ein string sein muss. Diese Fähigkeit, die schon VB und C# an bestimmten Stellen boten, nennt sich Typableitung oder Type inference und wurde von F# perfektioniert. Sie ist sogar derart mächtig, dass nicht selten in kompletten F#-Programmen kein einziger Datentyp angeben werden muss. Zum Eingewöhnen in die Typschreibweise werde ich hier dennoch immer wieder die Typsignaturen angeben. let a = 1 let b = 15 let c = a * a + b Auch hier ist der Compiler direkt in der Lage, a, b und c als Ganzzahlen (int) zu bestimmen. Wichtig: Alles in F# hat einen Wert, er ist nur manchmal Nichts (unit). Ein normales Hallo-Welt-Programm lautet in F# unter Verwendung der .NETFunktionen folgendermaßen. System.Console.WriteLine("Hallo, Welt") F# hat allerdings die eingebaute Funktion printf(n). printfn "Hallo, Welt" Beide geben Hallo, Welt in der Konsole aus und geben sinnigerweise nichts (also ()) zurück. Printfn ist selbst hochflexibel, wie folgendes Beispiel zeigt. let name = "Max Mustermann" let alter = 42 printfn "%s ist %i Jahre alt" name alter Die %-Werte sind (wie im printf der Sprache C) Platzhalter für die hinten angeführten Argumente. Interessant ist es, dass die Typsicherheit der Argumente zur Kompilierzeit geprüft wird. printfn "%s" 42.23 Ist somit ungültig, da ein string (%s) mit einer Kommazahl (float) belegt werden sollte. %A passt auf alles. Ein komplettes, kompilierbares F#-Programm sieht z.B. so aus: #light // System-Namespace einbinden open System // Ausgabe machen printfn "Hallo, Welt" (* Verhindern, dass die Konsole geschlossen wird *) ignore Console.ReadKey() Anders als in VB muss hier keine Programm-Klasse angegeben werden. Hier noch eine Übersicht der gängisten Typen. Typ Bedeutung Beispiel int Ganzzahl 42 float Kommazahl -11.3 string Zeichenkette (Wort/Text) "Hallo, Welt" bool Wahrheitswert true, false unit Nichts (Kein Wert – vgl. () void) Für Typumwandlungen existieren gleichnamige Konverterfunktionen. let x = int 42.3 Funktionen F# ist eine funktionale Programmiersprache! Wie dieser Namen vermuten lässt, sind Funktionen folglich für die Sprache von großer Bedeutung. Die Deklaration von Funktionen ist denkbar einfach: Man verwendet Let. Let? Wird das nicht für Variablen Werte verwendet? Ja und nein. In einer funktionalen Sprache ist ein Wert selbst prinzipiell nichts anderes als eine Funktion, die keine Werte übergeben bekommt und einen zurückgibt. Weil sich Werte, von denen sie abhängt, ja nicht ändern dürfen und das Ergebnis einer Funktion nur von ihren Eingabewerten abhängen darf, ergibt diese Definition keine Probleme. F# sieht das zwar etwas lockerer (als z.B. Haskell) und Funktionen dürfen sich auch ein wenig unfunktional verhalten (sonst wäre wie gesagt eine Konsolenausgabe oder ein Zufallsgenerator schon etwas schwierig), aber so lässt sich zumindest das Let erklären. Erste Funktionen Hier kommt unsere erste Funktion. let plus1 x = x + 1 Und der Aufruf: let a = 41 let b = plus1 a let c = plus1 22 Wichtig: Das Gleichheitszeichen ist keine Zuweisung, sondern eine Definition. Mathematisch steht hier: plus1 x= x1 (Lies: Plus1 von x) Eine ganze Funktion in nur einer Zeile und wieder ohne irgendeine Typangabe. Man denke an VB: Function Plus1(ByVal a As Integer) As Integer Return a + 1 End Function Keine Klammern, keine Kommata, keine Typangaben und kein Return. Die letzte Berechnung einer F#-Funktion ist automatisch ihr Rückgabewert. val plus1 : int -> int Der Typ dieser Funktion wird in F#, ähnlich der mathematischen Definition, so notiert, was aber wie gesagt zumeist überflüssig ist, da der Compiler den Typen ermittelt. Mit Typangabe: let plus1 (x : int) : int = x + 1 So - Jetzt wollen uns an eine etwas größere Funktion wagen: let leseString text = printf "%s: " text System.Console.ReadLine() let name = leseString "Gib deinen Namen ein" printfn "Hallo, %s" name LeseString (Funktionen sollten üblicherweise mit Kleinbuchstaben beginnen) gibt eine Aufforderung aus und gibt den eingegebenen Text zurück. Der Compiler weiß wiederum, dass es sich bei text um einen String handeln muss, denn er muss im printf auf das Typkennzeichen %s passen. Mehrzeilige Funktionen – generell alle Blöcke - haben keine schließenden Klammern oder Ends, sondern werden, wie in Python, über ihre Einrücktiefe identifiziert (Tabs sind in F# ungültig, man nehme Leerzeichen). Die letzte Berechnung wird zurückgegeben. Weiter geht’s: let pythagoras a b = let quadrat a = a ** 2.0 sqrt ((quadrat a) + (quadrat b)) let res = pythagoras 3.0 4.0 Diese (gewiss auch sinnvoller implementierbare) Funktion verdeutlicht zwei Dinge: 1: Man braucht keine Klammern, um eine Funktion aufzurufen, die Argumente kommen ohne Kommata einfach hintereinander. 2: Man kann zwei Funktionen / Let's problemlos verschachteln. Interessant ist auch die Typsignatur von pythagoras: val pythagoras : float -> float -> float Die Aussage float entstammt der Verwendung des Hochoperators **, der nur für floats funktioniert – Das eigentlich verwunderliche sind die Pfeile. Wieso jetzt zwei? Müsste es nicht vielmehr irgendwie so lauten? val pythagoras : (float, float) -> float Die Antwort auf diese syntaktische Besonderheit entstammt einem so genialen wie verwirrendem Konzept namens Currying. Wer nur einen groben Überblick über F# gewinnen will, für den genügt es, diesen Teil zu überfliegen. Currying (von einem Herrn namens Curry erfunden), bedeutet, dass eine Funktion immer nur einen Parameter zur Zeit aufnimmt und gebenenfalls eine weitere Funktion zurückgibt. Die Definition lautet geklammert so: val pythagoras : float -> (float -> float) Wenn ich pythagoras 3 4 aufrufe, entspricht das dem Aufruf (pythagoras 3) 4 Pythagoras 3 ist selbst eine Funktion, die noch einen Parameter aufnimmt und dieser ist hier die 4. Per Currying kann man Funktionen teilweise belegen. let plus a b = a + b let plus1 = plus 1 Man sieht: Generell gilt: Der letzte Typ ist der Rückgabetyp, die ersten sind val plus : int -> int -> int val plus1 : int -> int Parametertypen. Currying beschränkt sich nicht nur auf Funktionen, Operatoren selbst sind nichts anderes. let plus = (+) let durch2 = (/) 2 mit val (+) : int -> int -> int val durch2 : int -> int Wichtig: Nur F# und ähnliche Sprachen unterstützen Currying – Eine solche Funktion ist .NET-weit einzigartig. Eine „normale“ .NET-Funktion Pythagoras ließe sich in F# so nachbilden: let pythagoras (a, b) = ... Sie hat die Signatur: val pythagoras : (float * float) -> float und würde folgenermaßen aufgerufen: let res = pythagoras (3, 4) Mathematisch gesehen: pythagoras :ℝ×ℝ ℝ Die Funktion bekommt hier ein sog. Tupel aus zwei Floats übergeben. Die Tupelschreibweise für Parameter ist F#-untypisch und dient v.A. dazu, die Standard-.NET-Funktionen aufzurufen, während sie an anderer Stelle sehr nützlich ist. Per Tupel können Funktionen auch „mehrere“ Rückgabewerte besitzen: let divMod a b = (a / b, a % b) let (div, mod) = divMod 10 8 mit val divMod : int -> int -> (int * int) Hier werden im zweiten Let beide Werte gleichzeitig zugewiesen. Generische Funktionen Ich komme nun zu einem weiteren Highlight im F#-Typsystem, der ebenfalls in den Standard-.NET-Sprachen schon teilweise vorhanden ist. Dieses ist der Grund, weshalb man in F# so prägnant formulieren kann – Generizität. Als Einführungsbeispiel betrachten wir folgende Funktion: let getFive x = 5 Sie nimmt einen Parameter x auf und gibt einfach immer 5 zurück. Die Frage ist: Welchen Typen hat x? Die Antwort ist hier: Es ist egal, welchen Typen x hat, die Funktion funktioniert immer, d.h. vom Typen unabhängig, wie folgendes Beispiel beweist: getFive 1 getFive "Hello, World" getFive (1, "xy", 4.0) Wir brauchen nicht zu sagen, x hat den Typen int oder string. Wir sagen lediglich, x kann jeden beliebigen Typen α haben. FSI notiert das folgenermaßen: val getFive : 'a -> int Der Buchstabe für die sog. Typvariable ist dann einfach alphabetisch fortlaufend. Generische Typen sind extrem wichtig in der funktionalen Programmierung und treten unbewusst fast überall auf, wenngleich man sie fast nie explizit notiert, sondern der Compiler sie bequem ableitet. Higher-Order Functions Da in funktionalen Sprachen alles als Funktion gehandhabt wird, ist es ohne Weiteres möglich, mit Funktionen genau so zu rechnen, wie mit normalen Werten. Funktionen können andere Funktionen als Parameter haben oder welche zurückgeben – Man spricht von sog. Funktionen höherer Ordnung (Higher-Order functions). Betrachten wir folgende Definition: let zahlVerdoppeln let stringVerdoppeln x = 2 * x x = x ^ x // ^-Operator verknüpft Strings let zweimalAnwenden f x = f (f x) ZweimalAnwenden ist eine Funktion, die einen andere Funktion f auf einen xWert zwei mal anwendet. Logischerweise ergibt folgener Ausdruck den Wert 4. zweimalAnwenden zahlVerdoppeln 1 Genauso ergibt Folgendes „XXXX“. zweimalAnwenden stringVerdoppeln "X" Die Funktionen zum Verdoppeln sind als ganz normale Parameter übergeben worden. Interessant sind hier die Typsignaturen. val zahlVerdoppeln : int -> int val stringVerdoppeln : string -> string val zweimalAnwenden : ('a -> 'a) -> 'a -> 'a Hier sieht man, der erste Parameter von zweimalAnwenden hat den Typen ('a -> 'a), d.h. eine Funktion, die einen beliebigen Typen aufnimmt und den selben Typen wieder ausgibt. Deshalb funktioniert auch die Funktion sowohl mit dem Verdoppeln von Zahlen als auch von Strings, sie funktioniert sogar mit jeder beliebigen Funktion, die obiger Typangabe folgt. Beispiel: let zahlVervierfachen x = zweimalAnwenden zahlVerdoppeln x Durch Currying ermöglicht sich sogar folgende Definition: let zahlVervierfachen = zweimalAnwenden zahlVerdoppeln Anonyme Funktionen Bis jetzt hatten unsere Funktionen immer einen Namen. Das muss aber nicht sein, mit dem fun-Schlüsselwort kann man direkt anonyme Funktionen einführen, die komplett gleichwertig mit „richtigen“ sind. Diese Funktion (int -> int) verdoppelt wiederum den Wert x. fun x -> 2 * x Folgende Definition let verdoppeln x = 2 * x ist komplett identisch mit dieser: let verdoppeln = fun x -> 2 * x Anonyme Funktionen können problemlos mehrzeilig sein – Einrücktiefe und letzte Anweisung entscheiden wiederum wie bei normalen Funktionen. Betrachten wir nun einmal ein sinnvolles Beispiel: Wir möchten eine Funktion compose schreiben, die zwei Funktionen f und g miteinander kombiniert (verkettet). let compose f g = fun x -> f (g x) let mal2 x = 2 * x let plus1 x = x + 1 let mal2Plus1 = compose plus1 mal2 Compose nimmt zwei Funktionen und gibt eine anonyme zurück. Mal2Plus1 ist eine Funktion, die von einem Wert x den Wert (2 * x) + 1 berechnet. An dieser Stelle möchte ich auf die VB-Version des selben Codes hinweisen: Sub Main() Dim Mal2Plus1 = Compose(New Func(Of Integer, Integer)(AddressOf Plus1), _ New Func(Of Integer, Integer)(AddressOf Mal2)) End Sub Function Plus1(ByVal x As Integer) As Integer Return x + 1 End Function Function Mal2(ByVal x As Integer) As Integer Return 2 * x End Function Function Compose(Of A, B, C)(ByVal f As Func(Of B, C), _ ByVal g As Func(Of A, B)) As Func(Of A, C) Return Function(x) f(g(x)) End Function Die Typsignatur von compose ist schon etwas trickreich. Wiederum müssen wir die in F# zum Glück nicht angeben, dass tut ja der Compiler für uns! val compose : ('b -> 'c) -> ('a -> 'b) -> ('a -> 'c) Hier wird eindrucksvoll deutlich, wie nah man in funktionalen Sprachen an Problem / Aufgabenstellung arbeiten kann, ohne sich mit Typisierungen oder Deklarationen aufzuhalten. Pipelines F# hat eine syntaktische Besonderheit, den Pipeline-Operator. Von manchen wird er zwar als nicht schön empfunden, aber er hat durchaus seine Vorzüge und wird in F# oft ausgiebig eingesetzt. Wie der Name schon sagt dient er dem Zwecke, eine Reihe von Transformationen hintereinander durchzuführen. Nehmen wir an, wir wollten eine Kommazahl x quadrieren, zu einer Ganzzahl umwandeln und auf dem Bildschirm ausgeben. Klar – Das können wir: printfn "%i" (int (quadriere x)) Resultat: Mengen an Klammern (ein Punkt, bei dem immer über LISP gelästert wird) und die Operationen in falscher Reihenfolge. Durch den magischen Pipeline-Operator |> lässt sich das merklich entzerren. x |> quadriere |> int |> printfn "%i" Durch die Pipeline wird also immer der zuletzt berechnete Wert als letztes Argument der folgenden Funktion verwendet. Das war's schon zu diesem Thema. Verzweigungen If Die einfachste Programmverzweigung ist das aus VB hinreichend bekannte If. If prüft eine bestimmte Bedingung und führt, je nachdem, ob sie erfüllt ist, eine von zwei Alternativen aus. Ein Beispiel sagt mehr als tausend Worte: let passwort = System.Console.ReadLine() if passwort = "xyz" then printfn "Juhu, du hast das richtige Passwort eingegeben" else printfn "Nein!" Der Unterschied zu VB ist, dass das If in F# stehts einen Wert zurückgibt, auch wenn er in diesem Beispiel wieder nichts (()) ist. If-Blöcke werden wiederum durch Einrücktiefe bestimmt und die letzte Anweisung pro Zweig bestimmt das Ergebnis. Deshalb funktioniert auch Folgendes. let abs x = if x >= 0 then x else -x Es existiert auch eine If-Variante ohne else, diese muss vom Typ unit (()) sein. if irgendwas then // Tu was Pattern-Matching Pattern-Matching, zu Deutsch Mustervergleich, ist eine „funktionalere“ und viel mächtigere Alternative zu Ifs. Es kann als eine Weiterentwicklung zu Select...Case gesehen werden und wird benutzt, um Vergleiche zu tätigen und Daten (benutzerdefiniert) auseinanderzunehmen. Pattern-Matchings geben wie Ifs einen Wert zurück. Die einfachste Variante: match System.Console.ReadLine() with | "xyz" -> printfn "Juhu, du hast das richtige Passwort eingegeben" | "xYz" -> printfn "Fast" | _ -> printfn "Nein!" Der Tiefstrich (_) passt auf jeden Eingabewert und ist quasi das Case...Else. Der Clou am Pattern-Matching ist allerdings, dass weniger gegen Werte als gegen Datenmuster geprüft wird. let parse x = match System.Int32.TryParse x with | true, res -> res | false, _ -> 0 parse "42" parse "xyz" Möchte man z.B. einen Eingabe-String, der eine Zahl enthält, in diese Zahl umwandeln, verwendet man dazu meist die Funktion TryParse. Das Ergebnis von TryParse ist ein Tupel (Typ bool * int) der Form (Umwandlung erfolgreich?, Ergebnis). Es wird nun geprüft, ob die Rückgabe auf das Muster „true und ein Ergebnis“ passt – In diesem Falle war das Umwandeln der Zahl erfolgreich und das Ergebnis steht in res, andernfalls erfüllt es das Muster „false und irgendwas“, was bedeutet, dass die Eingabe keine gültige Zahl war und es wird 0 zurückgegeben. In den Mustern können auch sog. Guards, d.h. kleine Prüfungsbedingungen, verwendet werden: let sign x = match x with | 0 -> 0 | _ when x > 0 -> 1 | _ -> -1 Durch die verkürzte Notation mit function kann dieses Beispiel noch weiter gekürzt werden – ein Parameter kommt hinzu und es wird ein Matching eröffnet. Merke: Function erschafft einen zusätzlichen anonymen Parameter, der nur in den Matching-Alternativen auftritt. let sign = function | 0 -> 0 | x when x > 0 -> 1 | _ -> -1 Pattern-Matchings können beliebig viele Bedingungen enthalten, verschachtelt werden und die Ausdrücke können auch mehrzeilig sein. match ... with | x -> printfn "..." 42 | 0 -> printfn "..." 23 Spätestens hier erkennt man, dass es in F# oft viele Techniken gibt, das Selbe zu erreichen bzw. auszudrücken. Rekursive Funktionen Einfache Rekursionen Bis jetzt können wir in F# Werte anlegen, Funktionen definieren und mit ihnen rechnen. Wir konnten alles aber höchstens einmal ausführen lassen, sog. Schleifen und ähnliches fehlt. Das wird sich jetzt mit rekursiven Funktionen ändern. Eine Funktion heißt rekursiv, wenn sie sich selbst aufruft. Folgendes Beispiel: Wie berechne ich die Summe aller Zahlen von 1 bis 100? Antwort in VB: Dieser Code ist denkbar „unfunktional“, es werden ständig Lauf- und Summenvariable geändert. Function Summe(ByVal n As Integer) As Integer Dim Sum = 0 For i = 1 To n Sum += i Next Return i End Function Console.WriteLine(Summe(100)) Stellen wir statt dessen folgende Überlegung an: Die Summe von 1 bis 100 ist doch 100 plus die Summe von 1 bis 99. Die Summe von 1 bis 99 ist 99 plus die Summe von 1 bis 98. Wir können also sagen: 1. Die Summe von 1 bis n ist n plus die Summe von 1 bis n-1. 2. Weiterhin ist die Summe von 1 bis 1 gleich 1. Über Rekursionen können wir das direkt als Programm formulieren: let rec summe = function | 1 -> 1 | n -> n + (summe (n – 1)) printfn "%i" (summe 100) Hinzugekommen ist hier lediglich das Schlüsselwort rec, das angibt, dass eine bestimmte Funktion rekursiv ist. Und siehe da: Keine Mehrfachzuweisungen, keine Schleifen, sondern eine perfekt funktionale Summenfunktion. Viele Zusammenhänge, Baumstrukturen, Listen etc. sind rekursiv strukturiert und daher mit diesen funktionalen Mechanismen gut zu beschreiben. Endrekursion Ein Problem besteht noch bei obiger Summenfunktion: Für große Werte bekommt man Fehlermeldungen. Das liegt daran, dass sich das Programm bei jedem Funktionsaufruf, auch bei einem rekursiven, merken muss, wo es vor dem Aufruf war, damit es danach dorthin zurückkehren kann. Das geschieht auf dem sog. Stapel (Stack) und dieser läuft bei großen Datenmengen schlichtweg über. Die Lösung für dieses Problem nennt sich Endrekursion. Eine Funktion ist endrekursiv, wenn ihr Selbstaufruf der letzte ausgeführte Befehl ist, denn dann muss sich keine Addresse gemerkt werden! Um die berechnete Summe zwischenzuspeichern verwendet man einen sog. Akkumulator-Parameter. Das eigentliche Funktionsargument ist hier durch function anonym gehalten und quasi im Pattern-Matching versteckt. let rec summe acc = function | 1 -> acc | n -> summe (n + acc) (n - 1) Der Aufruf müsste nun summe 1 100 lauten. Der Compilier kann diese Art der Rekursion komplett wegoptimieren. Um das noch einmal zu verdeutlichen, zeige ich für die erste, nicht endrekursive Variante der Summenfunktion einen sog. Auswertungsbaum, also die Schritte, die das Programm zur Auswertung der Summenfunktion geht: = = = = = = = = = = = summe 6 6 + (summe 5) 6 + (5 + (summe 4)) 6 + (5 + (4 + (summe 3))) 6 + (5 + (4 + (3 + (summe 2)))) 6 + (5 + (4 + (3 + (2 + (summe 1))))) 6 + (5 + (4 + (3 + (2 + 1)))) 6 + (5 + (4 + (3 + 3))) 6 + (5 + (4 + 6)) 6 + (5 + 10) 6 + 15 21 Selbst für diese relativ kleine Eingabe sieht man, dass der Baum zwischenzeitig sehr stark wächst. Hier nun die Auswertung einer Endrekursion: summe 1 6 = summe = summe (6 + 1) 7 5 5 = summe = summe (5 + 7) 12 4 4 = summe (4 + 12) 3 = summe 16 3 = summe (3 + 16) 2 = summe 19 2 = summe (2 + 19) 1 = summe 21 1 = 21 Die Größe des Baumes ist hier beschränkt, kein Stapel läuft über. Anschaulich kann man sich den Akkumulator als den Beginn oder Ausgangspunkt einer Berechnung vorstellen. Weil wir von diesem Akkumulatorparameter eigentlich nichts wissen wollen, können wir ihn sogar in einer nichtrekursiven Hauptfunktion verstecken: let summe n let rec | 1 | n = summeRec acc = function -> acc -> summe (n + acc) (n – 1) summeRec 1 n // Aufruf: printfn "%i" (summe 100) Ein weiteres Beispiel für die Fakultätsfunktion: let factorial n = let rec factorialRec acc n = if n = 1 then acc else factorialRec (n * acc) (n - 1) factorialRec 1 n // Aufruf: let n = 6 printfn "%i! = %i" n (factorial n) Wechselseitige Rekursion Hier werde ich kurz auf einen Spezialfall von Rekursionen hinweisen: Wechselseitige Rekursion. Zwei Funktionen heißen wechselseitig rekursiv, wenn beide die jeweils andere aufrufen. Dies ist auch auch mehrere Funktionen übertragbar. Hier ein Beispiel in Visual Basic, über dessen Sinnhaftigkeit man sich streiten kann ;-) Sub Main() ZahlenZeigen(1, 10) End Sub Sub ZahlenZeigen(ByVal Zahl As Integer, ByVal Maximum As Integer) If Zahl Mod 2 = 0 Then GeradeZahlZeigen(Zahl, Maximum) Else UngeradeZahlZeigen(Zahl, Maximum) End Sub Sub GeradeZahlZeigen(ByVal Zahl As Integer, ByVal Maximum As Integer) If Zahl > Maximum Then Return Console.WriteLine("Gerade Zahl: {0}", Zahl) UngeradeZahlZeigen(Zahl + 1, Maximum) End Sub Sub UngeradeZahlZeigen(ByVal Zahl As Integer, ByVal Maximum As Integer) If Zahl > Maximum Then Return Console.WriteLine("Ungerade Zahl: {0}", Zahl) GeradeZahlZeigen(Zahl + 1, Maximum) End Sub Was nicht weiter spektakulär zu sein scheint stellt uns in F# vor ein Problem – Funktionen werden strikt von oben nach unten deklariert und verarbeitet! Dieser eigentlich korrekt anmutende Code kann deshalb nicht funktionieren, weil die Funktion ungeradeZahlZeigen zum Zeitpunkt, da geradeZahlZeigen diese aufruft, „noch gar nicht existiert“. let geradeZahlZeigen zahl maximum = if zahl <= maximum then printfn "Gerade Zahl: %i" zahl ungeradeZahlZeigen (zahl + 1) maximum let ungeradeZahlZeigen zahl maximum = if zahl <= maximum then printfn "Ungerade Zahl: %i" zahl geradeZahlZeigen (zahl + 1) maximum Streng genommen ist dieses Problem sogar zunächst in keiner Sprache elegant lösbar, da wir hier, anders als im obigen VB-Code nicht nur Funktionen, sondern auch Werte („Variablen“) deklarieren, die sich gegenseitig referenzieren, was auch VB nur durch mehrfache Zuweisung umgehen kann. Da wir in F# aber gerne funktional schreiben und ohne jene auskommen möchten, existiert glücklicherweise ein Konstrukt, um wechselseitige Rekursionen doch auszudrücken: let rec geradeZahlZeigen zahl maximum = if zahl <= maximum then printfn "Gerade Zahl: %i" zahl ungeradeZahlZeigen (zahl + 1) maximum and ungeradeZahlZeigen zahl maximum = if zahl <= maximum then printfn "Ungerade Zahl: %i" zahl geradeZahlZeigen (zahl + 1) maximum let zahlenZeigen n = if n % 2 = 0 then geradeZahlZeigen n else ungeradeZahlZeigen n zahlenZeigen 2 11 Erste grundlegende Datentypen F# hat, wie jede funktionale Sprache, verschiedene grundlegende Datentypen zusätzlich zu den „normalen“ .NET-Typen. Diese eingebauten Typen sind speziell auf die Sprache zugeschnitten und können sehr intuitiv verwendet werden. Tupel Den ersten dieser Typen haben wir bereits kennen gelernt: Tupel Sie können beliebig viele Werte aufnehmen und werden mit * notiert (bspw. int * string * float – Ähnlich der mathematischen Schreibweise des kartesischen Produktes). Durch Zuweisungen können sie wieder in ihre Bestandteile zerlegt werden. let meinTupel = (1, "Hallo", 3.141) let a, b, c = meinTupel Eine weitere Möglichkeit Tupel zu zerlegen ist das Pattern-Matching. Typenaliase Typenaliase sind keine Datentypen, sondern „Ersatznamen“ für andere. Man kann so Typangaben einen gesonderten Sinn geben: type Alter = int let alterAusgeben (jahre : Alter) = printfn "%A" jahre Records Records sind Tupeln ähnlich, nur dass hier Werte explizit benannt werden können. Sie entsprechen im weitesten Sinne den Structures aus VB. type Name = { Vorname : string; Nachname : string } Die einzelnen Felder können durch „normale“ Punkt-Schreibweise abgefragt werden. Ein Record ist, genau wie jeder andere Typ, unveränderbar. Eine Instanz wird durch einfache Angabe der Felder oder aus einem Prototypen erstellt. Der Typ wird automatisch abgeleitet. let max = { Vorname = "Max"; Nachname = "Mustermann" } let erika = { max with Vorname = "Erika" } In Funktionen kann der Record-Typ manchmal über die Felder identifiziert werden, da es aber auch uneindeutige Fälle gibt, sollte man den Record-Typen explizit angeben. let grüssen (name : Name) = printfn "Hallo, %s %s" name.Vorname name.Nachname Records können, wie andere benutzerdefinierte Datenstrukturen auch, mit generischen Typen belegt werden. type 'a Eintrag = { Position : int; Wert : 'a } let foo = { Position = 42; Wert = "Hallo, Welt" } Die Typschreibweise für foo lautet val foo : string Eintrag .NET-Typen F# kann alle .NET-Typen normal instantiieren und verwenden. let zufallsgenerator = new System.Random() let zufallszahl = zufallsgenerator.Next(1, 10) Für IDisposable-Typen existiert eine Using-Schreibweise, die diese automatisch am Ende des Blocks entsorgt. use foo = new Objekt() // Objekt verwenden Listen Listen sind die funktionalen Datenstrukturen schlechthin. Eine Liste enthält eine bestimmte Anzahl von Objekten eines bestimmten Typs. Auf Elemente kann nur nacheinander zugegriffen werden, nicht über einen Index. Listen sind unveränderbar, man erstellt aus Listen immer neue Listen, was aber extrem effizient möglich ist. Allem voran sind Listen aber keine Arrays! Listen erstellen let liste1 = [1; 2; 3; 4] let liste2 = [1..4] let liste3 = [1, 2; 3, 4] let liste4 = ["Hallo"; ", "; "Welt"] Ein paar Listen: Wie man sieht ist das Erstellen von Listen denkbar einfach: Eckige Klammern und dazwischen mit Semikolon getrennte Einträge. Kommata erstellen Listen von Tupeln! Eine weitere Möglichkeit ist die Bereichsnotation mit zwei Punkten. Die Beispiellisten haben folgendermaßen notierte Typen (Siehe generische Records) val val val val liste1 liste2 liste3 liste4 : int : int : (int * int) : string list list list list Der Hintergrund für Listen ist folgender: Eine Liste ist aufgebaut aus Knoten – Jeder Knoten enthält einen Wert und einen nächsten Knoten. Die Liste wird durch einen sog. Nil-Knoten beendet, der keine weitere Liste enthält: Eine gleichwertige Listenkonstruktion in der Sprache LISP sieht folgenermaßen aus: (cons 1 (cons 2 (cons 3 '()))) Unter F# können wir den Cons-Operator :: verwenden – Die leere Liste ist []. let liste = 1::2::3::[] Das steckt letztendlich hinter der Schreibweise [1; 2; 3]. Mithilfe des ::-Operators können wir leicht und unglaublich effizient eine Liste nach vorne verlängern: let liste1 = [1..4] // 1, 2, 3, 4 let liste2 = 0::liste1 // 0, 1, 2, 3, 4 Da sich die Listen wie alle Werte nicht ändern können, muss hier nicht einmal Speicher kopiert werden. Es werden einfach Zeiger „umgehängt“. Das Anfügen an das Ende einer Liste ist während dessen vergleichsweise ineffizent. Die Prozedur muss sich durch alle Knoten „durchhangeln“. let liste1 = [1..4] let liste2 = [6..8] // 1, 2, 3, 4 // 6, 7, 8 let liste3 = liste1 @ [5] @ liste2 // 1, 2, 3, 4, 5, 6, 7, 8 Wegen dieser Ineffizienz bauen F#-Programme Listen oft falschherum auf und drehen sie am Ende um, was sich wiederum schnell bewerkstelligen lässt. Manuelle Listenverarbeitung Listenverarbeitung ist in F# sehr wichtig. Es gibt hier zahlreiche Techniken, die grundlegende manuelle Listenverarbeitung ist trotzdem oft unumgänglich. Als klassische, rekursive Datenstruktur lässt sich funktional gut mit Listen arbeiten. Exemplarisch suchen wir eine Funktion, die die Summe einer Liste berechnet. Hier hilft wieder der rekursive Denkansatz von vorher. Wir wissen: Eine Liste ist aus Kopfelement und der Restliste aufgebaut. 1. Die Summe einer Liste ist um das Kopfelement größer als die Summe des Rests. 2. Die Summe von einer leeren Liste ist 0 Mit ein wenig Pattern-Matching lässt sich das fast 1:1 übernehmen: let rec summe liste = match liste with | [] -> 0 | x::xs -> x + (summe xs) Das Matching ist hier folgenermaßen zu sehen: Passt die Liste auf das Muster [], also leere Liste, so gib 0 zurück. Passt die Liste auf das Muster Kopf :: Rest, dann addiere Kopf und Summe des Rests. Die Benennung der einzelnen Teile ist beliebig, es haben sich aber verschiedene Konventionen eingebürgert: • x::xs (das s ist als Plural-s zu verstehen – Beispiel: apple::apples) • x::x' • h::t (head/tail) Unsere Summenfunktion ist leider noch nicht endrekursiv – Wir bemühen also wieder einen Akkumulatorparameter (und schreiben aus optischen Gründen das Match...With durch function um). let summe liste = let rec summeRec acc = function | [] -> acc | x::xs -> summeRec (x + acc) xs summeRec 0 liste Schön an Listen ist auch, dass der Compiler den Umgang mit ihnen so durchschaut, dass er automatisch auf ihre Verwendung schließt und wir (wie im Beispiel) wieder keinen einzigen Typen angeben müssen. Die Pattern-Matchings sind sehr variabel – Man kann sich fast beliebige Kombinationen aus Listenschreibweise ([]), Konstruktorschreibweise (::) und Platzhaltern (_) zusammenbauen. | | | | | | | [] -> x::xs -> x::[] -> _::xs -> _::x::_ -> a::b::rest -> _::[a, b; c, d] -> Die Syntax lässt sich sogar in Funktionsdeklarationen übertragen: let erstesElement (x::_) = x // val erstesElement :: 'a list -> 'a Zurück zur Summenfunktion: Es wäre doch sinnvoll, die Funktion so zu verallgemeinern, dass sie nicht nur die Summe, sondern jede beliebige Operation über die Listen akkumulieren kann. Eine Summenbildung wäre dann lediglich ein Spezialfall, bei dem diese Operation Plus lautet. Eine derartige Funktion nennt sich fold. Dank High-Order-Functions hat man hier kaum Mehraufwand. Man kann sich sogar sparen, den Akkumulatorparameter zu verstecken. let rec fold func acc = function | [] -> acc | x::xs -> fold func (func acc x) xs Mit dieser Funktion lassen sich jetzt Summen, Produkte und Vieles mehr auf einen Schlag abdecken. let sum = fold (+) 0 // Summe: Eine Liste von 0 beginnend mit + zusammenrechnen let prod = fold (*) 1 // Produkt: Eine Liste von 1 beginnend mit * zusammenrechnen let factorial n = prod [1..n] List-Comprehensions List-Comprehensions sind extrem mächtige Konstruktionen, um Listen in andere zu überführen und ihre Benutzung ist denkbar intuitiv. let zahlen = [1..10] let quadratZahlen = [ for x in zahlen -> x * x ] let geradeZahlen = [ for x in zahlen do if x % 2 = 0 then yield x ] Hier sieht man zwei List-Comprehensions in Aktion. Die obere Syntax ist stark vereinfacht, die untere ist die Langform, wobei yield letztendlich einen Wert in die neue Liste schreibt (die Pfeilschreibweise gilt als veraltet und sollte nur in sehr einfachen Fällen wie dem obigen verwendet werden). List-Comprehensions sind letztendlich nur ein möglicher Fall von Computation Expressions, einer Möglichkeit für F#-Programmierer, den Kontrollfluss vom Programmen extrem flexibel verändern zu können, auf die ich später noch einmal zu sprechen kommen werden. Ihre imperativ anmutende Syntax ändert nichts daran, dass ihre Funktionsweise rein funktional ist. Sie können ganz normalen F#-Code enthalten und sich über viel Zeilen erstrecken – wie man es aus normalen Programm kennt. Auch die for-Syntax ist nichts spezielles, sie wird auch in normalem F# verwendet. Mögliche Formen sind 1. for x in Aufzählung do 2. for x in Begin..Ende do 3. for x = Anfang to Ende do Weiterhin unterstützt F# auch while-Schleifen (Siehe Schleifen) Komplett-Beispiel: for (i, x) in [for x in 1..10 -> x, x * x] do printfn "#%i -> %i" i x Mit yield! kann jedes Element einer Liste in eine neue geschrieben werden. Beispiel: let copy list = [ yield! list ] Listenfunktionen Der Dreh- und Angelpunkt jeder F#-Listenverarbeitung sind und bleiben die integrierten Listenfunktionen. Viele lassen sich durch for, Pattern-Matching oder List-Comprehensions besser ausdrücken, manche hingegen sind wichtig. Für die Verwendung muss jeweils ein List . ergänzt werden. Funktio n Zweck Syntax hd/tl Kopf/Rest einer Liste exists Erfüllt mind. Bedingung? forall Erfüllen alle Elemente eine Bedingung? for_all liste length Länger der Liste length liste filter Bestimmte wählen map Funktion auf alle Elemente anwenden mapi Funktion mit Index auf alle Elemente mapi funktion liste anwenden rev Liste umkehren rev liste min/max(b Minimum/Maximum finden y) min(by) liste sum Liste summieren sum liste sum [1..10] sort Liste sortieren (MergeSort) sort vergleich liste Sort (-) [1..10] ein Elemente hd liste Beispiel Elemet per bedingung exists [1..10] even bedingung for_all [1..10] even Bedingung filter funktion liste fold/fold_le Liste akkumulieren (Siehe oben) ft zip eine exists liste hd [1..10] map funktion liste length [1..10] filter [1..10] gerade map [1..10] mal2 rev [1..10] (funktion) max [1..10] . funktion startwert ... (+) 0 [1..10] liste Zwei Listen zu einer Liste aus Tupeln zip liste1 liste2 verschmelzen zip [11.20] [1..10] Ein paar Beispiele Jetzt, da wir einiges über Listenverarbeitung gelernt haben, ist es Zeit für ein paar komplexere Beispiele. Mittelwert, Varianz und Standardabweichung let summeDerQuadrate = [1..10] |> List.fold_left (fun sum i -> sum + i * i) 0 QuickSort: QuickSort ist der Klassiker unter den Sortieralgorithmen. Man wählt aus einer Liste ein sog. Pivotelement (Trennelement) und teilt sie dann so auf, dass man zwei Listen erhält: Eine, die alle kleineren Elemente verglichen zum Pivotelement enthält und eine für die größeren. Dann werden beide Teillisten abermals mit QuickSort weitersortiert und die Ergebnisse in richtiger Reihenfolge zusammengehängt. Wer möchte, kann sich vor dem Weiterlesen an dieser Stelle zunächst selbst am Algorithmus versuchen ... Ein Tipp: Eine leere Liste ist sortiert immer noch leer! Geschafft? Hier ist ein fertiges Programm: #light let rec quickSort = function | [] -> [] | pivot::rest -> let linkeHälfte = [for x in rest do if x < pivot then yield x] let rechteHälfte = [for x in rest do if x >= pivot then yield x] (quickSort linkeHälfte) @ [pivot] @ (quickSort rechteHälfte) let zahlen = [4; 1; 7; 6; 6; 1; 0; 3; 4] let sortiert = quickSort zahlen printfn "Zahlen : %A\nSortiert: %A" zahlen sortiert System.Console.ReadKey() |> ignore Als weitere Übung könnte man probieren, quickSort so umzuschreiben, dass es nicht < zum Vergleichen verwendet, sondern eine benutzerdefinierte Funktion. Notiz: Die obige Implementierung ist natürlich nicht optimal, da die Liste zum Aufteilen zwei mal durchlaufen wird, in einer zeitkritischen Version verwendete man List.partition zum Aufteilen (oder ohnehin das eingebaute List.sort!) Merge Unser zweites Beispiel soll eine Funktion bieten, die zwei sortierte Listen miteinander zu einer großen, sortierten Liste verschmilzt. Beispiel: [1, 2, 7, 8] + [0, 3, 3, 4, 9] -> [0, 1, 2, 3, 3, 4, 7, 8, 9] In imperativen Programmiersprachen ist das relativ umständlich und nur mit temporärem Speicher durchzuführen, während wir das relativ intuitiv formulieren können. Gehen wir schrittweise vor: 1. Zwei leere Listen ergeben eine leere Liste. 2. Wenn beide Listen mindestens ein Element beinhalten, kommt das kleinere von beiden in die Zielliste und die Reste werden verschmolzen. Klingt nach einem klassischen, rekursiven Problem, das sich jedoch problemlos mit Pattern-Matching ausdrücken lässt. Damit Merge auch effizent ist, verwenden wir wiederum einen Akkumulatorparameter und bauen die Liste falschherum auf. Am Ende muss sie umgedreht werden. #light let merge list1 list2 = let rec mergeRec acc list1 list2 match list1, list2 with | [], [] -> | x::xs, [] -> | [] , y::ys -> | x::xs, y::ys when x < y -> | x::xs, y::ys -> = acc mergeRec mergeRec mergeRec mergeRec (x::acc) (y::acc) (x::acc) (y::acc) xs [] [] ys xs (y::ys) (x::xs) ys mergeRec [] list1 list2 |> List.rev let list1 = [1; 2; 7; 8] let list2 = [0; 3; 3; 4; 9] printfn "%A + %A -> %A" list1 list2 (merge list1 list2) System.Console.ReadKey() |> ignore Arrays Arrays kennen wir aus VB, sie sind dort quasi die Standard-Listenstruktur. Ein Array ist eine durchnummerierte Aufzählung fester Größe. Es ist deshalb auch nur in den Fällen sinnvoll, bei denen sich die Aufzählung in ihrer Größe nicht ändert und man zwingend über einen Index auf die Werte zugreifen muss. Sonst sind die flexibleren Lists die bessere Wahl. Der gesamte Umgang mit Arrays ist dem mit Listen sonst sehr ähnlich. let meinArray = [| 1; 2; 3 |] let meineWörter = "Hallo Welt".Split [| ' ' |] Arrays können durch for-Schleifen durchlaufen werden, genau so wie durch Zugriff per nullbasiertem Index. Die Elemente in einem Array sind veränderlich, was prinzipiell „unfunktional“, aber stellenweise auch sinnvoll ist. Hierfür verwendet man eine Notation mit <- . Für Arrays existiert ein Pendant zu den List-Comprehensions sowie für fast alle List.*-Methoden, die jetzt Array.* lauten. Auf Elemente kann in der Syntax .[] zugegriffen werden. Dieser Indizierungszugriff wird bei vielen .NET-Typen wie den System.Collection.-Listen oder auch Strings verwendet. Auch haben Arrays eine .Length-Eigenschaft, die die Anzahl der enthaltenen Elemente angibt. Ein paar Beispiele: Ein klassisches Beispiel für einen imperativen Algorithmus mit veränderlichen, indexbasierten Daten ist das Primzahlsieb des Eratosthenes: let ersteBuchstaben = [| for wort in meineWörter -> wort.[0] |] let meinArray = [1; 2; 3] |> List.to_array let meinArray2 = [| 1..3 |] printfn "Zweiter Eintrag: %i" meinArray.[1] meinArray.[0] <- 42 printfn "Zweiter Eintrag: %i" meinArray.[1] let primeEratosthenes n = let sieb = [| for i = 0 to n do yield 1 |] for i = 2 to n do if sieb.[i] >0 then for j in 2*i..i..n do sieb.[j] <- 0 [ for i in 2..n do if sieb.[i] > 0 then yield i ] Arraytypen werden folgenermaßen notiert: val sieb : int array Sequenzen Sequenzen sind eine abstraktere Form, eine Aufzählung von Objekten zu repräsentieren. Sowohl Listen als auch Arrays sind Spezialformen der Sequenzen. (Für die eingefleischten .NET-ter: Eine Sequenz ist alles, was IEnumerable<T> implementiert). Ein paar Stichpunkte zu den Eigenschaften von Sequenzen: • Typisiert • Unveränderlich • Nur schrittweise zugreifbar • Sequence-Expressions (Spezialfall: List-Comprehension) als bequeme Syntax • Iteration erfolgen per for • Zahlreiche Funktionen in Seq.* Unterschiede: • Keine Initialisierungsschreibweise – Nur yield kann Elemente generieren • Bedarfsauswertung • Kein Pattern-Matching zur Dekomposition • Selten manuelle Verarbeitung Der wichtigste Punkt ist hier die sog. Bedarfsauswertung. Das bedeutet, eine Sequenz wird immer nur soweit berechnet, wie sie gerade benötigt wird. Im Umkehrschluss bedeutet das, wenn ich einen Teil einer Sequenz nicht benötige, wird er, anders als bei Listen, gar nicht erst berechnet. Ich kann also als Sequenz jede beliebige, auch unendliche Folge darstellen. let zahlen = seq { 1..100 } let quadratZahlen = seq { for i in zahlen -> i * i } let alleNatürlicheZahlen = Seq.init_infinite (fun i -> i) let alleQuadratzahlen = seq { for i in zahlen -> i * i } printfn "%A" (quadratZahlen |> Seq.take 20) printfn "%A" (alleQuadratzahlen |> Seq.take 20) Man sieht: Es besteht kein Handhabungs-Unterschied zwischen endlichen und unendlichen Sequenzen, da beide hier nur für 20 Elemente ausgewertet werden. Hinter der Schreibweise seq {} verbirgt sich eine sehr interessante Kontruktion namens Computation-Expression, die im Grunde gar nicht speziell für Sequenzen geschaffen wurde und uns bei asynchronen Berechnungen nocheinmal begegnet. Notiert werden Sequenzen so: val zahlen : seq<int> Der Trick an Sequenzen ist, einfach mit allen Möglichkeiten zu rechnen und es dem Programm zu überlassen, dass es die Möglichkeiten, die nie in Frage kommen, gar nicht berechnet. let prim = let rec primRec altePrimzahlen n = seq { if altePrimzahlen |> List.forall (fun p -> n % p <> 0) then yield n yield! (primRec ( altePrimzahlen @ [n]) (n + 2)) else yield! (primRec altePrimzahlen (n + 2)) } seq { yield 2; yield! primRec [2] 3 } printfn "Die ersten 20 Primzahlen: " for p in prim |> Seq.take 20 do printfn "%i" p Berechnen wir einmal als Beispiel die unendliche Sequenz aller Primzahlen. Dabei können die bereits berechneten Primzahlen gespeichert werden, um die Berechnung später zu beschleunigen. Eine andere Anwendungsform ist z.B. das sehr effiziente Auswerten von Spielbäumen, denn auch hier werden nur die Lösungen generiert, die überhaupt in Betracht kommen. Hier nocheinmal eine Verdeutlichung des Terms Bedarfsauswertung. Hier wird nicht etwa die Eingabe von 10 Werten gefordert und dann eine Ausgabe gemacht, vielmehr wird nach einem Wert gefragt, dieser eingegeben und direkt danach ausgegeben. Die Sequenzfunktion selbst hat dabei keine Abbruchbedingung, wird aber dennoch nur so weit berechnet, wie benötigt. let rec numb3rs() = seq { printf "Gib einen Wert ein: " let num = int(System.Console.ReadLine()) yield num yield! numb3rs() } numb3rs() |> Seq.take 10 |> Seq.iter (printfn "Erhalte Wert: %i\n") System.Console.ReadKey() |> ignore Option types Option-Types werden an vielen Stellen in F# benötigt und zwar überall dort, wo Berechnungen nicht immer ein Ergebnis produzieren. Sie entsprechen den NULL-fähigen Datentypen aus SQL oder VB. Die Signatur val x : int option kann also verstanden werden als: „X ist das Ergebnis einer Berechnung, die eine Zahl ergeben kann, oder aber fehlschlägt und nichts zurückgibt“ Ein klassisches Beispiel für Nullwerte ist die Division zweier Zahlen – entweder funktioniert die Division oder es wurde durch 0 geteilt, was bekanntlich nicht funktioniert: let safeDiv a b = if b <> 0 then Some(a / b) else None Eigentlich recht simpel – Wenn der Divisor ungleich 0 ist, dann geben wir etwas zurück, nämlich a / b, andernfalls nichts. Hierzu verwenden wir die Datenkonstruktoren (siehe Discriminated unions) Some und None. Die zugehörige Signatur: val safeDiv : int -> int -> int option Über Pattern-Matching kann der Options-Type wieder auseinandergebaut werden. An dieser Stelle folgt das längst überfällige und obligatorische Zahlenraten: let zahlenRaten max zahlVersuche = let ziel = (new Random()).Next(1, max) printfn "Willkommen beim Zahlenraten\nErraten Sie die Zahl zwischen 1 und %i" max printfn "Sie haben %i Versuche\n**********************\n\n" zahlVersuche let rec rateZahl versuch = if versuch > zahlVersuche then None else printf "#%i - Geben sie ihren Tipp ein: " versuch match Int32.TryParse(Console.ReadLine()) with | false, _ -> printfn "Keine Zahl!\n"; rateZahl versuch | true, n when n < ziel -> printfn "Zu klein\n" ; rateZahl (versuch + 1) | true, n when n > ziel -> printfn "Zu groß\n" ; rateZahl (versuch + 1) | true, n -> Some(versuch) match rateZahl 1 with | Some(n) -> printfn "Juhu - Sie haben die Zahl in %i Versuchen erraten" n | None -> printfn "Sie haben verloren - Ergebnis: %i" ziel zahlenRaten 20 5 Mengen und Wörterbücher Oft benötigt man Datenstrukturen für Mengen (Set) und Wörterbücher (Map). Eine Menge ist eine Auflistung von einzigartigen Elementen (ohne doppelte Exemplare), ein Wörterbuch speichert Wert/Schlüssel-Paare. Obschon beides zurecht durch Listen machbar erscheint, bietet uns F# hierfür die Datenstrukturen Set<T> und Map<TKey, TValue> an, die durch ihre interne Repräsentation durch sog. Rot-Schwarz-Bäume immer geordnet vorliegen und gleichzeitig die Effizienz des Zugriffs stark erhöhen. Beide Datenstrukturen sind unveränderlich. Map und Set können hier als Konstruktoren aus Listen fungieren. Ähnlich wie für Listen sind die nötigen Funktionen (union, intersect, size, etc) unter Set.* bzw. Map.* vorhanden. Per Internet oder .NET-Reflector kann man die komplette Liste erhalten. Hier zwei Beispiele: Sets: let nums = Set [1..10] let nums2 = nums + Set [5..20] let quadrate = nums2 |> Set.map (fun i -> i * i) let diff = squares – Set [1..5] let schnitt = Set.intersect diff (Set [1..20]) for i in schnitt do printfn "%i" i Maps: let personen = Map ["Max", 42; "Erika", 41] let name = printf "Gib deinen Namen ein: " System.Console.ReadLine() let personen2 = personen.Add("John", 23) match personen2 |> Map.tryfind name with | Some(alter) -> printfn "Du bist %i Jahre alt" alter | None -> printfn "Du bist nicht im System verzeichnet – Glückwunsch" let foo = "Erika" printfn "Hallo %s, du bist %i Jahre alt" foo personen.[foo] Imperative Elemente Auch wenn man in F# nach Möglichkeit funktional programmieren sollte – wenn man das nicht will sollte man es gar nicht erst verwenden, sondern bei VB/C# bleiben – unterstützt F# zwecks Interaktion mit dem .NET-Framework und mancher Anwendungsgebiete wie OCaml selbst auch imperative Elemente. Variablen In F# gibt es neben den Werten auch „echte“ Variablen, die, ganz im Sinne des Wortes, variabel sind. Sie werden sehr selten verwendet – Ein Beispiel sollte hier genügen: let mutable foo = 23 foo <- 42 let mutable summe = 0 for x in 1..10 do summe <- summe + x printfn "%i" summe Die Zuweisungssyntax per <- findet auch bei Zuweisungen an Eigenschaften von .NET-Objekten Verwendung. Verzweigungen Verzweigungen können natürlich auch intuitiv eingesetzt werden – sowohl mit zwei als auch mit nur einer Alternative. let passwortAbfragen() = printf "Geben sie ihr Passwort ein: " let eingabe = System.Console.ReadLine() if eingabe = "Foo" then printfn "Gut" passwortAbfragen() Der Rückgabetyp ist entsprechend nichts (). Schleifen Der Vollständigkeit wegen hier nocheinmal die verschiedenen (schon in ListComprehensions angesprochenen) Schleifenkonstrukte auf eine Blick: printf "Programm fortsetzen? [y/n]" while System.Console.ReadLine() <> "n" do printf "Wirklich? " for i = 1 to 10 do printfn "%i" i for i in 11..20 do printfn "%i" i for i in [21..30] do printfn "%i" i Referenzzellen Sollten wirklich einmal veränderliche Werte nötig sein – und das kommt wohl oder übel vor – neigt man in F# wegen ihrer geringen Flexibilität dazu, Variablen durch eine Alternative auszudrücken – Referenzzellen. Die Referenzzelle kann selbst nicht verändert werden und speichert einen Wert. Dieser wiederum kann verändert werden. Vorteil der Referenzzellen ist, dass diese normal in Datenstrukturen gespeichert, typabgeleitet und aus Funktionen zurückgegeben werden können – Dinge, die bei mutable-Variablen nicht der Fall sind. let foo = ref 23 foo := 42 let summe = ref 0 for x in 1..10 do summe := !summe + x printfn "%i" !summe printfn "%A" [summe, foo] Notiert werden Referenzzellen so: val summe : int ref Die Verwendung der Operatoren := und ! sind dabei charakteristisch und können von der Typinferenz als Anhaltspunkt verwendet werden. Eine interessante Technik ist es, veränderliche Werte in anonymen Funktionen wegzukapseln. Beispiel 1: Die oberste Funktion selbst hat nichts mit veränderlichen Werten zu tun – Die Funktionalität entsteht erst durch die übergebene anonyme Funktion. let alleVerarbeiten list op = for x in list do op x let summe = ref 0 alleVerarbeiten [1..10] (fun x -> summe := !summe + x) printfn "Summe: %i" !summe Beispiel 2: Hier wird die Referenzzelle ganz verborgen. let counter = fun () -> let i = ref 0 fun () -> i := !i + 1 !i let foo = counter() let bar = counter() printfn printfn printfn printfn "%i" "%i" "%i" "%i" (foo()) (foo()) (bar()) (bar()) Dieses Programm hat erstaunlicherweise die Ausgabe 1 2 1 2 – Der vom Benutzer komplett abgekapselte Zähler i in foo oder bar erhöht sich bei jedem Aufruf um eins. Counter könnte dabei als eine Art Konstruktor gesehen werden – foo und bar als Instanzen. val counter : unit -> (unit -> int) val foo : unit -> int val bar : unit -> int Veränderliche .NET-Aufzählungsklassen Die (unfunktionalen) Aufzählungsklassen des FX können in F# natürlich ebenfalls verwendet werden. open System.Collections.Generic let zufall = new System.Random() let meineListe = new ResizeArray<int>() // Aliasname für List<int> for x in 1..10 do meineListe.Add (zufall.Next(1, 100)) meineListe.[0] <- 0 meineListe.Add (42) meineListe.Sort() for x in meineListe do printfn "%i" x Die Werte der Klassen können verändert werden und da auch sie auf IEnumerable basieren, lassen sie sich mit den Seq.*-Funktionen sowie F#Schleifen intuitiv verarbeiten. Wird beim generischen Parameter _ angegeben, versucht der Compiler, den Typen selbst abzuleiten. Vergleich zu imperativen Programmiersprachen An dieser Stelle möchte ich noch ein kleines Beispiel geben, wie eine funktionale Herangehensweise bestimmte Fehler vermeiden kann. Exemplarisch behandle ich ein Programm, welches die Anzahl der Nullen in einer Liste ermitteln soll. Das funktional zu formulieren ist kein Problem: • Eine leere Listen enthält keine Nullen • Eine Liste, die mit 0 beginnt enthält eine Null mehr als die Restliste Code: let rec nullenZählen = function | [] -> 0 | 0::xs -> nullenZählen xs + 1 | _::xs -> nullenZählen xs Hier ist die Korrektheit des Verfahrens unmittelbar erkenntlich. Zum Vergleich muss nun ein imperativer C-Code herhalten: (Der Operator ++ erhöht eine Variable um eins, length ist die Arraylänge) int nullen; for (int i = 1; i <= laenge; i++) { if (zahlen[i] = 0) { nullen++; } } Das sieht augenscheinlich richtig aus: Wir gehen alle Indizes durch, rufen das Arrayelement ab und wenn dieses 0 ist, dann erhöhen wir einen Zähler. Falsch gedacht, dieser winzige Code enhält gleich vier Fehler. Gefunden? Nein? Sie sind sehr versteckt, und auch wenn man hier auf die C-Syntax schimpfen kann, ist doch die eigentliche Ursache, dass man hier dem Programm Schritt für Schritt sagen muss, wie es zu rechnen hat. 1. zahlen[i] = 0 ist kein Vergleich, sondern eine Zuweisung an das aktuelle Arrayelement. Diese lässt deshalb in einem Vergleich verwenden, weil der gesamte Ausdruck den Wert 0 annimmt, der als False gehandhabt wird. Richtig: zahlen[i] == 0 2. Die Schleife zählt von 1 bis Länge; Arrays werden von 0 indiziert, deshalb lautet das richtige Intervall [0; Länge) 3. Nullen, unsere Zählvariable, wurde nicht initialisiert und hat deshalb irgendeinen Wert, aber nicht 0, wie am Anfang gewünscht. Unser korrigierter Code muss also folgenermaßen lauten: int nullen = 0; for (int i = 0; i < laenge; i++) { if (zahlen[i] == 0) { nullen++; } } All dies sind Fehler, die in funktionalen Sprachen nicht oder nur selten auftreten können. In C++ bieten die Standardbibliothek und z.B. BOOST Lösungen für solche Probleme und genau an diesen Stellen wird die Sprache ein Wenig mehr funktional. Im Gegenzug muss man wiederum betrachten, dass unsere Computer imperativ arbeiten. Es gibt einen Arbeitsspeicher, Festplatten, Bildschirme uvm. in denen laufend geschrieben, gelesen und verändert wird. Alles, was irgendwie mit Eingaben und Benutzerinteraktion zu tun hat, ist zwangsläufig nicht mehr rein funktional – Nach diesem Prinzip wäre es ja egal, wie oft und wann ein Benutzer einen Button anklickt. Discriminated unions Discriminated unions kann man alternativen Typen verstehen. am einfachsten als eine Menge von Am besten lässt sich das wie so oft mit einem Beispiel zeigen. Definition: Ein Wahrheitswert (Boolean) ist entweder True (Wahr) oder False (Falsch). Das können wir in F# notieren. type Boolean = True | False Es folgt das Anwendungsbeispiel: let wahr = True let falsch = False let zuBool = function | True -> true | False -> false let ist4 n = if n = 4 then True else False if zuBool (ist4 4) then printfn "Wahr" else printfn "Falsch" True und False sind hier sog. Konstruktoren des Typs Boolean (diese werden konventionell groß geschrieben). Wir können sie wie jeden anderen Wert auch zurückgeben oder an Funktionen übergeben. Interessant ist hierbei v.A. die Funktion zuBool, die unseren Boolean-Typen zu einem Standard-F#-Boolean umwandelt. Hier wird Pattern-Matching eingesetzt, um entstprechend der Alternativen zu verzweigen. Folglich können wir schreiben: let tauschen = function | True -> False | False -> True Die Funktionssignaturen lauten: val var val val val wahr falsch zuBool ist4 tauschen : : : : : Boolean Boolean Boolean -> bool int -> Boolean Boolean -> Boolean Parameterisierte und generische Varianten Unsere Typen und Konstrukturen können aber nicht nur statische Alternativen ausdrücken, sie können auch Werte aufnehmen. Beispiel wäre ein erweiterter Boolean, der neben Wahr und Falsch auch ein Fehlerergebnis mit Meldung aufnehmen kann. type BoolComputation = | True | False | ComputationError of string Beispiel: let kannTeilen a b = if b <> 0 then if a % b = 0 then True else False else ComputationError("Fehler - Teilen durch 0") Eine Auswertung erfolgt, wie gewohnt, mit Pattern-Matching: match kannTeilen 14 0 with | True -> printfn "Teilbar" | False -> printfn "Nicht teilbar" | ComputationError(message) -> printfn "Fehler: %s" message Soll ein Konstruktor mehrere Werte aufnehmen, so werden Tupel-Typen übergeben. Auch können (wie bei generischen Records) Typargumente übergeben werden. So können wir einen wichtigen F#-Typen selbst nachbauen: type 'a Option = | None | Some of 'a Dieser ist identisch mit den Option-Typen, die wir bereits kennengelernt haben, da sie in eben dieser Weise definiert wurde. Rekursive Typen Hier folgt der Hauptgrund, weshalb Discriminated unions so wichtig in F# sind: Sie können mit sich selbst (rekursiv) parameterisiert werden. Berechnungen, Bäume, Verkettete Listen können alle direkt ihrer Definition entsprechend definiert werden. Beispiel 1 – Verkettete Listen: Eine verkettete Liste ist entweder • Leer oder • Ein Kopfelement und eine Restliste Voilà: type 'a LinkedList = | Nil | Cons of 'a * 'a LinkedList let meineListe = Cons(1, Cons(2, Cons(3, Nil))) // int LinkedList let rec summe = function | Nil -> 0 | Cons(x, xs) -> x + summe xs Diese Definition der Liste entspricht wiederum genau der der F#-integrierten lists, wobei Nil (not in list) als [] und Cons (construct) als :: notiert wird. Beispiel 2 – Binäre Bäume: Ein binärer Baum ist entweder • Leer oder • Ein Wurzelelement und ein rechter sowie linker Teilbaum Auch hier kann die Definition direkt in einen Typen gefasst werden. type 'a Tree = | Leaf | Node of 'a * 'a Tree * 'a Tree let myTree = Node(5, Node(3, Node(1, Leaf, Leaf), Node(4, Leaf, Leaf)), Node(7, Leaf, Node(10, Leaf, Leaf))) Bäume so zu erstellen ist wenig sinnvoll, gemeinhin verwendet man sie eher, um z.B. Werte zu sortieren. Dazu benötigen wir Funktionen zum Einfügen und Durchlaufen des Baumes. Einfügen eines Elementes x in einem Baum: • Wenn x <= aktueller Knotenwert: Füge x in den linken Teilbaum ein • Wenn x > aktueller Knotenwert: Füge x in den rechten Teilbaum ein • Wenn der Baum leer ist, dann besteht der neue Baum aus x So sei es: let rec einfügen element baum = match baum with | Node(wurzel, links, rechts) when element <= wurzel -> Node(wurzel, einfügen element links, rechts) | Node(wurzel, links, rechts) when wurzel <= element -> Node(wurzel, links, einfügen element rechts) | _ -> Node(element, Leaf, Leaf) let berechneBaum liste = List.fold (fun baum element -> einfügen element baum) Leaf liste Jetzt benötigen wir nur noch eine Funktion zum Durchlaufen eines Baumes und fertig ist das Binärbaum-Sortieren. Hier gilt: Erst den linken Baum durchlaufen, dann das Wurzelelement und dann den rechten. let rec durchlaufen baum = match baum with | Leaf -> Seq.empty | Node(wurzel, links, rechts) -> seq { yield! durchlaufen links yield wurzel yield! durchlaufen rechts } Hier nocheinmal die komplette Baum-Bibliothek mit Sortierfunktion: type 'a Tree = | Leaf | Node of 'a * 'a Tree * 'a Tree let rec einfügen element = function | Node(wurzel, links, rechts) when element <= wurzel -> Node(wurzel, einfügen element links, rechts) | Node(wurzel, links, rechts) when wurzel <= element -> Node(wurzel, links, einfügen element rechts) | _ -> Node(element, Leaf, Leaf) let berechneBaum liste = List.fold (fun baum element -> einfügen element baum) Leaf liste let rec durchlaufen = function | Leaf -> Seq.empty | Node(wurzel, links, rechts) -> seq { yield! durchlaufen links yield wurzel yield! durchlaufen rechts } let sortieren daten = (berechneBaum >> durchlaufen) daten for x in sortieren [1; 4; -1; 0; 3; 1; 2; 1] do printf "%i, " x Berechnungen mit Discriminated unions Discriminated unions eignen sich sehr gut, um komplexe Daten im Speicher zu repräsentieren. Ein interessantes Beispiel ist hierbei die Darstellung eines Programms, wie in diesem Fall einer arithmetischen Berechnung, im Speicher. Beispiel: Ein Term ist hiermit definiert als eine Zahl, eine Variable oder eine Operation, die zwei Terme verknüpft. type Term = | Zahl | Var | Plus | Minus | Mal | Durch of of of of of of int string Term * Term * Term * Term * Term Term Term Term Ein Ausdruck wie 2x + 1 würde entsprechend so notiert: let beispiel = Plus(Mal(Var("x"), Zahl(2)), Zahl(1)) Das Ausrechnen gestaltet sich dank Mustervergleich und Rekursion extrem einfach: let auswerten variablen = let vars = Map.of_list variablen let rec eval = function | Zahl(n) -> n // Eine Zahl wir zu sich selbst ausgewertet | Var(name) -> vars.[name] // Variablen werden durch konkrete Werte ersetzt | Plus(a, b) -> (eval a) + (eval b) | Minus(a, b) -> (eval a) - (eval b) | Mal(a, b) -> (eval a) * (eval b) | Durch(a, b) -> (eval a) / (eval b) eval let ergebnis = auswerten ["x", 10] beispiel printfn "%i" ergebnis Diese Art der Repräsentation von Berechnungen ist extrem hilfreich. Man kann in F# beispielsweise relativ leicht eigene Programmiersprachen schreiben, deren Quelltext in solche Strukturen überführt und dann ausgewertet wird. Später werden wir sehen, dass es sehr praktische Wege gibt, Text-Eingaben zu parsen, d.h. sie vom Programm in eine solche, verwertbare Form umzuwandeln. Als Minimal-Beispiel werde ich trotzdem einmal zeigen, wie man einen Term in sog. UPN-Notation (umgekehrte polnische Notation) parst. Bei einem UPN-Term stehen die Operatoren (Rechenzeichen), also z.B. + oder -, nicht zwischen ihren Operanden, sondern hinter ihnen – Man spricht auch von Postfix-Notation, während ein „normaler“ Term in Infix-Notation steht. Beispiel: Der Term (1 + 2) * 3 entspricht folgendem in UPN. 1 2 + 3 * Der Vorteil dieser Notation im Bezug auf Computer ist vor Allem, dass sie sich viel leichter auswerten lässt und man keine Operatorprioritäten berücksichtigen muss (Punkt-Vor-Strich). Tatsächlich das Ausrechnen oder auch Umwandeln eines solchen Terms in konventionelle Notation mithilfe eines sog. Stapels nahezu trivial. 1. Eine Zahl wird oben auf den Stapel gelegt. 2. Ein Operator nimmt die beiden obersten Werte vom Stapel, rechnet mit ihnen und schreibt sie wieder oben hinauf Der folgende Ablauf zeigt, wie man obigen Term schrittweise ausrechnet: Beginn: 1 : 2 : + : 3 : * : Ende : Leerer Stapel => [] 1 auf Stapel legen => [1] 3 auf Stapel legen => [2, 1] Werte addieren => [1 + 2] 3 auf Stapel legen => [3, 1 + 2] Werte multiplizieren => [(1 + 2) * 3] Oberstes Element enthält das Ergebnis => (1 + 2) * 3 Das können wir nun problemlos in funktionalen F#-Code schreiben. Wir betrachten eine rekursive Funktion parse, die eine Liste von Symbolen (Zahlen, Variablen oder Operatoren) sowie einen Stapel von Termen als Argument enthält. Ein Stapel ist dabei nichts als eine konventionelle Liste, an die, wie üblich, von vorne angehängt wird. let rec parse stapel symbole = Jetzt gilt es, die einzelnen Vorschriften umzusetzen. Mit Pattern-Matching müssen wir nichteinmal groß umformulieren. match stapel, symbole with | y::x::stapel', "+"::symbole' | y::x::stapel', "-"::symbole' | y::x::stapel', "*"::symbole' | y::x::stapel', "/"::symbole' -> -> -> -> parse parse parse parse ((Plus(x, y))::stapel') symbole' ((Minus(x, y))::stapel') symbole' ((Mal(x, y))::stapel') symbole' ((Durch(x, y))::stapel') symbole' 1. Ist das aktuelle Symbol ein Operator und enthält der Stapel mind. zwei Elemente, dann verrechne beide und setze die Berechnung mit den restlichen Symbolen und dem aktuellen Ergebnis oben auf dem Stapel fort: 2. Wenn wir keinen Operator vorliegen haben, prüfe, ob es sich um eine Zahl oder eine Variable handelt und schreibe sie entsprechend auf den Stapel. | _, sym::symbole' -> match Int32.TryParse(sym) with | (true, i) -> parse ((Zahl(i)::stapel)) symbole' | _ -> parse ((Var(sym))::stapel) symbole' 3. Sind alle Symbole durch, sind wir fertig, geben also das oberste (und hoffentlich einzige) Element des Stapels zurück. | _, [] -> List.hd stapel Die ganze Funktion sieht dann so aus: let rec parse stapel symbole = match stapel, symbole with | y::x::stapel', "+"::symbole' -> parse ((Plus(x, y))::stapel') symbole' | y::x::stapel', "-"::symbole' -> parse ((Minus(x, y))::stapel') symbole' | y::x::stapel', "*"::symbole' -> parse ((Mal(x, y))::stapel') symbole' | y::x::stapel', "/"::symbole' -> parse ((Durch(x, y))::stapel') symbole' | _, sym::symbole' -> match Int32.TryParse(sym) with | (true, i) -> parse ((Zahl(i)::stapel)) symbole' | _ -> parse ((Var(sym))::stapel) symbole' | _, [] -> List.hd stapel Dieser Code ist tatsächlich völlig ausreichend, um einen mathematischen UPNAusdruck in eine F#-Struktur zu überführen. Was allerdings auffällt ist noch eine gewisse Redundanz in den einzelnen Matching-Fällen. Es tritt immer dasselbe Muster auf: parse (...::stapel') symbole' Ein solches Muster haben wir bereits kennen gelernt – Es entspricht einer FoldFunktion aus dem Kapitel über Listenverarbeitung. Dabei summieren wir quasi die Eingabesymbole zu einem Stapel auf. Wir können also tatsächlich die gesamte Funktion als einen Aufruf von Fold vereinfachen – Die Vorschrift, die wir dabei geben, heißt lediglich: „Wenn wir einen Stapel und ein Eingabesymbol haben, was kommt raus?“ Und zwar das: let stapelKombinieren stapel symbol = match symbol, stapel with | "+", y::x::stapel' -> (Plus(x, y))::stapel' | "-", y::x::stapel' -> (Minus(x, y))::stapel' | "*", y::x::stapel' -> (Mal(x, y))::stapel' | "/", y::x::stapel' -> (Durch(x, y))::stapel' | sym, _ -> match Int32.TryParse(sym) with | (true, i) -> (Zahl(i))::stapel | _ -> (Var(sym))::stapel Als arbeitsfähiges Gesamtpaket mit Verarbeitung der Eingabe sieht unsere Parser-Funktion also so aus: let parse2 (input : string) = let stapelKombinieren stapel symbol = match symbol, stapel with | "+", y::x::stapel' -> (Plus(x, y))::stapel' | "-", y::x::stapel' -> (Minus(x, y))::stapel' | "*", y::x::stapel' -> (Mal(x, y))::stapel' | "/", y::x::stapel' -> (Durch(x, y))::stapel' | sym, _ -> match Int32.TryParse(sym) with | (true, i) -> (Zahl(i))::stapel | _ -> (Var(sym))::stapel input.Split [| ' ' |] |> Array.fold stapelKombinieren [] |> List.hd Mit den generierten Term-Objekten kann man jetzt vielerlei Berechnungen durchführen, sie z.B. optimieren, vereinfachen, ableiten oder einfach nur schreiben. Als Beispiel zeige ich hier ein kleines Programm, das einen solchen Ausdruck wieder als Infix-Term schreibt: let rec alsInfix = function | Zahl(n) -> sprintf "%i" n | Var(sym) -> sprintf "%s" sym | Plus(a, b) -> sprintf "%s + %s" (alsInfix a) (alsInfix b) | Minus(a, b) -> sprintf "%s - %s" (alsInfix a) (alsInfix b) | Mal(a, b) -> sprintf "%s * %s" (klammern a) (klammern b) | Durch(a, b) -> sprintf "%s / %s" (klammern a) (klammern b) and klammern expr = match expr with | Plus(_) | Minus(_) -> sprintf "(%s)" (alsInfix expr) | _ -> alsInfix expr printf "Geben sie einen UPN-Ausdruck an: " let upn = Console.ReadLine() let term = parse2 upn printfn "Term in Infix-Notation: %s" (alsInfix term) Objektorientierung Bis hierher haben wir viele interessante Konzepte kennengelernt, doch eines, das für einen Großteil der heute populären Programmiersprachen zentral ist, fehlt noch: Die Objektorientierung. Objektorientiertes Programmieren ist, wie funktionales auch, ein Ansatz, um Programme sinnvoll zu gestalten und dem Programmierer so das Leben leichter zu machen. Die Idee dahinter ist, Klassen von Objekten zu erstellen, die über bestimmte Eigenschaften und Fähigkeiten verfügen und aus denen dann konkrete Objekte (Instanzen) erstellt werden können, die wiederum miteinander wechselwirken. So werden Daten und Quellcode in sinnvolle, wiederverwendbare Einheiten gruppiert. Beispiel: Ein Auto. Ein Auto hat verschiedene Eigenschaften, wie eine Farbe, einen Hersteller, ein Kennzeichen, seine Fahrtgeschwindigkeit und verschiedene Fähigkeiten, z.B. Anfahren oder Bremsen. Aus dieser Vorschrift heraus kann man nun „konkrete“ Autos erstellen und im Programm fahren lassen. Die Vorteile der Objektorientierung bestehen in der anschaulichen, oft wirklichkeitsnahen Darstellung von Zusammenhängen. Zudem beugt es Fehlern vor, da in einer Klasse Programmcode gekapselt, also vor dem Programmierer verborgen wird. Bildlich gesprochen: Ich kann ein Auto fahren, ohne wissen zu müssen, wie ein Motor exakt funktioniert und ohne dort Schaden anrichten zu können – der Motor ist vor dem Fahrer verborgen. Die .NET-Umgebung, auf der F# aufbaut, ist stark objektorientiert – letztendlich ist sogar alles, womit gearbeitet wird, Listen, Zahlen oder Funktionen ein Objekt. Dem kann sich auch F# nicht entziehen und stellt zusätzlich zu seinen funktionalen Fähigkeiten auch objektorientierte bereit. Beide Paradigmen haben Vorteile und Nachteile, Gemeinsamkeiten und Differenzen. Allgemein lässt sich sagen, dass es sehr problemabhängig ist, ob nun funktionale oder OO-Ansätze besser geeignet sind. Bis jetzt und ein bisschen weiter Daten in sinnvolle Einheiten gruppieren ... Das können wir doch schon: • Records • Tupel • Discriminated unions Tatsächlich handelt es sich hier um sehr einfache Klassen, gemacht um Daten zu speichern. Ein Beispiel zur Erinnerung: type Punkt = { x : float; y : float } type Linie = { von : Punkt; bis : Punkt } let linie = { von = { x = 1.0; y = 2.5 }; bis = { x = 4.0; y = 5.5 } } Und wenn wir nun die Länge einer Linie bestimmen wollen? Kein Problem – Eine Funktion muss her. let länge li = ((li.von.x - li.bis.x)**2.0 + (li.von.y – li.bis.y)**2.0)**0.5 printfn "Länge: %f" (länge linie) Der objektorientierte Ansatz wäre hier allerdings ein anderer. Man würde sagen, die Länge ist eine Eigenschaft des Objekts Linie, ein Element (member) dieses Typs. Das lässt sich in F# ebenfalls notieren. type Linie = { von : Punkt; bis : Punkt } with member this.Länge = ((this.von.x - this.bis.x)**2.0 + (this.von.y this.bis.y)**2.0)**0.5 Member fügt dem Typ ein neues Element hinzu – In diesem Fall die Eigenschaft Länge. Man kann sich member als das Pendant zu let auf Klassenebene vorstellen, Funktionen und Werte funktionieren wiederum auf gleiche Art. Einziger Unterschied ist das this, man kann es sich als den Stellvertreter der Instanz vorstellen, für die die Funktion aufgerufen wird. Man kann statt dessen auch andere Namen verwenden. Anwendung: printfn "Länge: %f" Linie.Länge Voilà – Die neue Eigenschaft/Funktion kann einfach per Punkt angesprochen werden. Das ganze Verfahren ist bei Discriminated Unions nicht verschieden. In diesem Beispiel wurden dem Typen Berechnung gleich zwei Member type 'a Berechnung = | Ergebnis of 'a | Fehler with member private this.Fehlgeschlagen = match this with | Fehler -> true | _ -> false member this.Ausruf fehlermeldung = if this.Fehlgeschlagen then printfn "%s!" fehlermeldung else printfn "Juhu" let geradeZahl n = if n % 2 = 0 then Ergebnis n else Fehler for zahl in [13; 42] do (geradeZahl zahl).Ausruf "Nein" hinzugefügt – Der eine ist nun eine Funktion vom Typ string -> unit und Fehlgeschlagen eine Eigenschaft vom Typ bool, die bestimmt, ob die Berechnung fehlgeschlagen ist. Bemerkenswert ist, dass durch member private letztere als privat gekennzeichnet wurde. Das bedeutet, die Eigenschaft ist nur innerhalb des Objekts sichtbar, also in diesem Fall nur für Ausruf. Von außen kann die Eigenschaft nicht abgefragt werden. Erste Klasse Bis jetzt haben wir die F#-Strukturen wie Records oder Discriminated Unions um weitere objektorientierte Merkmale bereichert. Was jetzt folgt, setzt noch einen Schritt tiefer an: Wir schreiben unsere erste eigenständige F#-Klasse. Als Beispiel verwenden wir hier ein Auto. Als Eigenschaften bekommt es eine Maximalgeschwindigkeit und eine Modellbezeichnung. type Auto(bezeichnung : string, vmax : int) = member this.VMax = vmax member this.Bezeichnung = bezeichnung member this.InfosAnzeigen() = printfn "Ich bin ein %s und kann %i km/h schnell fahren" bezeichnung vmax So, nehmen wir das Beispiel schrittweise auseinander: Es wird hier offensichtlich ein neuer Typ (eine Klasse) namens Auto beschrieben. Dieser verfügt über zwei öffentliche Eigenschaften – VMax und Modell – sowie eine Methode (Funktion) namens InfosAnzeigen, die die entsprechenden Daten des Autos auf der Konsole ausgibt. Aber was bedeutet die Zeile type Auto(bezeichnung : string, vmax : int) nun genau? Modell und vmax sind hier sog. Konstruktorargumente. Wenn später eine AutoInstanz erzeugt wird, übergeben wir dieser direkt jene beiden Werte. Sie sind nur innerhalb der Klasse sichtbar. Das geht so: let porsche = new Auto("Porsche", 330) let truck = new Auto("Vierzigtonner", 80) Diese beiden Objekte können wir nun verwenden. for auto in [ porsche; truck ] do auto.InfosAnzeigen() Große Klasse Die F#-Klassensyntax erlaubt alles, was mit Klassen in VB oder C# auch machbar ist. Von lokalen Variablen über Eigenschaften bis hin zu Methoden und gekapselten Werten. Vererbung Schnittstellen Ausnahmebehandlung Code-Strukturierung Module Namespaces Language-Oriented Programming Herzlichen Glückwunsch - Mit den grundlegenden Kapiteln sind sie an dieser Stelle durch. Womit es im Folgenden weiter geht, sind Konzepte des sog. Language-Oriented Programming (LOP). Damit bezeichnet man zusammenfassend die Möglichkeiten, in einer Sprache „an dieser Sprache“ zu programmieren. Und gerade hier man mit F# wegen seiner hohen Flexibilität sehr effizient in der Lage, diese Sprache seinen aktuellen Bedürfnissen anzupassen, um direkter und meist einfacher an einem Problem formulieren zu können. (Anmerkung – Bei den folgenden Kapiteln werde ich mich oft am englischsprachigen F#-Wikibook orientieren) Operatordefinition Schon in VB ist es möglich, bestimmte Operatoren wie +, -, * oder / für eigene Klassen zu überladen. Dim x = -SomeVector * New Vector(-1, 3) * New Vector(0, 2) + Vector2 F# geht hier noch einen Schritt weiter: Es ist möglich, völlig neue, eigene Operatoren aus Sonderzeichen zu erschaffen. Ein Operator unterscheidet sich sonst kaum von einer herkömmlichen Funktion. Betrachten wir die n Fakultätsfunktion. n !=∏ i=1⋅2⋅3⋅⋯⋅n i=1 let (!) n = seq {1..n} |> Seq.fold ( * ) 1 printfn "6! = %i" !6 Für Infixoperatoren (Operatoren zwischen 2 Operanden – a + b) ist das Konzept gleich. let (@@) x y = 10 * x + y let res = 2 @@ 3 printfn "%i" res Wenngleich sich bei diesem Beispiel über die Sinnhaftigkeit gestritten werden kann, sind solche Operatoren z.T. Doch sehr nützlich! Im Folgenden wird eine neue Syntax für optisch ansprechende Abfragen mir Regulären Ausdrucken eingeführt: open System open System.Text.RegularExpressions let (=~) text pattern = (new Regex(pattern)).IsMatch(text) printf "Geben sie eine dreistellige Zahl ein: " if Console.ReadLine() =~ "\d{3}" then printfn "Danke" else printfn "Falsche Eingabe" Weiterhin sehen wir hier etwas, was auf viele Bereiche von F# zutrifft: F# besteht aus F#! Viele „eingebaute“ Sprachelemente sind nichts weiter als normale Funktionen und Typen, wie man sie selbst formulieren kann: let (|>) x f = f x 1 |> printf "i: %i" Sogar das ganze System der Referenzzellen ist rein durch Benutzertypen- und Operatoren definiert: type 'a ref = { mutable contents : 'a } let ref x = { contents = x } let (:=) p v = p.contents <- v let (!) p = p.contents Maßeinheiten In F# kann man, Wissenschaftler wird’s freuen, endlich Werten eine Maßeinheit mitgeben. Unmittelbarer Nutzen hiervon ist, dass immer klar ist, was für Werte man vorliegen hat und dass v.A. Fehler (Äpfel mit Birnen vergleichen) vom Compiler als solche erkannt werden. Wir können Einheiten als leere Typen definieren, die mit dem Measure-Attribut versehen sind: [<Measure>] type m [<Measure>] type s [<Measure>] type kg Auch können wir Typsynonyme einrichten: [<Measure>] type N = kg * m / s ^ 2 Wenn wir nun einheitenbehaftete Werte notieren, hängen wir die gewünschte Einheit in spitzen Klammern an – Alle Operationen verhalten sich wie intuitiv zu erwarten. let g = 9.81<m/s^2> let masse = 1.0<kg> let kraft = masse * g let kraft2 = 10.0<N> let deltaF = kraft2 - kraft Der Compiler kann dabei die verwendeten Typen automatisch prüfen und ggf. ableiten, so dass für kraft und deltaF korrektermaßen die Einheit Newton angenommen wird. Der Code let delta = kraft2 - masse schlägt bereits beim Kompilieren fehl – Einheiten inkompatibel! Bei Funktionen verhält sich das Ganze nicht anders: let fallzeit höhe : float<s> = sqrt (höhe / (0.5 * g)) Mit der Angabe, die Fallzeit solle in Sekunden angegeben sein, kann F# den Parameter höhe automatisch auf Meter inferieren. printfn "%A" (fallzeit 10.0<m>) printfn "%A" (fallzeit 20.3<kg>) // Fehler! Inkompatible Einheiten Um zwischen Einheiten umzurechnen, Umrechnungsfaktoren multipliziert werden. muss lediglich In der Fsharp.Powerpack.dll werden die physikalischen Konstanten bereits definiert! gängigen mit SI-Einheiten den und Aktive Muster Wie der Name schon vermuten lässt, sind Aktive Muster (Active Patterns) eine Möglichkeit, mit der wir benutzerdefiniertes, aktives Verhalten in Mustervergleichen definieren können. Sie sind nichts Fundamentales und doch oft eine große Hilfe, wenn es darum geht, Strukturen von Daten sehr kompakt zu verarbeiten oder zu vergleichen. Einfache Muster Zu erkennen definieren sind sie leicht – Aber ein Beispiel sagt mehr als tausend Worte: let (|Gerade|Ungerade|) x = if x % 2 = 0 then Gerade else Ungerade printf "Geben sie eine Zahl ein: " match int (Console.ReadLine()) with | Gerade -> printfn "Die Zahl ist gerade" | Ungerade -> printfn "Die Zahl ist ungerade" Das oben stehende dünkt einen nicht sonderlich kompliziert, sein Effekt ist unmittelbar ersichtlich. Das aktive Muster wird durch eine Funktion in den Banana-Brackets (||) reprästiert. Dabei werden hier sozusagen zwei anonyme Fälle einer Discriminated Union generiert – Gerade und Ungerade. Beim Auswerten des Pattern-Matchings wird die Funktion ausgewertet und zu dem Fall verzweigt, den diese zurückgibt – Fertig. Eine weitere gern genommene Verwendungsmöglichkeit aktiver Muster ist das elegante Zerlegen von Datenstrukturen, die normales Pattern-Matching nicht erlauben. Typisch hierfür sind z.B. Sequenzen. Zwar existieren hier viele integrierte Hilfsfunktionen und die sehr hilfreiche Notation der Sequencecomprehensions, aber trotzdem ist, anders als bei Listen, die manuelle Verarbeitung nur mit einiger Kenntnis des .NET-Frameworks und relativ unschönen, nicht-funktionalen Schleifenkonstrukten möglich. Abhilfe schaffen aktive Muster: let (|Leer|Knoten|) sequenz = if Seq.isEmpty sequenz then Leer else Knoten (Seq.hd sequenz, Seq.skip 1 sequenz) let rec seqSumme = function | Leer -> 0 | Knoten(x, xs) -> x + seqSumme xs Einziger Unterschied zum vorherigen Muster ist, dass der Fall Knoten einen Wert zurückgibt in Form eines 2-Tupels zurückgibt. Partielle Muster Die nächste Variante ist schon zu mehr fähig. Es handelt sich um partielle Muster, also solche, die fehlschlagen können und dabei Werte transportieren. Beim Mustervergleich werden die angegebenen Fälle so lange ausgewertet, bis einer Some(wert) zurückgibt. Folgendes Beispiel zeigt einen eleganten Weg, Daten zu interpretieren und zu konvertieren. let (|Zahl|_|) eingabe = match Int32.TryParse eingabe with | true, res -> Some(res) | _ -> None let (|Kommazahl|_|) eingabe = match Double.TryParse eingabe with | true, res -> Some(res) | _ -> None printf "Geben sie etwas ein: " match Console.ReadLine() with | Zahl n -> printfn "Zahl: %i" n | Kommazahl x -> printfn "Kommazahl: %f" x | str -> printfn "<unbekannt>: \"%s\"" str Viel Neues ist auch hier nicht dazugekommen – Der Tiefstrich in der Funktionsdefinition symbolisiert das Partialität des Vergleichs, der Typ der Funktion ist immer ein Option-Value. Interessant ist es dabei auch, Funktionsargumente zu betrachten. die Muster als Spezifikationen für let (|NichtNull|_|) x = if x <> 0 then Some(x) else None let teilen a (NichtNull b) = a / b printfn "%A" (teilen 1 2) printfn "%A" (teilen 2 0) Der zweite Fall wird mit einer MatchFailureException abbrechen, da der 2. Funktionsparameter die Bedingung, er solle nicht 0 sein, nicht erfüllt. Die Möglichkeit, Muster ineinander zu kombinieren, lässt extrem mächtige Ausdrücke zu. Folgender Ausdruck kann einen Eingabestring gegen einen Regulären Ausdruck prüfen und gibt im Erfolgsfall die angegebenen Gruppen als Liste zurück. let (|RegMatch|_|) (grps : int list) regex eingabe = let treffer = Regex.Match(eingabe, regex) if not treffer.Success then None else let res = [| for g in treffer.Groups -> g.Value |] Some([ for i in grps -> res.[i] ]) Mit den bereits vorhandenen Funktionen kombinieren wir dies zu einer Funktion, die einen String als Angabe von 2D-Koordinaten interpretieren soll, wobei beide Koordinaten ganzzahlig und ungleich 0 sein sollen! let punkt = function | RegMatch [1;2] "\((.+?), (.+?)\)" [Zahl(NichtNull x); Zahl(NichtNull y)] -> (x, y) | _ -> failwith "Falsches Format" Hier wird die vom ersten Muster ausgegebene Ergebnissequenz von weiteren immer mehr zerlegt. printfn "%A" (punkt "(1, 2)") printfn "%A" (punkt "(1, 0)") printfn "%A" (punkt "foo") Logischerweise wird also der erste Aufruf korrekt funktionieren, die beiden anderen aber mit Fehler abbrechen, da eben das spezifizierte Muster nicht erfüllt ist. Computation Expressions Willkommen zu den sog. Computation Expressions. Diese sind zweifellos ein Highlight von F# und machen es unter den Programmiersprachen vielleicht nicht einzigartig, aber auf jeden Fall besonders. Eine Computation Expression ist eine Verallgemeinerung eines Programmablaufs, den der Programmierer selbst definieren kann. Dabei können in normaler F#-Syntax komplizierte Abläufe automatisch generiert werden. Einleitendes Beispiel Ich beginne dieses Kapitel mit einer einfachen Aufgabe: Es gilt eine Funktion zu entwickeln, die den Benutzer auffordert, drei Zahlen a, b und c einzugeben, und deren Summe zurückgibt. Da der Benutzer Fehler machen kann, verwenden wir Option-Typen. Geschafft? Das Ergebnis wird vielleicht so aussehen: let zahlEingeben id = printf "%s: " id match Int32.TryParse (Console.ReadLine()) with | true, res -> Some(res) | _ -> None let summe() = let a = zahlEingeben "a" let b = zahlEingeben "b" let c = zahlEingeben "c" match a, b, c with | Some(x), Some(y), Some(z) -> Some(x + y + z) | _ -> None Sieht richtig aus, funktioniert auch. Es gibt nur ein Problem: Man betrachte folgende Eingabe: A: xyz B: 10 C: 12 Hmm, da hat doch der Benutzer schon beim ersten Wert etwas Ungültiges eingegeben, und trotzdem geht die Abfrage weiter. Eigentlich ist doch klar: Das kann gar nichts mehr werden, an dieser Stelle müsste eigentlich Schluss sein! Aber gut, ich habe sie vermutlich unterschätzt ;) Also ab zur richtigen Funktion: let summe() = match zahlEingeben "a" with | None -> None | Some(a) -> match zahlEingeben "b" with | None -> None | Some(b) -> match zahlEingeben "c" with | None -> None | Some(c) -> Some(a + b + c) Bitte was? ZEHN Zeilen für eine so triviale Aufgabe? Die Frage ist berechtigt und im Weiteren geht es darum, wie man diesen Code vereinfachen könnte. Der obige Code enthält zahlreiche Redundanzen und wie gewöhnlich könnten wir versuchen, den oft verwendeten Code als Funktion auszulagern. Und hier kommt eine schlagende Idee ins Spiel: Wir definieren eine Funktion namens Bind und teilen die Berechnung in einen aktuellen Stand und den Rest der Berechnung auf: 1. Wenn der Benutzer einen gültigen Wert eingibt, führen wir mit diesem Wert die Berechnung fort 2. Wenn der Benutzer einen Fehler macht, ist Schluss; der Rest der Berechnung wird einfach gar nicht fortgesetzt. Aber wie geben wir „den Rest einer Berechnung“ an? Klar: Als Funktion. Et voilà: let bind eingabe rest = match eingabe with | Some(ergebnis) -> rest(ergebnis) // Gültig - Rest berechnen | None -> None // Ungültig - Tschüss Der Typ ist hierbei val bind : 'a option -> ('a -> 'b option) -> 'b option Mithilfe dieser unscheinbaren Funktion können wir unsere Summenfunktion signifikant vereinfachen: let summe() = bind (zahlEingeben "a") (fun a -> bind (zahlEingeben "b") (fun b -> bind (zahlEingeben "c") (fun c -> Some(a + b + c)))) Diese Art von Funktionsauswertung, bei der der Rest einer Berechnung als Funktion mitübergeben wird, nennt sich Continuation-passing style (CPS). Computation Expressions definieren Verallgemeinert man das Konzept, das wir gerade angewendet haben, so spricht man von sog. Monaden. Vor Monaden haben viele Programmier Angst, denn sie werden oftmals als sehr kompliziert und theoretisch empfunden, aber letzendlich sind sie nur eines: Extrem nützlich. Dabei ist bei Allem nämlich nichts anderes gemeint, als dass wir für einen Typen zwei Funktionen definiert haben: Die eine ist Bind (oder auch >>=) und tut genau das wie im obigen Beispiel – Einen Wert auf bestimmte Weise mit dem Rest einer Berechnung verknüpfen. Die andere ist Return und dient lediglich dazu, ein Ergebnis zurückzugeben. Für die Option-Typen ist auch Return offensichtlich – Es handelt sich um den Konstruktor Some, denn dieser gibt ja einen (Erfolgs-)Wert zurück. Hiermit ist auch geklärt, was Computation Expressions eigentlich sind: Monadische Berechnungen Jetzt geht es nur noch darum, so eine auch zu definieren. Um anzugeben, woher die entsprechenden Funktion (Bind etc.) genommen werden sollen, sieht F# ein sog. Builder-Objekt vor, für das wir eine Klasse schreiben: type MaybeBuilder() = member this.Delay(f) = f() member this.Bind(x, f) = bind x f member this.Return(x) = Some(x) Wie wir sehen, definiert das genau die obigen Funktionen – Bind und Return (und Delay, das werden wir aber vernachlässigen). Von diesem können wir jetzt eine Instanz erstellen let maybe = new MaybeBuilder() und schon sehen die Dinge so aus: let summe() = maybe { let! a = zahlEingeben "a" let! b = zahlEingeben "b" let! c = zahlEingeben "c" return a + b + c } Perfekt – Einfacher geht’s nicht! Und so einen Ausdruck nennt man jetzt eben Computation Expression. Die Aufrufe von Bind werden nun vom Compiler automatisch eingesetzt, und zwar immer an den Zuweisungen mit let!. Weitere Computation Expressions Aufmerksamen Lesern wird die Funktion, die wir oben geschrieben haben, merkwürdig bekannt vorkommen. Richtig ... es existiert doch so eine Syntax mit seq { ... }. Seq ist tatsächlich der wohl berühmteste Vertreter von Computation Expressions – Er arbeitet nicht mit Option-Werten, sondern mit Listen. Aber nichtsdestotrotz handelt es sich um nichts Anderes als oben, der Zweck ist nur ein anderer. Hier sieht man die außerordentliche Vielfalt von monadischen Berechnungen. F# erlaubt uns nämlich neben der Definition von Bind und Return auch noch, Implementierungen für Kontrollstrukturen zu schreiben – Das bedeutet, innerhalb dieser Blöcke kann man den gesamten F#-Code schrittweise auseinandernehmen und mit „Spezialeffekten aufgerüstet“ wieder ins Rennen schicken. Beweis: Für List-Comprehensions kann ebenso simpel eine Computation Expression definiert werden: type ListComprehension() = member this.Delay(f) = f() member this.Return(x) = [x] member this.Bind(liste, f) = List.concat [ for x in liste -> f x ] let list = new ListComprehension() let punkte let! x let! y return } = list { = [1..3] = [1..3] (x, y) printfn "%A" punkte // [(1, 1); (1, 2); (1, 3); (2, 1); (2, 2); (2, 3); (3, 1); (3, 2); (3, 3)] Aber genug theoretisch geredet – Folgende Liste gibt eine Reihe von Operationen an, die monadischen Gesetzen folgen und daher durch Computation Expressions beschreibbar sind: 1. Option-Werte 2. List-Comprehensions 3. Sichere Ein- und Ausgaben 4. Asynchrone Programmabläufe (Multithreading) 5. Transaktionen 6. Continuations (Einfrieren und Speichern von Zeitpunkten in Programmen) 7. Backtracking (Fehlerrückverfolgung) 8. Zufallsgeneratoren 9. Zustandsänderungen / State machines 10. Stochastische Prozesse (Zufallsmodelle) 11. Parser (Texte von Programmen verarbeiten lassen) 12. Interpreter für Programmiersprachen Mit Computation Expressions lässt sich also auf einer Vielzahl von Objekten operieren. Dabei handelt es sich immer um generische Typen ('a option, 'a list, 'a async, ...). Verallgemeinert nennen wir diesen Typen 'a M und erhalten folgende Signaturen für ein Builder-Objekt. val Bind : 'a M * ('a -> 'b M) -> 'b M val Return : 'a -> 'a M Zufallsgeneratoren Zufallsgeneratoren und funktionale Programmierung hatten schon immer eine schwierige Beziehung. Zumeist werden sie, zusammen mit Ein/Ausgaben, von Anfängern oder Gegnern als das Argument gezückt, aufgrund dessen funktionale Programmierung nicht funktionieren könne. Um das spektakuläre Gegenteil zu beweisen, v.A. aber, um einmal die Entwicklung einer Computation Expression vollständig durchzuexerzieren, werden wir nun schrittweise eine vollständig funktionale Zufalls-Monade entwickeln. Dazu ist ersteinmal wichtig, die Funktionsweise handelsüblicher Zufallsgeneratoren zu verstehen. Normalerweise hält ein jeder Zufallsgenerator einen bestimmten gespeicherten Wert und eine Rechenvorschrift. Ganz am Anfang seiner Existenz wird er mit einem sog. Seed, einem Anfangswert, initialisiert. Bei jeder Anfrage für eine Zufallszahl berechnet er nun aus diesem Seed mithilfe einer hochkomplexen Rechenvorschrift eine Zufallszahl aus. Gleichzeitig kommt er damit auf einen neuen Seed-Wert, der ihn bei der nächsten Berechnung als Ausgangswert dient. (Übrigens: Bei gleichen Startwerten produziert jeder Zufallsgenerator auch die gleiche Zufallsfolge!) In Ordnung – Das klingt jetzt, von der komplizierten Formel abgesehen, nach nichts, was nicht in ein paar Zeilen machbar ist: type Random(seed : int) = let mutable value = seed member this.Next(min, max) = value <- komplizierteFormel value min + value % (max - min) Tjaaaha, freut sich unser Widersacher und das schlechte Gewissen sieht, was er meint (hier zur unmissverständlichen Kenntnis rot hervorgehomben). type Random(seed : int) = let mutable value = seed member this.Next(min, max) = value <- komplizierteFormel value min + value % (max - min) Veränderliche Werte – Schlimmer: Ein Globaler Zustand, der sich permantent ändert. Nein, noch schlimmer: Jede Funktion, die den Zufallsgenerator verwendet, ändert einen globalen Zustand. Wenn jetzt noch Multithreading mit ins Spiel käme, hätten wir ein ernstes Problem. In anderen Sprachen ist sowas zwar Gang und Gäbe (ja, auch der System.Random arbeitet so), aber da wir in F# funktional programmieren und genau soetwas vermeiden wollen, muss einfach eine andere Lösung her. Und die gibt es – Völlig frei von Veränderlichen und mit einer perfekten, durchs Typsystem getragenen Unterscheidung von zufälligem und „normalem“ Code. Aber wo kommt diese her? Gewieftermaßen reicht hier die Kapitelüberschrift zur Antwort: Computation Expressions Betrachten wir mal den Zufallsgenerator von allen Zustanden gelöst – Was wollen wir haben? Ein Objekt, das wenn es einen Anfangswert bekommt, einen Zufallswert und einen neuen Anfangswert produziert! Das zumindest ist problemlos funktional: type 'a Random = Random of (int -> 'a * int) Logisch – Ein Objekt, das einen Startwert (eine Zahl) bekommt und ein zufälliges Ergebnis plus den nächsten Startwert produziert ... So weit waren wir. Ein Zufallsobjekt, das aus einem angegebenen Intervall einen Zufallswert erstellt, können wir auch angeben: let choose (min, max) = Random (fun seed -> (min + seed % (max - min) (* Zufallswert *), abs(327 * seed - 403471) % 37231) (* Neuer Startwert *) ) (Die Formeln sind geraten und natürlich nicht auf dem neusten Stand der Dinge – Es geht mehr ums Prinzip). Um das Schreiben noch etwas einfacher Generatorobjekt noch eine Memberfunktion: zu gestalten, erhält unser type 'a Random = Random of (int -> 'a * int) with member this.Gen seed = let (Random f) = this in fst (f seed) Mittels der Gen-Funktion können wir bei gegebenem Anfangswert nun einfach das Ergebnis ausgeben lassen. Jetzt können wir beginnen, Zufallsfolgen zu berechnen. let generator = choose (1, 10) printfn "%i" (generator.Gen 42) printfn "%i" (generator.Gen 42) printfn "%i" (generator.Gen 42) Wundervoll – Wir erhalten drei mal die Ausgabe 7. Sehr zufällig ist das aber nicht – aber bekanntlich ändern sich ja auch keine Werte. Aber halt, wir wissen ja, dass der Generator noch einen neuen Startwert ausspuckt. Den können wir ja einfach weitergeben. let (Random generator) = choose (1, 10) let startwert1 = 42 let (ergebnis1, startwert2) = generator startwert1 printfn "%i" ergebnis1 let (ergebnis2, startwert3) = generator startwert2 printfn "%i" ergebnis2 let (ergebnis3, startwert4) = generator startwert3 printfn "%i" ergebnis3 7, 4, 5 – Das lass ich glatt als Zufallsfolge gelten! Jetzt geht es einzig darum, das Weitergeben der Startwerte irgendwie vor dem Programmierer zu verbergen. Und genau hier kommt wieder die Bind-Funktion ins Spiel. Der neue Zufallsgenerator setzt einen gegebenen Startwert in den ersten ein, verarbeitet dessen Ergebnis und macht mit dem neu gelieferten Wert weiter. let bind (Random gen) (next : 'a -> 'b Random) = Random (fun seed -> let (res, seed2) = gen seed let (Random gen2) = next res gen2 seed2) Damit wird aus unserem Beispiel Folgendes: let generator = choose (1, 10) let startwert1 = 42 (bind generator (fun ergebnis1 -> printfn "%i" ergebnis1 bind generator (fun ergebnis2 -> printfn "%i" ergebnis2 bind generator (fun ergebnis3 -> printfn "%i" ergebnis3 Random (fun x -> ((), x)))))).Gen startwert1 Super – Zumindest müssen wir keine Werte mehr per Hand herumtragen, bind hält sie schön von uns fern. Damit wir die längst überfällige Computation Expression endlich definieren können, fehlt nur noch eine sinnvolle Implementierung von Return. Sprich: Ein Generator, der einen Wert x zurückgeben soll, tut dies für jede Eingabe! Damit lautet das Builder-Objekt so: type RandomBuilder() = member this.Delay(f) = f() member this.Return(x) = Random (fun seed -> (x, seed)) member this.Bind(Random gen, next) = Random (fun seed -> let (res, seed2) = gen seed let (Random gen2) = next res gen2 seed2) Hiermit ist es uns also möglich, zufallsabhängigen Code völlig imperativ aussehend zu schreiben. Dennoch kann dieser außerhalb eines RandomObjektes nicht existieren und ist daher perfekt von jeglichem nicht-zufälligen Code getrennt – Seiteneffekte und Wertänderungen haben wir an keiner Stelle! Das war's. let random = new RandomBuilder() let demo = random { let! a = choose (1, 10) printfn "%i" a let! b = choose (1, 10) printfn "%i" b let! c = choose (1, 10) printfn "%i" c return () } demo.Gen 42 Dabei gilt: val demo : unit Random Hier noch ein Beispiel, das verschiedenste Generatoren kombiniert: let zufallsPunkt = random { let! x = choose (-5, 5) let! y = choose (-10, 10) return (x, y) } let rec zufallsSequenz bereich = function | 0 -> random { return [] } | k -> random { let! wert = choose bereich let! rest = zufallsSequenz bereich (k - 1) return wert::rest } let zufallsWort = let buchstaben = [|'A' .. 'z'|] fun länge -> random { let! ascii = zufallsSequenz (0, buchstaben.Length) länge return new String(ascii |> List.map (fun i -> buchstaben.[i]) |> List.to_array) } let zufallsDemo = random { let! länge = choose (5, 10) let! liste = zufallsSequenz (1, 100) länge printfn "%A" liste let! punkt = zufallsPunkt printfn "%A" punkt printf "Länge des Namens? " let len = int (Console.ReadLine()) let! name = zufallsWort len printfn "Hallo, %s" name return () } zufallsDemo.Gen 42 printfn " ************************* " zufallsDemo.Gen 1 Asynchrones Programmieren Aber genug über die Interna von Computation Expressions. Dieser Abschnitt dreht sich um ein kleines Objekt, das F# von Haus aus definiert und mit wir in der absolut einzigartigen Lage sind, ein sehr kompliziertes Thema über eine Computation Expression abhandeln zu können, ohne auch nur darüber nachdenken zu müssen: Async Betrachten wir folgende Beispiel: Wir haben eine Berechnung, die potenziell seehr lange dauert: // val langsameBerechnung : 'a -> 'a let langsameBerechnung wert = for x in 1..10 do printfn "#%A: %i" wert x Threading.Thread.Sleep 200 // 200 ms schlafen wert Wenn wir hiervon ein paar Werte brauchen, ist das kein Problem: let ergebnisse = [ for x in 1..5 -> langsameBerechnung x ] let summe = List.sum ergebnisse Als Ausgabe bekommen wir fein säuberlich hintereinander: #1: #1: . . #1: #2: #2: . . #2: #3: . . #5: 1 2 10 1 2 10 1 10 Insgesamt müssen wir auf dieses Ergebnis 200ms * 10 * 5 = 10 Sekunden warten. Die berechtigte Überlegung ist nun: Wieso berechnen wir die Werte nicht parallel? Keiner von ihnen ändert einen anderen, es sind unabhängige Berechnungen. Unsere Funktion parallelisierbar zu machen, ist dank Computation Expressions im Schlag möglich: let langsameBerechnung wert = async { for x in 1..10 do printfn "#%A: %i" wert x Threading.Thread.Sleep 200 // 200 ms schlafen return wert } Ihre Signatur ändert sich in: val langsameBerechnung : 'a -> Async<'a> Das bedeutet, sie gibt ein Async-Objekt zurück, das, wenn es ausgewertet wird, einen Wert vom Typen 'a produziert. Der Trick ist nun, dass wir mit einer weiteren Computation Expression diese Berechnungen bequem kombinieren können. let summe = async { let! ergebnisse = [ for x in 1..5 -> langsameBerechnung x ] |> Async.Parallel return Array.sum ergebnisse } Mit Async.Parallel setzen wir eine Liste von asynchronen Berechnungen zu einer neuen zusammen, die die Ergebnisse als Array zusammenfasst. val Async.Parallel : Async<'a> list -> Async<'a array> Dieses Ergebnis können wir mit der let!-Syntax extrahieren, summieren zu zurückgeben. Dabei hat Summe den Typen Async<int>, gibt also eine Berechnung an, die, wenn sie ausgewertet wird, eine Zahl zurückgibt. Und das tun wir jetzt: printfn "%i" (Async.RunSynchronously summe) Was wir erhalten, sieht nun in etwa so aus: Dieses Ergebnis zeigt: Die Funktionen werden tatsächlich parallel, d.h. von mehreren Threads, ausgewertet. Und viel wichtiger: Die Rechenzeit ist weit unter 10 Sekunden, da die Funktionen nicht mehr auf einander warten. Die Möglichkeit zur parallelen Auswertung gibt es bereits in vielen Programmiersprachen – Der entscheidende Vorteil unserer Async-Ausdrücke ist es allerdings, dass wir jede Art von potenziell asynchroner Berechnung in fast normaler Syntax formulieren können – Seien es asynchrone Internet-, Dateioder Datenbankzugriffe, Message Passing oder nur eine parallele Auswertung. Mit Thread-Handles oder Callback-Funktionen wie unter VB oder C# müssen wir uns dabei an keiner Stelle herumschlagen – Im let!-Aufruf werden diese vom Compiler automatisch hinzugefügt. In Kombination mit seiner funktionalen Natur macht dies F# zu einer idealen Multithreading-Sprache. Codes, die in anderen Sprachen Hunderte von Zeilen benötigen und dabei oft fehleranfällig sind, lassen sich mit async {} - Blöcken in wenigen Zeilen, einer völlig offensichtlichen Syntax und mit weitaus geringerem Gefahrenpotenzial notieren. Denn ein weiterer Vorteil ist, dass F# (unbeabsichtigte) Veränderung von Werten unterbindet – An dieser Stelle nämlich krankt viel sonstiger Multithreading-Code, da unkontrolliert auf gemeinsame Werte zugegriffen wird und deren Zustand dadurch Schäden erleidet. Thread-unterbrechungspunkte / callback .NET-Interaktion Schlusswort und Ausblick Beispiele! Raytracer, Mini-Sprache Quellen und Links Wikibooks F# <mehr>