F# lernen Kata für Kata Ralf Westphal Dieses Buch können Sie hier kaufen http://leanpub.com/Fsharp_lernen_Kata_fuer_Kata Diese Version wurde auf 2014-01-20 veröffentlicht Das ist ein Leanpub-Buch. Leanpub bietet Autoren und Verlagen mit Hilfe des Lean-Publishing-Prozesses ganz neue Möglichkeiten des Publizierens. Lean Publishing bedeutet die permanente, iterative Veröffentlichung neuer Beta-Versionen eines E-Books unter der Zuhilfenahme schlanker Werkzeuge. Das Feedback der Erstleser hilft dem Autor bei der Finalisierung und der anschließenden Vermarktung des Buches. Lean Publishing unterstützt de Autor darin ein Buch zu schreiben, das auch gelesen wird. ©2013 - 2014 Ralf Westphal Ebenfalls von Ralf Westphal Messaging as a Programming Model Inhaltsverzeichnis Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Ansatz “Kata für Kata” . . . . . . . . . . . . . . . . . 1 3 Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Hello, World! - F#-Code zum Laufen bringen F# auf der Kommandozeile . . . . . . . . . . F# interaktiv im Terminalfenster . . . . . . . F# interaktiv in der IDE . . . . . . . . . . . . F# interaktiv im Web . . . . . . . . . . . . . F# übersetzen in der IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 8 11 13 14 Kata “FizzBuzz” . . . . . . . . . . . . . . . . . Literale als Ausdrücke . . . . . . . . . . . . Literale an Namen binden . . . . . . . . . . Scope bilden durch Einrückungen . . . . . . Nur der letzte Wert zählt . . . . . . . . . . . Bindungen sind keine Funktionsdefinitionen Zwischenstand . . . . . . . . . . . . . . . . Funktionen als Werte . . . . . . . . . . . . . Bindungen benutzen zur Funktionsdefinition Zwischenstand . . . . . . . . . . . . . . . . Fallunterscheidung mit if . . . . . . . . . . . Zwischenstand . . . . . . . . . . . . . . . . Listen erzeugen . . . . . . . . . . . . . . . . Aufzählen mit einer for-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 17 19 21 22 24 26 27 29 31 33 37 37 39 INHALTSVERZEICHNIS Eine Liste von innen her aufbauen . . . . . . . . . . . . . Reflexion . . . . . . . . . . . . . . . . . . . . . . . . . . Kata “Tannenbaum” . . Analyse . . . . . . . Zeichenketten bauen Rekursives Geäst . . Reflexion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 45 46 48 50 Kata “ToDictionary” . . . . . . . . . . . . . . Analyse . . . . . . . . . . . . . . . . . . . Name-Wert-Paare in einer Map verwalten Zeichenketten zerlegen . . . . . . . . . . . Explizite Typangabe . . . . . . . . . . . . Transformieren mit map . . . . . . . . . . Tupel als Rückgabewerte . . . . . . . . . . Datenflüsse herstellen . . . . . . . . . . . Reflexion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 52 53 54 56 57 59 61 63 usw. . . . . . . . . . . . . . . . . . . . Kata “Römische Zahlen I” . . . . . Kata “Römische Zahlen II” . . . . . Kata “Zustandsautomat” . . . . . . Reflexion des Lernfortschritts . . . Zwischenspiel: Automatisiert testen Kata “Ringbuffer” . . . . . . . . . . Kata “Verzeichnisstatistik” . . . . . Kata “Benutzeranmeldung” . . . . . Kata “CSV Viewer” . . . . . . . . . Kata “Leiterspiel” . . . . . . . . . . Kata “Taschenrechner” . . . . . . . Tiefer… . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 66 66 66 66 66 66 66 66 66 66 66 66 Release Notes . . . . . . . . . . . . . . . . . . . . . . . . . 68 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 44 . . . . . . . . . . . . . . . . . . . . . . . . . . Einleitung Die Zukunft gehört den polyglotten Projekten. Ja, das glaube ich. Anders wird es schwer sein, technologisch Schritt zu halten. Anders wird es schwer sein, auf Dauer neue Teammitglieder zu finden. Anders wird es schwer sein, Software für hohe Wandelbarkeit zu strukturieren. Aber wann ist Zeit, sich in eine weitere Programmiersprache einzuarbeiten? Enthusiasten tun das nebenbei und ihnen sind die Quellen für Lernstoff nahezu egal. Für normale Entwickler mit wenig Hang zur Opferung der Freizeit darf es jedoch gern einen effizienteren Weg geben. Und wie sieht das aus, effizientes Sprachenlernen? Man nimmt sich eine empfohlene Sprachreferenz und versucht deren mehr oder weniger systematische und mehr oder weniger vollständige Darstellung in das Chaos des Alltags zu übertragen. Das hat lange Tradition. So habe ich auch angefangen damals mit Pascal. So wird es in der Erstausbildung gelehrt. Bei meinen Versuchen in der letzten Zeit jedoch, mir Ruby oder JavaScript oder F# drauf zu schaffen, hatte ich damit allerdings schnell meine Frustgrenze erreicht. Die Geschwindigkeit der Bücher war mir zu gering. Die Systematik war für mich mehr Rauschen denn Nutzen. Ich habe mich an früheren Unterricht in natürlichen Sprachen erinnert gefühlt. Der ging auch systematisch vor: Grammatik Schritt für Schritt, Vokabeln Schritt für Schritt. Inzwischen hat sich der Unterricht natürlicher Sprachen jedoch gewandelt. Heute unterrichtet man viel praxisorientierter. Sprachmittel werden idiomatischer eingeführt. Weniger Erklärungen, weniger Theorie, mehr “Das ist halt grad so.” Man will die Schüler Einleitung 2 schneller in eine angstfreie flüssige Anwendung bringen. Die mag fehlerhaft sein, aber sie findet halt überhaupt statt. Damit ist dann die Voraussetzung für Feedback und weiteres Lernen geschaffen. Nun denke ich mir: Warum nicht auch so formale Sprachen lernen? Könnte das vielleicht praxistauglicher für ohnehin überlastete Softwareentwickler sein? Weniger Konzepte, mehr Muster. Schneller ran an nicht trivialen Code. Sprachkonstrukte gleichzeitig im Zusammenhang zeigen, statt sie isoliert nacheinander gemäß einer Systematik vorstellen. Das möchte ich mit dieser Einführung in F# ausprobieren. Das ist doppelt spannend, weil ich F# noch nicht beherrsche. Ich bin C#-Entwickler und bisher nur F# interessiert. Wenn Sie dieser Einführung folgen, dann folgen Sie mir also auf meinem Weg des Selbststudiums. Gewiss können Sie dabei nur dessen sein, dass die Codebeispiele¹ tun, was sie tun sollen. Es sind pragmatische Lösungen - die in den Augen von F#-Experten nicht immer Gnade finde mögen. Aber so ist das eben, wenn man eine Sprache lernt. Am Anfang holpert es. Mit der Zeit wird es besser. Insofern kann es auch sein, dass ich zunächst ein Sprachfeature auf die eine Weise benutze und später anders. Oder meine Lösung sieht am Anfang so aus und am Ende würde ich sie anders schreiben, weil ich mehr über F# weiß. Für mich ist dieses Büchlein ein Experiment. Schaffe ich es, mich mit diesem Ansatz in F# genügend einzuarbeiten, um es produktiv im Alltag einzusetzen? Funktioniert der Ansatz auch für Sie als Leser? Wie konsequent bleibe ich am Ball? Naja, wir werden sehen. ¹Alle Codebeispiele im Text gehören zu Projekten im Github Repository https://github.com/ralfw/fsharpkatabykata Einleitung 3 Der Ansatz “Kata für Kata” Und wie soll das gehen mit der praxisorientierteren Sprachvermittlung? Ich habe mir überlegt, dass ich mich in F# anhand von Code Katas einarbeiten will. Nicht irgendeine sprachbezogene Systematik soll mich leiten, sondern der Bedarf an Sprachmitteln, um Katas lösen zu können. Natürlich besteht dabei die Gefahr, dass ich mich übernehme. Deshalb systematisiere ich die Katas ein wenig. Ich nehme sie mir in einer gewissen Reihenfolge vor. Kata für Kata soll der Lösungsumfang steigen. Kata für Kata sollen die Anforderungen an Sprache und Plattform steigen. Ersteres kann ich relativ leicht kontrollieren. Die Katas im Coding Dojo der Clean Code Developer School² sind schon grob nach Umfang gegliedert. Ich fange also mit Function Katas an und arbeite mich dann weiter vor bis zu Application Katas. Letzteres jedoch ist für mich als F#-Laie schwieriger zu beurteilen. Welche F#-Features könnten bei Lösung von Kata Y nützlich sein, die Kata X noch nicht motiviert hat? Hier bin ich darauf angewiesen, dass es eine gewisse Korrelation zwischen Aufgabenumfang und Ausreizungsgrad der Sprache gibt. Je größer die Aufgabe, desto mehr Features der Sprache können sinnvoll zum Einsatz kommen. Es besteht zwar eine Gefahr, dass ich die Sprache mit einem gewissen Tunnelblick betrachte, wenn ich mir das Material zur Lösung einer Kata aus der Literatur zusammenklaube. Aber das nehme ich mal in Kauf. Pragmatism rulez! Zur Verfeinerung gibt es immer noch Gelegenheit. Zunächst möchte ich mich in die Lage versetzen, überhaupt anspruchsvollere Aufgaben mit F# bewältigen zu können - selbst wenn ich dabei F# nur “gebrochen sprechen” sollte. Vielleicht kommt es ja zu einer Diskusson über das Büchlein. Dann ²http://ccd-school.de/coding-dojo/ Einleitung 4 sind F#-Experten herzlich eingeladen, idiomatischere Lösungen vorzuschlagen. Die haben dann auch mehr Wert, würde ich sagen, weil sie nicht einfach so materialisieren, sondern im Kontrast zu etwas für Neulinge besser Verständlichem stehen. Nun genug der Vorrede. Auf geht´s in die F# Lernpraxis. Installation Bevor Sie sich in F#-Features vertiefen können, müssen Sie natürlich F# installieren. F# ist immerhin für eine ganze Reihe von Betriebssystemen verfügbar. Wie die Installation vonstatten geht, möchte ich hier allerdings nicht beschreiben. Dazu finden Sie an anderer Stelle ausführliche Informationen, z.B. • fsharp.org³, s. “Getting F#” • F# for fun and profit⁴ Soviel sei jedoch verraten: Wenn Sie mit Visual Studio arbeiten, dann haben Sie es am einfachsten. F# wird standardmäßig mit Visual Studio ausgeliefert. ³http://fsharp.org/ ⁴http://fsharpforfunandprofit.com/installing-and-using/#installing-fsharp Hello, World! - F#-Code zum Laufen bringen F# durch die Bearbeitung von Code Katas zu lernen, setzt voraus, dass Sie überhaupt F# Code zum Laufen bringen können. Die Installation ist eine Sache, dann aber auch Code “auf Knopfdruck” auszuführen, eine andere. Immerhin gibt es dahin mehrere Wege. Die trage ich Ihnen kurz einmal vor, bevor wir uns anschließend in kleinen Schritten F# annähern. Soweit habe ich F# schon erkundet. F# auf der Kommandozeile Wenn Sie Purist sind, dann wollen Sie F#-Code mit einem Editor Ihrer Wahl schreiben können. Sie wollen nicht auf eine IDE wie Visual Studio angewiesen sein. Dazu müssen Sie zweierlei wissen: • Wie sieht minimaler F#-Code überhaupt aus? Code für ein “Hello, World!” reicht zunächst aus. • Wie übersetzen Sie den Code? Beide Fragen sind leicht zu beantworten. Minimaler F#-Code besteht aus einer Zeile für die Ausgabe des Begrüßungstextes auf Standard-Output. Legen Sie einfach eine Textdatei wie folgt an: hello_world.fs: Hello, World! - F#-Code zum Laufen bringen 1 7 printfn "Hello, World!" printfn ist eine Standardfunktion von F#. Sie entspricht System.Console.WriteLine() der BCL des .NET Framework. Wenn Sie mögen, können Sie die auch direkt aufrufen: hello_world_v2.fs: 1 System.Console.WriteLine "Hello, World!" F# als Sprache ist unabhängig vom .NET Framework. Deshalb gibt es für grundlegende Kommunikation mit der Umwelt auch F#eigene Funktionen. Letztlich basieren die jedoch immer auf einer Laufzeitumgebung wie .NET Framework oder Mono. Aus F# heraus ist die Nutzung deren Bibliotheken also leicht möglich. Zum Teil stehen Sie ohne weiteren Aufwand automatisch zur Verfügung wie hier die Nutzung der Klasse System.Console demonstriert. Beachten Sie jedoch: Anders als in vielen Sprachen werden die aktuellen Parameter in F# beim Aufruf einer Funktion nicht in Klammern eingeschlossen. Das gilt für F#-eigene Funktionen wie für importierte Funktionen aus Bibliotheken. Ebenso fehlt eine Kennzeichnung des Anweisungsendes. Es ist kein Semikolon zu setzen. Das sind erste Hinweise darauf, dass sich F# bemüht, eine knappe, klare Sprache zu sein, die das Wesentliche nicht hinter notationellem Rauschen verbirgt. Jetzt aber zur Übersetzung. Öffnen Sie ein Terminalfenster, wechseln Sie in das Verzeichnis der Codedatei und übersetzen Sie sie wie folgt:⁵ fsc hello_world.fs ⁵Wenn ich öfter mit einem Terminalfenster in einem Verzeichnis arbeiten muss, weise ich dem einen Laufwerksbuchstaben zu (hier: F:). Das geht mit dem SUBST -Kommando oder visueller mit einem Werkzeug wie Visual Subst. Hello, World! - F#-Code zum Laufen bringen 8 Der F#-Compiler erzeugt daraufhin eine ausführbare Datei hello_world.exe: F# auf der Kommandozeile übersetzen Beachten Sie jedoch: Der F#-Compiler muss über PATH (oder eine vergleichbare Umgebungsvariable bei anderen Betriebssystemen als Windows) zu finden sein, wenn Sie es so einfach haben wollen.⁶ F# interaktiv im Terminalfenster Mit einem Texteditor und dem Kommandozeilencompiler zu arbeiten, mag der traditionelle Weg sein. Der einfachste ist er jedoch nicht. Denn F# kann auch ganz ohne diese Werkzeuge interaktiv in einer REPL-Umgebung (Read-Eval-Print-Loop) genutzt werden. Dazu rufen Sie in einem Terminalfenster fsi.exe auf.⁷ ⁶Wenn Sie unter Windows mit Visual Studio arbeiten, können Sie im ProgrammMenü bei “Visual Studio” das “Developer Command Prompt for Visual Studio” aufrufen. In dem Terminalfenster sind Pfade zum F#-Compiler und anderen Werkzeugen wie MSBuild automatisch gesetzt. ⁷Auch dieses Werkzeug muss natürlich im PATH zu finden sein. Nutzen Sie der Einfachheit halber wieder das “Developer Command Prompt for Visual Studio”. Hello, World! - F#-Code zum Laufen bringen 9 Die REPL-Umgebung bietet Ihnen wieder eine Art Kommandozeile, hinter deren Prompt Sie F#-Anweisungen eingeben können. Diese Anweisungen können mehrzeilig sein. Ausgeführt werden sie erst, wenn Sie sie mit “;;” abschließen. F# in der REPL-Umgebung interaktiv ausführen Hier sehen Sie den Unterschied, wenn Sie die Ausgabe von “Hello, World!” mit zwei printfn-Aufrufen durchführen. Beim ersten Mal wird jede Anweisung mit “;;” abgeschlossen und sofort ausgeführt; beim zweiten Mal werden sie zusammen ausgeführt, da der Abschluss mit “;;” erst nach dem zweiten erfolgt. Hello, World! - F#-Code zum Laufen bringen Mehrere Anweisungen zusammen ausführen durch Abschluss mit “;;” 10 Hello, World! - F#-Code zum Laufen bringen 11 Um die REPL-Umgebung zu verlassen, geben Sie #quit;; ein. F# interaktiv in der IDE Soweit F# für Puristen. Es geht aber auch moderner. F# in der IDE kann genauso einfach sein. Eine Datei mit dem Typ .fs (auch außerhalb eines Projektes) genügt. Wenn Sie in einer F#-Datei das Kontextmenü aufrufen, bietet Ihnen Visual Studio an, die aktuelle Zeile in einer REPL-Umgebung auszuführen: F# zeilenweise in Visual Studio ausführen Oder Sie markieren einen ganzen Quellcodebereich zur interaktiven Ausführung: Hello, World! - F#-Code zum Laufen bringen 12 F# blockweise in Visual Studio ausführen Oder Sie öffnen die REPL-Umgebung über View|Other Windows|F# Interactive, um darin Anweisungen einzugeben wie im Terminalfenster unter fsi.exe: Hello, World! - F#-Code zum Laufen bringen 13 F# interaktiv in Visual Studio ausführen Denken Sie dann allerdings daran, Anweisungsblöcke mit “;;” abzuschließen, um sie auszuführen. F# interaktiv im Web Noch moderner und vor allem ohne Installationsaufwand ist es jedoch, F# im Web bei Try F#⁸ interaktiv auszuprobieren: ⁸http://www.tryfsharp.org/Create Hello, World! - F#-Code zum Laufen bringen 14 F# in einer REPL-Umgebung im Browser Der Run-Button enthebt Sie sogar der Notwendigkeit, Anweisungen mit “;;” abzuschließen. Einzige Voraussetzung: der Browser muss Silverlight unterstützen. Das ist auf iOS-Geräten leider nicht der Fall. Derzeit können Sie also nicht mit F# auf dem iPad spielen. F# übersetzen in der IDE Zu guter Letzt können Sie in einer IDE natürlich auch ein F#Projekt anlegen. Visual Studio bietet Ihnen z.B. “F# Application” (Konsolenanwendung) und “F# Library” (DLL). Für einfache sofortige Ausführbahrkeit wählen Sie “F# Application”, tragen Ihren Code in Program.fs ein und starten ihn mit Ctrl-F5: Hello, World! - F#-Code zum Laufen bringen 15 Eine F#-Konsolenanwendung in Visual Studio übersetzt und gestartet That´s it. Sie können weder das Setzen von PATH noch den Anweisungsabschluss mit “;;” vergessen. Ich denke, für einen Start in die Erkundung von F# sind das genügend Optionen, Ihren Code zum Laufen zu bringen. In den REPL-Umgebungen können Sie experimentieren. Und mit .fs-Dateien können Sie interaktiv “erspielte” Lösungsansätze zusammenfassen - die Sie in der IDE oder auf der Kommandozeile übersetzen. Also auf zur ersten Kata… Kata “FizzBuzz” Jetzt sind wir auf demselben Stand. Sie können F#-Code grundsätzlich ausführen. “Hello, World!” ist der traditionelle Einstieg in eine Programmiersprache. Dieser Tradition habe ich gern entsprochen. Mit der “Belehrung” hat es nun allerdings ein Ende. Mehr weiß ich auch nicht über F#. Von nun an sind wir auf Augenhöhe. Sie haben keine Ahnung, ich auch nicht. Stellen wir uns größeren Herausforderungen gemeinsam. Also ran an die Katas. Ich wähle mal die Kata “FizzBuzz”⁹ aus dem Coding Dojo der Clean Code Developer School¹⁰, weil diese Aufgabe auch quasi den Status eines “Hello, World!” hat - allerdings für den Einstieg in das Test-Driven Development (TDD). Hier geht es zwar nicht um TDD, aber doch darum, mehr über das Programmieren zu lernen. Also warum nicht FizzBuzz? Die Aufgabe ist simpel und trotzdem für unseren Stand der F#-Kenntnis herausfordernd, denke ich: Schreibe eine Funktion, die die Zahlen von 1 bis 100 zurückgibt. Manche Zahlen sollen dabei allerdings übersetzt werden: • Für Vielfache von 3 gibt „Fizz“ aus. • Für Vielfache von 5 gib „Buzz“ aus. • Für Vielfache von 3 und 5 gib „FizzBuzz“ aus. Beispiel: 1, 2, Fizz, 4, Buzz, Fizz, 7 … 14, FizzBuz, 16… ⁹http://de.scribd.com/doc/140817312/Function-Kata-FizzBuzz ¹⁰http://ccd-school.de 17 Kata “FizzBuzz” In C# könnte ich eine Lösung sofort herunterschreiben; und sie können das in Ihrer “Muttersprache” auch. Bei F# jedoch stolpere zumindest ich schon über den Begriff Funktion. Wie schreibt, wie definiert man eine Funktion? Literale als Ausdrücke Funktionen sollen in einer Sprache der Funktionalen Programmierung (FP) ja first class citizens, also gleichauf mit Zeichenketten oder Zahlen sein. Mit solchen Literalen lässt sich sogar ein gültiges F#-Programm schreiben: Literale als “Funktionen” Versuchen Sie das mal mit C# oder Java :-) Bei F# jedoch wird ein “einsames Literal” als Ergebnis eines Ausdrucks einfach zu einem Wert mit dem Namen it. Deshalb steht in der Ausgabe: Kata “FizzBuzz” 1 18 val it : string = "hello, world!" In der REPL-Umgebung im Web kann man darauf nicht Bezug nehmen - aber bei fsi.exe geht es:¹¹ Seltsam. Oder? Im Terminalfenster ist dann auch zu sehen, was passiert, wenn mehrere Literale einzeln “ausgeführt” werden: ¹¹printfn mit zwei Parametern zu nutzen, ist keine Kunst. Auch das “%d” liegt ja nahe. Aber it + 1 in Klammern einzuschließen, hat mich einen Moment gekostet herauszufinden. Die Parameter sind bei F# ja nicht durch Komma getrennt. Deshalb muss über die Klammern angezeigt werden, dass it + 1 zusammengehört. Kata “FizzBuzz” 19 Nicht nur verändern sich die Daten von it, it wechselt dabei auch den Typ! Zuerst hat it den Typ int, dann den Typ string. Das weißt auf die Natur von it hin. it ist keine Variable mit einem fixen Datentyp im Sinne von C# oder Java. it ist vielmehr “nur” ein Name. Und dieser Name wird an einen Wert gebunden. Und der Wert ist eine Kombination aus Daten und Typ, z.B. (42, int).¹² Literale an Namen binden Eine solche Bindung können Sie auch explizit vornehmen: ¹²Woher weiß F# eigentlich den Typ? F# ist eine genauso streng typisierte Sprache wie C#. Aber F# gibt sich alle Mühe, Datentypen selbst zu bestimmen. Das nennt man Typ-Inferenz. Damit soll dem Code-Autor Mühe und dem Code-Leser Rauschen erspart werden. 20 Kata “FizzBuzz” Einen Wert an einen Namen binden mit let. let assoziiert den Wert (42, int) mit dem Namen myname. Oder genauer: Es wird der letzte Werte einer Reihe von Ausdrücken damit assoziiert: Kata “FizzBuzz” 21 Sie sehen, am Ende steht jetzt 1 val myname : string = "hello, world!" weil der Wert das “Ergebnis” der Anweisungsfolge in der letBindung ist. So ganz einfach ist das jedoch nicht. Erstens taucht da eine Warnung auf, zweitens sind die beiden Literale eingerückt. Scope bilden durch Einrückungen Die Einrückungen dienen der Scope-Definition. In C# oder Java gibt es dafür {}-Blöcke. In Python und F# erreicht man das selbe mit Whitespace, also durch Einrückung. Kata “FizzBuzz” 22 Zuerst hatte ich es ohne Einrückung probiert und diesen Fehler gemeldet bekommen: Mit zwei Leerzeichen vor der 42 hat es dann funktioniert. 1 2 let myname = 42 Scope über Einrückungen zu definieren, mag etwas gewöhnungsbedürftig sein. Letztlich dient es aber der Lesbarkeit. Blocksymbole wie {} werden ja auch immer mit Einrückungen versehen. Sie selbst dienen nur dem Compiler, die Einrückungen dem Menschen. F# reduziert das auf ein Mittel: Whitespace für Compiler und Menschen. Nur der letzte Wert zählt Die Warnung für die eingerückte 42 war für mich hingegen nicht so einfach zu verstehen. Was eben noch direkt auf der REPLKommandozeile funktioniert hat, sollte jetzt nicht mehr funktionieren? Kata “FizzBuzz” 23 Die vollständige Warnung lautet: “This expression should have type ‘unit’, but has type ‘int’. Use ‘ignore’ to discard the result of the expression, or ‘let’ to bind the result to a name.” Warum sollte 42 den Typ uint bekommen? Warum reicht int nicht aus? Oder warum irgendetwas ignorieren? Aber dann habe ich genauer gelesen. Es geht um unit, nicht uint. Und unit ist, wie sich herausstellte, soetwas wie void für C#/Java, also quasi ein “Untyp”. Was der Compiler hier bemängelt, das ist Unentschiedenheit. Er weiß nicht, was das soll, so ein Wert “allein in der Landschaft”, wenn doch am Ende nur der letzte Wert zählt für die Bindung an den Namen. Deshalb schlägt er vor, ihn auch an einen Namen mit let zu binden oder ihn explizit zu ignorieren. Das kann so gehen: 1 2 3 let myname = let x = 42 "hello, world!" oder so 1 2 3 let myname = ignore 42 "hello, world!" Beides macht natürlich keinen recht Sinn. Aber darum geht es hier ja nicht. Ich bin in diese “Falle” nur reingestolpert, als ich mit einer Sequenz von Literalen gespielt habe. ignore macht aus einem Wert beliebigen Typs ein unit, d.h. einen “kein Wert”-Wert :-) Kata “FizzBuzz” 24 In einer funktionalen Sprache ist das sicherlich der Sonderfall, aber er kommt vor. Manchmal will man halt nichts berechnen, also keinen Wert erzeugen, sondern nur etwas tun. Das ist z.B. der Fall bei der Konsolenausgabe. Die erzeugt kein Resultat, sondern einen Seiteneffekt auf einem Gerät. Deshalb könnte sie in einer letBindung problemlos stehen: Keine Warnung des Compilers. Auch keine Überraschung durch die Ausgabe von “hello, world!”. Immerhin wird die Bindung ja ausgeführt und damit printfn aufgerufen. Bindungen sind keine Funktionsdefinitionen Doch schauen Sie einmal hier. Da habe ich einen Moment grübeln müssen, bevor ich das verstanden habe: Kata “FizzBuzz” 25 “hello, world!” wird nur einmal ausgegeben, obwohl myname zweimal “aufgerufen” wird. Das finde ich sehr überraschend. Letztlich hat sich dadurch aber endlich geklärt, was bei F# mit Bindung gemeint ist. Eine Bindung ist nur die Assoziation eines Wertes mit einem Namen. Wenn dafür die Ausführung von mehreren Anweisungen mit einem abschließenden Resultat nötig ist, dann ist das ok. Ausgeführt wird, wenn der Kontrollfluss über das let läuft. Anschließend jedoch hat der Name eben einen Wert. Der wird ihm Kata “FizzBuzz” 26 entnommen, wo er genutzt wird. Dafür werden die Anweisungen, die dazu geführt haben, jedoch nicht wieder durchlaufen. Auch wenn die let-Bindung also aussehen mag wie eine Funktionsdefinition, so ist sie keine. Sie wird nicht wieder und wieder durchlaufen bei jeder Nutzung des Namens. Und so ist es plausibel, dass “hello, world!” nur einmal ausgegeben wird. Zwischenstand Eigentlich wollte ich nur herausfinden, wie man in F# eine Funktion definiert. Doch der dünne Faden der Literale hat sich als Wollknäuel erwiesen. Also, wo stehen wir? F# kennt Literale und die haben einen Typ, z.B. int oder string. Weitere werden uns bestimmt im weiteren Verlauf unserer Erkundung begegnen. Literale als Werte sind die simpelste Form von Ausdrücken. Um Werte nicht immer wieder berechnen zu müssen oder ihnen eine Bedeutung zu geben, können sie mit let an einen Namen gebunden werden. Findet das nicht statt, bindet F# sie an den vordefinierten Namen it. Solche Namen sind nur das: Namen. Es sind keine Variablen. Er ist nur ein Platzhalter, dessen Typ automatisch vom Compiler bestimmt wird. 1 2 let myname = 42 // int let yourname = "hello, world!" // string Ist ein Name gebunden, kann er (im selben Scope) nicht erneut gebunden werden. Kata “FizzBuzz” 1 2 27 let myname = 42 let myname = "hello, world!" // fehler! Eine Bindung kann mehrere Anweisungen enthalten, die aber nur einmal ausgeführt werden. Die letzte muss einen Wert liefern, der dann an den Namen gebunden wird. Funktionen als Werte Eigentlich hatte ich doch nur darüber nachgesonnen, dass Funktionen in F# eigentlich Werte sein müssten. Was ist denn nun damit? Tatsächlich ist das auch so. Nur sieht ein “Funktionswert”, d.h. ein Lambda-Ausdruck, etwas komplizierter aus als ein Literal. Die Syntax ist: 1 "fun" Parameterliste "->" Ausdruck Hier einige Beispiele: Beispiele für “Funktionswerte”, d.h. Lambda-Ausdrücke Kata “FizzBuzz” 28 Die Funktionen haben keinen Namen, ihre Parameterliste ist eine Reihe von Namen für formale Parameter und falls sie fehlt, steht dort ein leeres Klammerpaar. Wie schon bei den Literalen wird jeder “Funktionswert” an den Namen it gebunden. Die Daten des Wertes bildet eine interne Id wie “<fun:clo@4-3>”, der Typ nennt die formalen Parameter inkl. Typ und den Ergebnistyp, z.B. a:int -> b:int -> int. Warum das so geschrieben wird? Ich ahne es bisher nur. Um nicht wieder vom Ziel abzukommen, möchte ich das Thema hier jedoch zurückstellen. Einstweilen soll es reichen, wenn ich Ihren Blick auf den Unterschied der Typen bei der zweiten und dritten Funktion lenke: 1 2 x:'a -> 'a x:int -> int Was ist ‘a für ein Typ? Mit ein wenig Recherche wird schnell klar, dass hier F# Magie im Spiel ist. ‘a ist nämlich ein Typ-Platzhalter. Bei allen Funktionen außer der zweiten konnt der Compiler die Typen der formalen Parameter und des Resultats inferieren. Wo () steht, muss der Typ unit sein, also “kein Typ”. Wo 42 steht, muss der Typ int sein. Wo x + 1 oder a + b steht, musst der Typ auch int sein, denn die Addition ist ohne weitere Angaben eine Operation auf ganzen Zahlen. Der F#-Compiler ist also schlau. Viel schlauer sogar als der C#Compiler, der gerade mal bei einer lokalen Variablendeklaration wie 1 2 // C# var x = 42; den Typ inferieren kann. Bei Funktionsresultaten oder Parametern beherrscht C# das Kunststück nicht. Kata “FizzBuzz” 29 Bei aller Schläue kann der F#-Compiler jedoch nicht hellsehen. Die zweite Funktion lässt jeden Hinweis auf den Typ des Parameters vermissen. Also kann der Compiler keinen konkreten Typ für Parameter und Resultat inferieren - und trägt nur einen Platzhalter ein: ‘a. Er erzeugt sozusagen einen generischen “Funktionswert”. Der legt allerdings fest, dass Parametertyp und Resultatstyp gleich sind. Bindungen benutzen zur Funktionsdefinition Wie Sie sehen, bekommen “Funktionswerte” per default auch den Namen it. Das bedeutet, wir können ihnen auch einen eigenen Namen geben, oder? Ich probiere das mal aus: Ah, das sieht nun langsam nach einer Funktionsdefinition aus, wie ich sie für die Lösung der Kata brauche, oder? Interessanterweise Kata “FizzBuzz” 30 fehlt da jetzt die Id; sie ist wohl nicht mehr nötig, da der “Funktionswert” einen eigenen Namen bekommen hat. Einen solchen Lambda-Ausdruck kann ich nun aufrufen, um ihn rechnen zu lassen: Yesss! Das ist es, was ich brauche. Nach nur wenigen Umwegen endlich eine solide Funktion :-) Das macht mich mutig. Ich packe mal das bisher gelernte zusammen in einem längeren Programm: 1 2 3 let add = fun a b -> a + b let x = add 2 3 add 4 x Meine Erwartung: nach Ausführen der letzten Anweisung ist it an den Wert (9, int) gebunden. Und siehe da: Es funktioniert. Zuerst wird ein “Funktionswert” an add gebunden. Dann wird die Funktion add während der Bindung von x aufgerufen. Schließlich wird add nochmal aufgerufen, um 4 mit dem an x gebundenen Wert zu verknüpfen. Das sind am Ende die Namen mit ihren Werten: Kata “FizzBuzz” 1 2 3 31 val add : a:int -> b:int -> int val x : int = 5 val it : int = 9 Funktionen kann ich nun also grundsätzlich definieren. Allerdings ist die Syntax ein wenig umständlich. Bisher waren die Formulierungen in F# so knapp - aber beim Dreh- und Angelpunkt einer funktionalen Programmiersprache, den Funktionen, soll es so umständlich sein? Tatsächlich geht es kürzer. Das habe ich bei meiner Recherche zu “Funktionswerten” schon gesehen. Die Funktion add lässt sich auch so definieren: 1 let add a b = a + b = fun a b -> wird zusammengezogen zu a b =. Damit lässt sich leben, würde ich sagen. Zwischenstand Ich kann einfache Funktionen definieren. Und ich habe auch schon einen Ausdruck berechnet. Damit kann ich doch schon einen Teil der FizzBuzz-Aufgabe angehen. Darin müssen ja Zahlen auf ihre Kategorie geprüft werden: Liegt eine Fizz-Zahl oder eine Buzz-Zahl oder eine FizzBuzz-Zahl vor? Dafür brauche ich zwei Operatoren: Modulo und Gleichheitsprüfung. Ich probiere einfach mal, ob es dieselben sind wie bei C#: 1 2 > 2 % 3;; val it : int = 2 Kata “FizzBuzz” 32 Für Modulo ist das der Fall. Aber == ist kein Standardoperator. Die Referenz der Operatoren¹³ nennt vielmehr ein simples Gleichheitszeichen als Gleichheitsoperator. Das kann ich ja kaum glauben. So einfach kann es sein in einer Programmiersprache. 1 2 > 2 = 3;; val it : bool = false Jetzt kann ich ein paar erste Funktionen für die FizzBuzz-Lösung bauen. Deren Typ ist n:int -> bool: 1 2 let isFizz n = n % 3 = 0 let isBuzz n = n % 5 = 0 Die Prüfung auf eine FizzBuzz-Zahl soll nun diese beiden aber schon benutzen. Dazu muss ich ihre Resultate mit UND verknüpfen. Ich probiere es mal wieder mit dem C#-Operator: 1 2 > true && false;; val it : bool = false Das funktioniert tatsächlich. Also hier die dritte Funktion im Bunde: 1 let isFizzBuzz n = isFizz n && isBuzz n Einen Moment habe ich gezögert, ob vielleicht eine Klammerung nötig ist wie oben bei (it + 1). Aber es funktioniert ohne. Jetzt auf zur nächst schwierigeren Funktion, die eine Zahl bei Bedarf in eines der Worte wandelt. Sie wird diese Funktionen benutzen, muss nach deren Ergebnis jedoch unterschiedlich handeln. ¹³http://msdn.microsoft.com/en-us/library/dd233228.aspx Kata “FizzBuzz” 33 Fallunterscheidung mit if Was ich für die Transformationsfunktion brauche, ist eine Fallunterscheidung. Ein gutes altes if wäre jetzt schön. Ob F# das als funktionale Programmiersprache kennt? Wie sich herausstellt, ist das if F# nicht fremd. F# ist eine hybride Sprache, d.h. sie kennt Konstrukte der prozeduralen und objektorientierten Programmierung. Das finde ich wunderbar. Die Syntax ist simpel: 1 "if" Bedingung "then" Ausdruck ["else" Ausdruck] Es funktioniert also 1 if 2 = 2 then printfn "is true" wie dies 1 if 2 = 3 then printfn "is true" else printfn "is false" und dies 1 2 if 2 = 2 then printfn "is true" else printfn "is false" und schließlich dies Kata “FizzBuzz” 1 2 3 4 34 if 2 = 3 then printfn "is true" else printfn "is false" Wenn Sie mehr als eine Anweisung in einem Zweig ausführen wollen, müssen Sie das natürlich wieder über Whitespace dem Compiler deutlich machen. Das sieht eigentlich aus wie in C# oder Java, oder? Trotz der prozeduralen Syntax ist die if -Anweisung bei F# jedoch anders als ihre Cousins. Sie ist nämlich selbst ein Ausdruck. Sie können nämlich soetwas schreiben: 1 let b = if 2 = 3 then true else false Deshalb steht in der Syntaxdefinition auch “Ausdruck” und nicht “Anweisung”. Jeder Zweig liefert ein Resultat. In den ersten Beispielen ist das unit. Aber der Wert kann jeden Typ haben - solange beide Zweige denselben Typ liefern. Daraus folgt übrigens, dass ein if immer einen then- und einen else-Zweig haben muss. Denn was wäre das Resultat, wenn die Bedingung unwahr wäre und ein else fehlte? Eine Ausnahme von dieser Regel bilden nur if -Ausdrücke, deren then-Zweig unit liefert wie im ersten Beispiel. Dann darf else fehlen. Ich denke, damit gewappnet kann ich nun einen ersten Versuch für die Konvertierungsfunktion wagen: Kata “FizzBuzz” 1 2 3 4 5 6 7 8 9 10 11 35 let convert n = if isFizzBuzz n then "FizzBuzz" else if isFizz n then "Fizz" else if isBuzz n then "Buzz" else n.ToString() Als Bedingungen rufe ich die vorher schon definierten Prüfungsfunktionen auf. Die Ausdrücke in den Zweigen bestehen nur aus string-Literalen. Und nur wenn keine “besondere” Zahl anliegt, wird die in eine Zeichenkette gewandelt, damit alle Zweige den selben Typ zurückliefern. Die Umwandlung funktioniert genauso wie in C# durch Aufruf von ToString(). Wie gesagt, F# ist eine hybride Sprache. Zuerst habe ich übrigens n.ToString geschrieben. Aber das funktioniert nicht; dann erwartet der Compiler aktuelle Parameter für die Funktion. Sie ist jedoch parameterlos und das muss mit () angezeigt werden. Diese Konvertierungsfunktion funktioniert. convert 4 liefert “4”, convert 5 liefert “Buzz” und convert 15 liefert “FizzBuzz”. Ich könnte sie so stehenlassen und mich dem Restproblem der Kata zuwenden. Aber ich habe noch eine Idee, wie ich die Funktion verbessern kann, um dem hohen Wert der Kapselung zu dienen. Im Moment sind die Prüfungsfunktionen unabhängig von der Konvertierung definiert. Das muss jedoch nicht so sein. Es handelt sich dabei ja nur um Namensbindungen, also keine syntaktisch speziellen Funktionsdefinitionen. Nichts spricht daher dagegen, die in die Konvertierungsfunktion zu verlegen: Kata “FizzBuzz” 1 2 3 4 36 let convert n = let isFizz n = n % 3 = 0 let isBuzz n = n % 5 = 0 let isFizzBuzz n = isFizz n && isBuzz n 5 6 7 8 9 10 11 12 13 14 15 if isFizzBuzz n then "FizzBuzz" else if isFizz n then "Fizz" else if isBuzz n then "Buzz" else n.ToString() Jetzt sind die Prüfungsfunktionen nur innerhalb der Konvertierung sichtbar. Das finde ich angemessen. Die Umgebung muss über diese Details nicht Bescheid wissen. Solche Schachtelung von Funktionen habe ich seit Pascal in allen anderen Sprachen vermisst. Ich finde sie ausgenommen praktisch. Wenn sich bei der Codierung einer Funktion herausstellt, dass ein Teil besser in eine eigene Funktion ausgelagert werden sollte (Refactoring “Extract Method”), dann kann die lokal definiert werden. Der umfassendende Namensraum wird rauschfrei gehalten. Und jetzt noch eine kleine Schönheitskorrektur für die geschachtelte Fallunterscheidung. Bei der Recherche zu if habe ich gesehen, dass es nicht nur else, sondern auch elif gibt. Damit lassen sich solche Kaskaden knapper formulieren. Mit ein bisschen eingestreutem Whitespace ist nun alles viel klarer, oder? Kata “FizzBuzz” 1 2 3 4 37 let convert n = let isFizz n = n % 3 = 0 let isBuzz n = n % 5 = 0 let isFizzBuzz n = isFizz n && isBuzz n 5 6 7 8 9 if isFizzBuzz n then "FizzBuzz" elif isFizz n then "Fizz" elif isBuzz n then "Buzz" else n.ToString() Zwischenstand Die FizzBuzz-Lösung nimmt Form an. Funktionen sind definiert, sogar geschachtelt. Fallunterscheidungen werden getroffen, sogar geschachtelt. Das alles ist etwas anders als in C# oder Java - aber dann doch wieder nicht so anders. In jedem Fall kommt mir die Andersartigkeit stringend und nützlich vor: Werte lassen sich mit Namen griffig machen, Funktionsdefinitionen sind als Werte leichtgewichtig, Fallunterscheidungen sind auch nur Ausdrücke. Listen erzeugen Bisher habe ich mich um die Übersetzung einer einzelnen Zahl gekümmert. Das ist die eine Hälfte des FizzBuzz-Problems. Die andere ist die Menge der Zahlen. Es sollen ja Übersetzungen für die Zahlen von 1 bis 100 geliefert werden. Das hört sich nach einem Einsatz für eine Liste an. Die könnte Zahl für Zahl mit den Übersetzungen gefüllt werden. Eine Wiederholung ist also auch noch nötig. Womit beschäftige ich mich zuerst? Mit der Liste. Nach soviel Kontrollfluss jetzt mal wieder etwas mit Daten. Ich könnte jetzt sicherlich auf die Listen des .NET Framework zurückgreifen. Doch das wäre Mogelei. Wir möchten doch etwas über Kata “FizzBuzz” 38 F# lernen, oder? Listen werden dort bestimmt nativ unterstützt. Funktionale Programmiersprachen haben immer ein Faible für Liste ;-) Wie ein kurzer Blick in die Literatur zeigt, liege ich mit dieser Vermutung richtig. Bei [Pickering] stehen Listen ganz am Anfang und die Referenzseite¹⁴ ist überschaubar. Und der Umgang mit ihnen scheint angenehm einfach. Anders als bei C# oder Java sind sie Sprachbestandteil. Die einfachste Liste ist die leere Liste: 1 [] Deren Typ ist 1 'a list Das finde ich nett zu lesen. Da macht der Typ-Platzhalter ‘a Sinn: “[] ist eine Liste” und offen für jeden Typ. Sobald Sie aber etwas in eine Liste stecken, bekommt sie einen Typ: 1 2 3 4 > [42];; val it : int list = [1] > ["foo"];; val it : string list = ["foo"] Mehrere Elemente werden durch “;” getrennt. Das finde ich nicht so intuitiv wie eine Trennung durch “,” - aber was soll´s… 1 ["foo"; "bar"; "baz"] ¹⁴http://msdn.microsoft.com/en-us/library/dd233224.aspx Kata “FizzBuzz” 39 Gleich den Feldern müssen die Elemente in einer Liste vom selben Typ sein. Auch hier also keine Überraschung. Soweit die Notation für ein Listenliteral. Für die Kata-Lösung brauchen wir jedoch eine Möglichkeit, eine Liste schrittweise aufzubauen. Wie kann man Elemente zu einer Liste hinzufügen? Wenn ich die Literatur richtig verstehe, dann kann man einzelne Elemente in Liste nur am Anfang hinzufügen: 1 2 3 4 > "foo" :: [] val it : string list = ["foo"] > "bar" :: ["foo"] val it : string list = ["bar"; "foo"] :: ist der “prepend operator”, der einer Liste einen neuen Kopf aufsetzt. Das kann auch geschachtelte und in Reihe geschehen: 1 2 3 4 > "bar" :: ("foo" :: []) val it : string list = ["bar"; "foo"] > "bar" :: "foo" :: [] val it : string list = ["bar"; "foo"] Für die Problemlösung kommt mir das auf den ersten Blick allerdings umständlich vor. Ich denke die Liste von vorne nach hinten, von 1 bis 100. Ich würde also lieber an eine leere Liste eine 1 anhängen wollen und dann eine 2 und dann ein “Fizz” usw. Aber wenn es denn sein soll, dann können wir die Liste der Übersetzungen auch von hinten nach vorne wachsen lassen. Eine Schleife brauchen wir so oder so. Aufzählen mit einer for-Schleife Die funktionale Programmierung brüstet sich ja manchmal damit, dass sie sich mit soetwas wie Schleifen gar nicht abgebe. Wahre Entwickler lösen “Wiederholungsprobleme” mit Rekursionen. Ich habe Kata “FizzBuzz” 40 jedoch keine Lust, eine simple Aufzählung von Zahlen kompliziert mit einer Rekursion zu implementieren. Eine for-Schleife täte es auch - und das auch noch besser verständlich. Eleganz ist nett, ich ziehe meist allerdings Lesbarkeit vor. Zum Glück ist F# nicht so dogmatisch. Es gibt for-Schleifen, wie wir sie aus anderen Sprachen gewohnt sind. 1 2 for i = 1 to 3 do printfn "%d" i gibt die Zahlen 1 bis 3 aus. So einfach kann F# manchmal sein ;-) Und damit es für uns leichter ist, die Liste von hinten her aufzubauen, gibt es auch ein downto: 1 2 for i = 3 downto 1 do printfn "%d" i So lässt sich ein Start machen, denke ich. Zur Probe erzeuge ich mal eine kurze Liste von Zahlen: Arrghhh! Das geht nicht! Warum? Die Fehlermeldung “Block following this ‘let’ is unfinished. Expect an expression.” finde ich nicht sehr erhellend. Wenn ich schreibe Kata “FizzBuzz” 1 41 let liste = 1 :: [] ist auch nichts “unfinished”. Doch halt! Genau lesen hilft. Nicht das let ist “unfinished”, sondern der “Block following this ‘let’”. Dann hilft vielleicht, den Block zu vervollständigen:¹⁵ 1 2 3 4 let liste = [] for i = 3 downto 1 do let liste = i :: liste printfn "%A" liste Und tatsächlich, jetzt wird kein Fehler mehr gemeldet und es läuft. Das Ergebnis ist allerdings nicht, was ich erwartet habe: 1 2 3 [3] [2] [1] 4 5 6 val liste : 'a list val it : unit = () Die Liste wächst nicht. Das ist nicht schön. Wenn ich darüber aber einen Moment nachdenke, dann ist klar, woran es liegt. Sehen Sie es auch? Es liegt am Scope. Eigentlich möchte ich in der Schleife die liste von außerhalb der Schleife verändern. Stattdessen setze ich dem Wert der äußeren liste einen Kopf vor und binde die resultierende Liste an einen lokalen Namen - der dann die äußere Bindung verdeckt. Das war also ein naiver erster Versuch. Aber wie geht es besser, wie geht es more F#-like? ¹⁵printfn “%A” ist das schweizer Taschenmesser der Konsolenausgabe. Damit kann jeder Wert in eine Zeichenkette zur Ausgabe gewandelt werden. Kata “FizzBuzz” 42 More F#-like bedeutet sicherlich “funktionaler”. Und das bedeutet, weniger in veränderbaren Daten denken. Abgesehen vom ScopeProblem habe ich nämlich ganz prozedural gedacht, indem ich versucht habe, eine Datenstruktur zu verändern. Das tut man bei der Funktionalen Programmierung aber nicht. Dort arbeitet man mit immutable data. Die sind auch in F# der Default. Ich sollte also nicht davon ausgehen, eine Liste verändern zu können. Stattdessen werde ich für jede Zahl eine neue Liste erzeugen müssen. Aber wie soll das in einer Schleife gehen und als Ergebnis eine Liste mit allen einhundert Zahlen liefern? Eine Liste von innen her aufbauen Wenn ich in die Referenz zum Thema Listen schaue, findet sich dort zum Glück ein Ausweg. Statt die Schleife um die Liste herumzulegen, kann man sie auch in die Liste hineinstecken: 1 2 > let liste = [for i in 1 .. 3 -> i];; val liste : int list = [1; 2; 3] Das ist eine Liste, wie ich sie mir wünsche. Und ich muss auch nicht mehr “verkehrt herum” denken. List comprehension lautet das Zauberwort. Damit ist eine Syntax zur dynamischen Bildung von Listen gemeint. Wenn die Werte in einer Liste einfach zu bilden sind, kann das der Compiler selbst übernehmen. Die Definition 1 [1..3] erzeugt ebenfalls die Liste [1; 2; 3]. Aber ich will ja nicht die Zahlen selbst in der Liste haben, sondern konvertierte Zahlen. Also ist für mich die for-Schleife in der Liste Kata “FizzBuzz” 43 die Lösung, denke ich. Sie durchläuft wie foreach in C# eine Liste und weist jedes Element der Laufvariablen zu. Damit habe ich die Lösung für die Kata in der Tasche: 1 let fizzbuzz = [for i in 1 .. 100 -> convert i] fizzbuzz ist eine Liste mit den gesuchten Zeichenketten: 1 2 val fizzbuzz : string list = ["1"; "2"; "Fizz"; "4"; "Buzz"; "Fizz"; "7"; ...] Wenn ich wollte, könnte ich die sogar variabel gestalten, indem ich die obere Grenze hineinreiche: 1 let fizzbuzz max = [for i in 1 .. max -> convert i] Aber Achtung: Durch diesen kleinen Eingriff verändert sich der ganze Charakter von fizzbuzz! Ohne Berücksichtigung einer variablen Obergrenze max ist fizzbuz eine Liste: 1 val fizzbuzz : string list = ["1"; "2"; "Fizz"; ...] Mit der Obergrenze jedoch ist fizzbuzz eine Funktion: 1 val fizzbuzz : max:int -> string list Trickreich, oder? Ich jedenfalls bin verblüfft. Für mich ist das ein Zeichen von first class citizenship von Funktionen. Zum Abschluss noch ein bisschen mehr Kapselung. Code soll man ja wie eine Werkbank sauber hinterlassen: Kata “FizzBuzz” 1 2 3 4 5 44 let fizzbuzz max = let convert n = let isFizz n = n % 3 = 0 let isBuzz n = n % 5 = 0 let isFizzBuzz n = isFizz n && isBuzz n 6 7 8 9 10 if isFizzBuzz n then "FizzBuzz" elif isFizz n then "Fizz" elif isBuzz n then "Buzz" else n.ToString() 11 12 [for i in 1..max -> convert i] Jetzt ist von außen nicht mehr zu erkennen, wie fizzbuzz funktioniert. Die Detailfunktionen convert usw. sind lokal. Reflexion Problem gelöst - allerdings mit ein paar Umwegen. Doch die erhöhen ja bekanntlich die Ortskenntnis. F# ist syntaktisch nicht so weit weg von C# oder Java - aber im Detail ist der Umgang doch anders. Die Namensbindungen haben es in sich: Sie sehen wie Zuweisungen an Variablen aus, sind es aber nicht. Und Werte sind von Hause aus unveränderlich. Das hat mich dann doch zu etwas Umdenken gezwungen. Die Lösung gefällt mir jedoch. Sie ist schnörkellos, mit viel geringerem Rauschen als entsprechender C#-Code. Ohne Block-Klammern ist der Code kompakt. Und die leichtgewichtige Funktionsdefinition lädt dazu ein, schneller mal Code in eine Funktion auszulagern. Kata “Tannenbaum” Als zweite Aufgabe wähle ich die Kata “Tannenbaum”¹⁶, weil das jahreszeitlich gerade passt. Ich schreibe diese Zeilen in den Weihnachstagen 2013. Schreibe eine Funktion, die einen Tannenbaum mit ASCII-Art „gezeichnet“ als Text zurückliefert, der aus mehreren durch CRLF getrennten Zeilen besteht. Eingabe ist die Höhe des Tannenbaums. Als Beispiel ein Tannenbaum der Höhe 5: 1 2 3 4 5 6 7 8 > tannenbaum 5;; val it : string = " X XXX XXXXX XXXXXXX XXXXXXXXX I" Analyse Bevor ist mich an die Lösung mache, ein kurzer Blick auf das Problem. Wie ist so ein Tannenbaum aufgebaut? Er besteht aus mehreren Ästen - die Zeilen mit den “X” - und einem Stamm - die Zeile mit dem “I”. ¹⁶http://de.scribd.com/doc/141348450/Function-Kata-Tannenbaum Kata “Tannenbaum” 46 Bei einer Höhe von n gibt es n Zeilen mit Ästen. Die i-te Astzeile enthält dabei 2xi-1 mal “X” und ist n-1 Zeichen eingerückt. Beispiel: Astzeile 3 enthält 2x3-1=5 “X” und ist 5-3=2 Zeichen eingerückt. Um die “X” und die Einrückung “berechnen” zu können, müssen Baumhöhe sowie Astnummer bekannt sein. Der Stamm besteht aus einem einzigen “I”, das n-1 Zeichen eingerückt ist. Mit einem monospaced Font ausgegeben kommt so ein Tannenbaum schön zur Geltung :-) Zeichenketten bauen Die Analyse legt zwei Funktionen nahe: eine zum Bau von Ästen, eine zum Bau des Stamms. 1 2 let stamm baumhöhe = ... "I" ... let ast baumhöhe astnummer = ... "X" ... Aber wie kann ich Zeichen in einem string wiederholen? Was bietet F#? Wie sich herausstellt, kann man mit + nicht nur Zahlen addieren, sondern auch Zeichenketten aneinanderhängen: Kata “Tannenbaum” 1 2 47 > "foo" + "bar";; val it : string = "foobar" Das ist nützlich, um Einrückung und Ast zusammenzufügen. Vorher jedoch müssen beide in der richtigen Länge hergestellt werden. Dazu sind Zeichen zu vervielfältigen. Das möchte ich ungern mit einer Schleife tun. In C# geht das z.B. mit PadLeft(). Darauf könnte ich auch in F# zurückgreifen - aber vielleicht gibt es ja einen nativeren Weg. Tatsächlich bietet F# ein module für Zeichenkettenoperationen¹⁷ in seiner Standardbibliothek. Auf die darin definierten Namen kann ich mit dem Präfix String zugreifen. So kann ich z.B. die Länge einer Zeichenkette ermitteln: 1 2 > String.length "foo";; val it : int = 3 Oder ich kann Zeichenketten mit Wiederholungen füllen: 1 2 > String.replicate 3 "X";; val it : string = "XXX" Das ist genau, was ich brauche. Die ersten Funktionen der Lösung sind damit gefunden: ¹⁷http://msdn.microsoft.com/en-us/library/ee353758.aspx Kata “Tannenbaum” 1 48 let einrücken n = String.replicate n " " 2 3 4 5 6 let ast baumhöhe astnummer = einrücken (baumhöhe - astnummer) + String.replicate (2 * astnummer - 1) "X" + "\n" 7 8 9 10 let stamm baumhöhe = einrücken (baumhöhe-1) + "I" Zwei Funktionen für die Bestandteile des Baumes, eine für´s Einrücken, das beide brauchen. Erinnern Sie sich: Da es keine Trennzeichen zwischen Parametern gibt, müssen die berechneten aktuellen Werte in Klammern gesetzt werden. Das finde ich immer noch ein wenig gewöhnungsbedürftig. Rekursives Geäst Der Tannebaum besteht aus einer Anzahl Ästen wachsender Länge. Das riecht nach dem Einsatz einer Schleife. Zunächst jedenfalls. Aber wenn ich mir den Baum nochmal genauer ansehe, dann scheint er mir eher eine rekursive Struktur: Ein Tannenbaum der Höhe n ist ein Tannenbaum der Höhe n-1 und ein Ast der Länge n. Und ein Tannenbaum der Höhe 1 ist ein Ast der Länge 1. Dazu kommt dann noch ein Stamm unten drunter. Statt die Äste mit einer Schleife übereinander zu lesen, möchte ich lieber eine rekursive Funktion einsetzen. Aber wie geht Rekursion mit F#? Wenn ich das “Hello, World!” der Rekursion versuche zu codieren die Berechnung von Fibonnaci-Zahlen - Kata “Tannenbaum” 1 2 3 49 let fib n = if n = 1 || n = 2 then 1 else fib (n-1) + fib (n-2) dann meldet der Compiler “The value or constructor ‘fib’ is not defined” bei den Aufrufen von fib in der letzten Zeile. Die Auflösung ist jedoch simpel: F# möchte, dass Rekursion bewusst eingesetzt wird, weil sie potenziell “gefährlich” ist. Wie leicht kann man vergessen, eine Abbruchbedingung zu definieren. Deshalb müssen Sie rekursive Funktionen mit rec kennzeichnen: 1 2 3 let rec fib n = if n = 1 || n = 2 then 1 else fib (n-1) + fib (n-2) Mit dieser Information ist es nun allerdings simpel, den Tannenbaum zu bauen: 1 2 3 4 5 6 7 let tannenbaum baumhöhe = let rec geäst baumhöhe astnummer = if astnummer = 1 then ast baumhöhe astnummer else geäst baumhöhe (astnummer-1) + ast baumhöhe astnummer 8 9 10 geäst baumhöhe baumhöhe + stamm baumhöhe Er besteht aus rekursivem Geäst und darunter einem Stamm wie oben beschrieben. Wie immer sieht die Gesamtlösung natürlich schöner aus, wenn soviele Details wie möglich gekapselt sind. Hier der gesäuberte Code als Visual Studio Konsolenanwendung inklusive Ausgabe: Kata “Tannenbaum” 50 Reflexion Dieses Mal bin ich schneller zu einer Lösung gekommen. Ich konnte auf dem bei der ersten Kata Gelernten aufbauen. Das macht Spaß. Der Umgang mit Zeichenketten in F# war nicht schwierig. Aber ich merke mir: Bevor ich auf Gewohntes von C# zurückgreife, schaue ich ersteinmal, was F# nativ zu bieten hat. Etwas ungewohnt nach Jahren der Objektorientierung ist da der prozedurale Stil String.Length “foo” statt “foo”.Length -, doch letztlich finde ich den nicht wirklich nachteilig. Im Detail ist er hier und da vielleicht ein Kata “Tannenbaum” 51 Verlust an Lesbarkeit; dafür bietet F# ansonsten durch seine Syntax viele Vorteile, um knapp und übersichtlich zu formulieren. Dazu gehört auch der rekursive Ansatz. In C# hätte ich wahrscheinlich schnell eine iterative Lösung aufgesetzt. Doch mit F# lag angesichts unveränderlicher Daten eine rekursive Lösung näher, die ich dann im Grunde ohne Rauschen umsetzen konnte. Kata “ToDictionary” Weil es so schön war, gleich noch eine Aufgabe mit Zeichenketten: die Kata “ToDictionary”¹⁸: Schreibe eine Funktion, die eine “Konfigurationszeichenkette” in einen Dictionary übersetzt, also eine Liste von Name-Wert-Paaren generiert. Beispiele für solche configstrings: “a=1;b=2;c=3”, “a=1;a=2”, “a=1;;b=2”, “abc=1, d=xyz”. Mehrfachzuweisungen sind also erlaubt und leere Zuweisungen stören nicht. Analyse Die Kata ist vor allem ein Parsing-Problem. Ein Text mit einer Syntax ist in eine Datenstruktur zu übersetzen. Die Syntaxregeln sehen so aus: 1 2 Konfiguration ::= { Zuweisung ";" } . Zuweisung ::= Name "=" Wert . Leere Zuweisungen sind eine Sache des Parsers, Mehrfachzuweisungen müssen beim Füllen des Dictionary beachtet werden. ¹⁸http://de.scribd.com/doc/141109135/Function-Kata-ToDictionary Kata “ToDictionary” 53 Name-Wert-Paare in einer Map verwalten Als erstes will ich mich mal um die Datenstruktur kümmern. Da steckt für diese Aufgabe das Risiko, mit Zeichenketten kann ich ja schon umgehen. Naja, zumindest ein bisschen. Dictionary ist ein Collection-Typ der BCL. Den könnte ich in F# benutzen. Aber wieder stellt sich die Frage, ob F# nicht etwas eigenes bietet zur Verwaltung von Name-Wert-Paaren. Und tatsächlich: Der Name, mit dem in F# eine solche Datenstruktur bezeichnet wird, ist Map und steht für ein eigenes Modul¹⁹. Am einfachsten lässt sich so eine Map aus einer Liste von Name-WertPaaren füllen: 1 2 3 > Map.ofList [("a", "1"); ("b", "2")];; val it : Map<string,string> = map [("a", "1"); ("b", "2")] Das ist allerdings erklärungsbedürftig. Listen kennen wir schon. Aber was bedeutet (“a”, “1”) in der Liste? Literale sind einzelne Werte, z.B. 42, “a”. Mehrere Werte in Klammern durch Komma getrennt sind Tupel, z.B. (42, “a”, false). Tupel fassen Werte gleichen oder unterschiedlichen Typs zu einem neuen Wert zusammen. 1 2 3 let spielkarte = ("Kreuz", "König") let flug = ("AB1234", "HAM", "MUC", System.DateTime.Parse("30.1.2014 10:45")) Tupel sind sozusagen “adhoc Strukturen”, deren Bestandteile anonym sind. ¹⁹http://msdn.microsoft.com/en-us/library/ee353880.aspx Kata “ToDictionary” 54 Bei der Erzeugung einer Map aus einer Liste von Tupeln, wird der erste Werte in jedem Tupel als Name interpretiert und der zweite als Wert. Bei gegebener Liste von Name-Wert-Paaren, ist es also nicht schwer, eine Map zu erzeugen. Wie ist es aber mit doppelt vorkommenden Namen? 1 2 > Map.ofList [("a", "1"); ("a", "2")];; val it : Map<string,string> = map [("a", "2")] Zum Glück stört sich die Map daran nicht. Der letzte Wert “siegt”. Das passt gut für den Einsatzfall. Als Collection kann eine Map noch eine ganze Menge mehr. Doch das brauche ich hier nicht. Damit will ich mich und Sie also erstmal nicht belasten. Zeichenketten zerlegen Konfigurationen bestehen aus Angaben zwischen denen Trennzeichen stehen. Auf der obersten Ebene ist das ein “;”, auf der darunter liegenden Ebene ist das ein “=”. Ja, ich halte die Konfigurationsdatenstruktur für hierarchisch; deshalb spreche ich von Ebenen. In der Syntaxdefinition ist die Zuweisung in die Konfiguration eingeschachtelt. Wie kann ich also Zeichenketten an Trennzeichen zerlegen? F# bietet dafür keine eigenen Mittel. Die muss ich mir vielmehr vom .NET Framework borgen. 1 2 > "a=1".Split '=';; val it : string [] = [|"a"; "1"|] 55 Kata “ToDictionary” Die Split()-Funktion der String-Klasse ist auch in F# verfügbar. Und sie wird auch über Punkt-Notation auf die F#-Zeichenkette angewandt. (Der erste Parameter ist übrigens ein char, der auch in F# in einfache Anführungszeichen gesetzt wird.) Bei der Zerlegung der Konfiguration in Zuweisungen sollen allerdings leere Zuweisungen ausgelassen werden. Dafür gibt es eine Option: StringSplitOption.RemoveEmptyEntries. Wenn ich die aber versuche anzuwenden 1 "a;;b".Split ';' StringSplitOptions.RemoveEmptyEntries kommt es zu einem Fehler: “This value is not a function and cannot be applied”. Was soll das bedeuten? Warum taucht der nach dem ‘;’ auf? Ah, ich habe vergessen, dass die Überladung der Funktion mit der Option als ersten Parameter ein Feld von Trennzeichen erwartet. Hier die Nachbesserung: 1 2 "a;;b".Split [|';'|] StringSplitOptions.RemoveEmptyEntries Felder werden durch [||] gekennzeichnet, Listen durch []. Das ist nicht schwer. Aber das Problem bleibt. Was nun? Nach einigem Stöbern stoße ich auf die Lösung: Methoden des .NET Framework mit mehreren Parametern werden anders aufgerufen: 1 2 "a;;b".Split ([|';'|], StringSplitOptions.RemoveEmptyEntries) Mehrere Parameter müssen zu einem Tupel zusammengefasst werden. Kata “ToDictionary” 56 Damit ist das bisherige Problem gelöst - doch ein neues tritt auf: StringSplitOptions ist dem Compiler nicht bekannt. Das kann ich zum Glück leicht durch Zugabe des Namensraumes lösen: 1 2 3 "a;;b".Split ([|';'|], System.StringSplitOptions.RemoveEmptyEntries) Nun funktioniert es. Das Ergebnis ist wie gewünscht: 1 val it : string [] = [|"a"; "b"|] Etwas nervig finde ich alledings die Qulifizierung der Option. Der Name wird dadurch lang. Und wenn es öfter vorkommt, dann entsteht rauschen. Doch F# bietet Abhilfe. Auch hier kann man Namensräume importieren, mit open: 1 2 3 open System "a;;b".Split ([|';'|], StringSplitOptions.RemoveEmptyEntries) Explizite Typangabe Ich denke, jetzt bin ich soweit und kann die Funktionen für die Lösung der Aufgabe definieren. Die einfachste ist die zur Zerlegung der Konfiguration in Zuweisungen, d.h. die Auftrennung der eingehenden Zeichenkette bei “;”: Kata “ToDictionary” 1 2 3 4 57 open System let in_zuweisungen_zerlegen konfig = konfig.Split ([|';'|], StringSplitOptions.RemoveEmptyEntries) Der Compiler fühlt sich damit jedoch nicht wohl. Er meldet bei konfig.Split: “Lookup on object of indeterminate type…” Das bedeutet, er hat keine Ahnung, ob Split auf konfig angewandt werden darf. Die Typinferenz hat zu wenige Informationen. Besser wird es, wenn ich mit einer Typangabe für den formalen Parameter nachhelfe: 1 2 3 4 open System let in_zuweisungen_zerlegen (konfig:string) = konfig.Split ([|';'|], StringSplitOptions.RemoveEmptyEntries) Der Typ wird dem Namen wie weiland bei Pascal nachgestellt (type annotation²⁰). Das finde ich sehr gut leserlich. Manchmal ist das halt nötig, meist aber zum Glück nicht. Ich mag strenge Typisierung, aber sie kann auch lästig sein und zu Rauschen im Quelltext führen. Typinferenz ist für mich da eine sehr zeitgemäßer Schritt nach vorn. Transformieren mit map Das Ergebnis der Zerlegung in Zuweisungen ist ein Feld von Zeichenketten, z.B. [|”a=1”; “b=2”|]. Dessen Elemente müssen wiederum in Name und Wert zerlegt werden. Dafür muss ich über das Feld iterieren und ein neues Feld erzeugen. ²⁰http://msdn.microsoft.com/en-us/library/dd233180.aspx Kata “ToDictionary” 58 Mache ich das mit einer Schleife? Oder mit einer Rekursion? Nein, dieses Mal ist es einfacher, wie ich beim Stöbern zum Thema Collections gesehen habe. Es gibt eine ganze Reihe von Standardfunktionen auf Listen, Feldern, Maps. Eine davon ist map. map ist eine Funktion, in die eine Liste (oder ein Feld usw.) hineingereicht wird - und auch noch eine Funktion, die auf jedes Element der Collection anzuwenden ist. Das Ergebnis ist dann eine Collection gleichen Typs, allerdings mit durch die Funktion veränderten Elementen. Als Beispiel die Verdopplung der Zahlenwerte in einer Liste: 1 2 > List.map (fun n -> 2*n) [1;2;3];; val it : int list = [2; 4; 6] Damit sollte die Transformation der Zuweisungen in Name-WertPaare, die dann in eine Map eingetragen werden, ein Kinderspiel sein: 1 2 3 let in_name_wert_paare_zerlegen (zuweisungen:string array) = Array.map (fun (z:string) -> z.Split '=') zuweisungen Wieder sind Typannotationen nötig. Aber die kennen wir ja nun schon. Hervorhebenswert mag allerdings sein, wie der Typ string zu einem Feld wird: string array. Zusammen mit der vorherigen Funktion leistet diese nun schon einen guten Teil dessen, was zur Lösung der Aufgabe nötig ist: 1 2 3 > in_name_wert_paare_zerlegen (in_zuweisungen_zerlegen "a=1;b=2");; val it : string [] [] = [|[|"a"; "1"|]; [|"b"; "2"|]|] Kata “ToDictionary” 59 Tupel als Rückgabewerte Der letzte Schritt besteht darin, die Name-Wert-Paare in eine Map zu verwandelt. Wenn ich das allerdings mit dem bisherigen Ergebnis versuche, schlägt es fehl: 1 Map.ofArray [|[|"a"; "1"|]; [|"b"; "2"|]|] Der Fehler ist wieder einmal kryptisch: “This expression was expected to have type ‘a * ‘b but here has type ‘c[]”. Bei den obigen Experimenten habe ich die Map aus einer Liste statt aus einem Array erzeugt. Das sollte doch aber kein Problem sein. Es gibt nicht umsonst eine ofArray-Funktion. Also muss es an dem liegen, was in dem Feld steckt. Ja, genau, darin liegt das Problem. Oben enthielt die Liste Tupel, jetzt enthält das Feld Felder. Das drückt wohl “here has type c’[]” aus. Und somit wird klar, was “‘a * ‘b” bedeutet. Das ist die Beschreibung eines Tupel-Typs. Hier die Bestätigung meiner Vermutung: 1 2 > (42, "foo");; val it : int * string = (42, "foo") Die möglichen Werte eines Tupels ergeben sich sozusagen als Produkt der möglichen Werte seiner Elemente, deshalb int * string als Typbezeichnung. Angewandt auf die “Mappenbildung” bedeutet das: 1 Map.ofArray [|("a", "1"); ("b", "2")|] Kata “ToDictionary” 60 Kaum macht man es richtig, funktioniert es schon :-) Wunderbar. Das bedeutet jedoch, dass ich den Rückgabewert von in_name_wert_paare_zerlegen überdenken sollte, oder? Eigentlich war der bisher auch nicht wirklich passend. Wer würde erwarten, dass eine solche Methode Name und Wert als Feld zurückliefert? Die Korrektur kann ich auf zwei Weisen anbringen: Entweder verändere ich die Transformationsfunktion oder ich schalte dem map eine weitere Transformation nach. Ich entscheide mich für die Veränderung der Transformationsfunktion, um einen weiteren Durchlauf des Feldes zu vermeiden. Im ersten Schritt ziehe ich die Transformation nur in eine eigene Funktion heraus: 1 2 3 4 let in_name_wert_paare_zerlegen (zuweisungen:string array) = let zerlegen (z:string) = z.Split '=' Array.map zerlegen zuweisungen Schön, dass Funktionen so leichtgewichtig sind. Im zweiten Schritt baue ich die Zerlegungsfunktion so um, dass sie kein Feld mehr, sondern ein Tupel zurückliefert: 1 2 3 let zerlegen (z:string) = let name_wert = z.Split '=' (name_wert.[0], name_wert.[1]) Zweierlei ist hier neu: • Der Zugriff auf Feldelemente erfolgt über .[index]. Das ist ein bisschen umständlicher als in C#, aber noch erträglich. Kata “ToDictionary” 61 • Eine Funktion kann mehrere Werte gleichzeitig zurückliefern - in Form eines Tupels. Das finde ich sehr, sehr praktisch. In C# geht das zwar auch, nur ist es auch mit dem Tupel-Typ noch sehr umständlich. Tupel sind in C# und anderen Sprachen einfach keine first class citizens. Da bietet F# einfach mehr. Damit ist die Aufgabe im Grunde gelöst. In drei Schritten wird aus der Konfigurationszeichenkette eine Map: 1 2 3 4 > Map.ofArray (in_name_wert_paare_zerlegen (in_zuweisungen_zerlegen "a=1;b=2"));; val it : Map<string,string> = map [("a", "1"); ("b", "2")] Datenflüsse herstellen So richtig zufrieden bin ich allerdings noch nicht damit. Ich finde die Lösung nämlich schlecht zu lesen. Die geschachtelten Funktionsaufrufe sind zwar für C# oder Java ganz normal und funktionieren auch in F#. Schön ist jedoch etwas anderes. Schachtelungen waren bisher schlecht zu lesen - von rechts nach links und von innen nach außen -, das wird auch mit F# nicht besser. Bei meiner Recherche bin ich jedoch immer wieder über eine andere Möglichkeit gestolpert, Funktionen zu verknüpfen. Denn um nichts anderes geht es ja: mehrere Funktionen sollen zu etwas größerem verbunden werden. Schauen Sie einmal hier die Erzeugung einer Map aus einer Liste. Die kann “ganz normal” erfolgen: 1 Map.ofList [("a", 1), ("b", 2)] Oder man kann das so machen: Kata “ToDictionary” 1 62 [("a", 1), ("b", 2)] |> Map.ofList Im ersten Fall ist klar, dass die Liste der Tupel ein Parameter von ofList ist; es handelt sich um einen Funktionsaufruf. Im zweiten Fall hingegen fließen die Daten. Das soll zumindest der PipelineOperator |> ausdrücken. Zweck des Pipeline-Operators ist die Erhöhung der Verständlichkeit des Codes. Der ist jetzt nämlich von links nach rechts lesbar. Damit kann meine Lösung nämlich so aussehen: 1 2 3 "a=1;b=2" |> in_zuweisungen_zerlegen |> in_name_wert_paare_zerlegen |> Map.ofArray Wenn das nicht besser lesbar ist :-) Die Konfiguration fließt in in_zuweisungen_zerlegen, deren Ergebnis fließt in in_name_wert_paare_zerlegen und deren Ergebnis fließt in Map.ofArray. Der Wert, der links von |> steht, wird der letzte Parameter der folgenden Funktion. Mit dem Pipeline-Operator in der Hand möchte ich auch nochmal Zerlegung der Zuweisungen angehen. Da hatte ich mich ja für die Veränderung der Transformationsfunktion entschieden, um das Name-Wert-Array in ein Tupel umzuwandeln. Alternativ hätte ich aber auch “Tupelisierung” der Aufspaltung in Name und Wert nachschalten können: Kata “ToDictionary” 1 2 3 4 5 6 63 let in_name_wert_paare_zerlegen (zuweisungen:string array) = let zerlegen (z:string) = z.Split '=' let in_tupel_wandeln (a:string array) = (a.[0], a.[1]) zuweisungen |> Array.map zerlegen |> Array.map in_tupel_wandeln Auch wenn jetzt zweimal über ein Array iteriert wird, gefällt mir diese Lösung besser, glaube ich. Aber vielleicht bin ich auch einfach nur im Pipeline-Rausch :-) Reflexion Die komplette Lösung in 16 Zeilen: 1 open System 2 3 4 5 6 let toDictionary konfig = let in_zuweisungen_zerlegen (konfig:string) = konfig.Split ([|';'|], StringSplitOptions.RemoveEmptyEntries) 7 8 9 10 11 12 13 14 let in_name_wert_paare_zerlegen (zuweisungen:string array) = let zerlegen (z:string) = z.Split '=' let in_tupel_wandeln (a:string array) = (a.[0], a.[1]) zuweisungen |> Array.map zerlegen |> Array.map in_tupel_wandeln 15 16 17 18 konfig |> in_zuweisungen_zerlegen |> in_name_wert_paare_zerlegen |> Map.ofArray Kata “ToDictionary” 64 Das wäre auch mit weniger Zeilen gegangen. Doch so finde ich die Lösung lesbar, verständlich. Nochmal ein hoch auf die leichtgewichtigen Funktionsdefinitionen von F#. Merkenn Sie schon, wie man F#-Lösungen liest? Von unten nach oben. Anders als C# braucht F# Definitionen vor der Verwendung. In dieser Lösung konnte F# die Typinferenz nicht so ausspielen. Die type annotations finde ich aber nicht schlimm. Von der Syntax her gefallen sie mir sogar besser als in C#: erst der Name, dann der Typ. Das ist gut lesbar. Besonders nützlich finde ich allerdings den Pipeline-Operator. Sie werden sehen: Der kommt jetzt bestimmt in jeder weiteren Lösung vor. Zusammen mit Tupeln als Rückgabewerte von Funktionen bzw. Elementen in Listen wird er mir helfen, Flow-Designs mit F# umzusetzen. usw. Und hier noch weitere schon fertiggestellte sowie geplante Kapitel… usw. 66 Kata “Römische Zahlen I” Kata “Römische Zahlen II” Kata “Zustandsautomat” Reflexion des Lernfortschritts Zwischenspiel: Automatisiert testen Kata “Ringbuffer” Kata “Verzeichnisstatistik” Kata “Benutzeranmeldung” Kata “CSV Viewer” Kata “Leiterspiel” Kata “Taschenrechner” Tiefer… …können Sie mit mir in F# einsteigen, wenn das Büchlein fertig ist. Das Inhaltsverzeichnis gibt einen Ausblick, was nach den Katas in dieser Vorschau kommt. usw. 67 Es mag noch ein wenig dauern bis dahin. Aber tragen Sie sich doch bei Leanpub in die Interessenteliste ein, dann werden Sie benachrichtigt, wenn es soweit ist. Wenn Sie wollen, können Sie bei Leanpub auch einen Kommentar hinterlassen. So kämen wir ins Gespräch. -Ralf Westphal, Hamburg, Dezember 2013 Release Notes • Aktuelles Release – Kapitel “Zwischenspiel: Automatisiert testen” – Codeumbrüche verbessert • 5.1.2014 – Korrektur in FizzBuzz/Zwischenstand: Namen können im selben Scope natürlich nicht mehrfach gebunden werden. – Release Notes hinzugefügt • 3.1.2014: Erste Veröffentlichung bis Kata “Zustandsautomat” und inkl. Reflexion über den Lernfortschritt.