F# lernen

Werbung
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.
Herunterladen