Kapitel 01: Was tun wir hier eigentlich?
Einführung
Hallo! Ich bin Professor Franklin Frisby. Freut mich, Sie kennenzulernen. Wir werden etwas Zeit miteinander verbringen, denn ich soll Ihnen etwas über funktionale Programmierung beibringen. Aber genug über mich - was ist mit Ihnen? Ich hoffe, Sie sind zumindest ein wenig mit JavaScript vertraut, haben etwas objektorientierte Erfahrung und betrachten sich als arbeitender Programmierer. Sie brauchen keinen Doktortitel in Entomologie, sondern nur die Fähigkeit, Fehler zu finden und zu beheben.
Ich gehe nicht davon aus, dass Sie Vorkenntnisse in funktionaler Programmierung haben - wir wissen beide, wie gefährlich Annahmen sein können. Ich erwarte allerdings, dass Sie bereits mit problematischen Situationen durch veränderliche Zustände, unkontrollierte Nebeneffekte und prinzipienloses Design konfrontiert wurden. Da wir uns nun vorgestellt haben, legen wir los.
Dieses Kapitel soll vermitteln, worauf wir bei funktionaler Programmierung abzielen. Um die folgenden Kapitel zu verstehen, müssen wir zunächst klären, was ein Programm funktional macht. Sonst kritzeln wir nur planlos herum und vermeiden Objekte um jeden Preis - ein wirklich unbeholfener Ansatz. Wir brauchen eine klare Zielscheibe für unseren Code und einen verlässlichen Kompass bei stürmischer See.
Es gibt allgemeine Programmierprinzipien - verschiedene Leitsätze in Akronymform, die uns durch komplexe Anwendungen leiten: DRY (Don't Repeat Yourself), YAGNI (du wirst es nicht brauchen), lose Kopplung bei hoher Kohäsion, das Prinzip der minimalen Überraschung, Single Responsibility und so weiter.
Ich werde Sie nicht mit allen Regeln belästigen, die ich je gehört habe... Entscheidend ist, dass diese Prinzipien auch für funktionale Programmierung gelten, obwohl sie nur am Rande unser Hauptziel berühren. Bevor wir weitermachen, möchte ich Ihnen unser eigentliches Ziel beim Programmieren vermitteln - unser funktionales Xanadu.
Eine kurze Begegnung
Beginnen wir mit einem Hauch von Wahnsinn. Hier eine Möwen-Anwendung: Wenn sich Schwärme vereinen, werden sie größer. Bei der Fortpflanzung vermehren sie sich um die Anzahl der brütenden Möwen. Nun, dies ist bewusst kein guter objektorientierter Code, sondern soll die Gefahren zustandsbasierter Ansätze verdeutlichen:
class Flock {
constructor(n) {
this.seagulls = n;
}
conjoin(other) {
this.seagulls += other.seagulls;
return this;
}
breed(other) {
this.seagulls = this.seagulls * other.seagulls;
return this;
}
}
const flockA = new Flock(4);
const flockB = new Flock(2);
const flockC = new Flock(0);
const result = flockA
.conjoin(flockC)
.breed(flockB)
.conjoin(flockA.breed(flockB))
.seagulls;
// 32Wer würde solch monströsen Code schreiben? Die Verfolgung veränderlicher Zustände ist hier unverhältnismäßig schwer. Und siehe da - das Ergebnis ist sogar falsch! Eigentlich sollte 16 herauskommen, aber flockA wurde dauerhaft verändert. Arme flockA. Das ist IT-Anarchie! Das ist Tierarithmetik!
Falls Sie das Programm nicht verstehen - kein Problem, ich auch nicht. Entscheidend ist: Zustände und veränderliche Werte sind selbst in kleinem Maßstab schwer nachvollziehbar.
Versuchen wir es mit einem funktionalen Ansatz:
const conjoin = (flockX, flockY) => flockX + flockY;
const breed = (flockX, flockY) => flockX * flockY;
const flockA = 4;
const flockB = 2;
const flockC = 0;
const result =
conjoin(breed(flockB, conjoin(flockA, flockC)), breed(flockA, flockB));
// 16Jetzt erhalten wir das richtige Ergebnis - mit viel weniger Code. Die Funktionsverschachtelung ist etwas verwirrend (wir beheben dies in Kapitel 5). Gehen wir aber tiefer: Klare Benennung hilft. Bei genauer Betrachtung unserer Funktionen erkennen wir einfache Addition (conjoin) und Multiplikation (breed).
Diese Funktionen sind nur unnötig umbenannt. Nennen wir sie in multiply und add um:
const add = (x, y) => x + y;
const multiply = (x, y) => x * y;
const flockA = 4;
const flockB = 2;
const flockC = 0;
const result =
add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));
// 16Damit erschließen sich uralte mathematische Weisheiten:
// associative
add(add(x, y), z) === add(x, add(y, z));
// commutative
add(x, y) === add(y, x);
// identity
add(x, 0) === x;
// distributive
multiply(x, add(y,z)) === add(multiply(x, y), multiply(x, z));Diese bewährten Rechengesetze werden nützlich sein. Keine Sorge, wenn Sie sie nicht sofort parat hatten. Viele haben diese Grundlagen lange nicht mehr genutzt. Sehen wir, wie sie unsere Möwen-Anwendung vereinfachen:
// Original line
add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));
// Apply the identity property to remove the extra add
// (add(flockA, flockC) == flockA)
add(multiply(flockB, flockA), multiply(flockA, flockB));
// Apply distributive property to achieve our result
multiply(flockB, add(flockA, flockA));Brillant! Außer der Hauptfunktion benötigten wir keinen eigenen Code. Die add- und multiply-Definitionen dienen nur der Vollständigkeit - praktisch würden wir Bibliotheksfunktionen nutzen.
Vielleicht denken Sie „wie typisch, dieses Mathematik-Beispiel als Strohmann-Argument“ oder „echte Programme sind nicht so simpel und lassen sich nicht so analysieren“. Ich habe dieses Beispiel gewählt, weil die Grundrechenarten allgemein bekannt sind und die Nützlichkeit mathematischer Konzepte klar zeigen.
Keine Sorge: Im Buch behandeln wir Kategorientheorie, Mengenlehre und Lambda-Kalkül mit praxisnahen Beispielen, die ebenso elegant sind wie unser Möwen-Beispiel. Sie brauchen kein Mathematiker zu sein - es wird sich natürlich anfühlen, als würden Sie ein ganz normales Framework oder eine API verwenden.
Sie werden überrascht sein, dass wir ganze Anwendungen nach diesem funktionalen Muster schreiben können: Robuste Programme, präzise und nachvollziehbar, ohne ständige Neuentwicklungen. Gesetzlosigkeit mag verlockend sein, wenn man ein Verbrecher ist - in diesem Buch jedoch wollen wir die mathematischen Gesetze achten und befolgen.
Wir nutzen eine Theorie, bei der jedes Teil perfekt zusammenpasst. Wir modellieren Probleme mit generischen, kombinierbaren Bausteinen und nutzen ihre Eigenschaften strategisch. Dies erfordert mehr Disziplin als der „Anything goes“-Ansatz imperativer Programmierung (später genau definiert). Die Vorteile dieses mathematischen Rahmens werden Sie verblüffen.
Wir sahen einen ersten Funken unseres funktionalen Leitsterns, aber einige Konzepte gilt es vor der Reise noch zu verstehen.