Funktionale Programmierung: Von Null zum Helden
Ethan Miller
Product Engineer · Leapcell

Detaillierte Erklärung der funktionalen Programmierung
Vielleicht haben Sie schon von funktionaler Programmierung gehört und sie sogar schon seit einiger Zeit verwendet. Aber können Sie klar erklären, was sie ist?
Wenn Sie online suchen, finden Sie leicht viele Antworten:
- Es ist ein Programmierparadigma parallel zur objektorientierten Programmierung und zur prozeduralen Programmierung.
- Sein wichtigstes Merkmal ist, dass Funktionen erstklassige Bürger sind.
- Es betont die Zerlegung des Rechenprozesses in wiederverwendbare Funktionen. Ein typisches Beispiel ist der MapReduce-Algorithmus, der aus der
map
-Methode und derreduce
-Methode besteht. - Nur reine Funktionen ohne Nebenwirkungen sind qualifizierte Funktionen.
Alle obigen Aussagen sind richtig, aber sie reichen nicht aus. Sie beantworten nicht die tiefere Frage: Warum macht man das so? Das ist es, was dieser Artikel beantworten soll. Ich werde Ihnen helfen, die funktionale Programmierung zu verstehen und ihre grundlegende Syntax in der einfachsten Sprache zu erlernen.
I. Kategorientheorie
Der Ursprung der funktionalen Programmierung ist ein Zweig der Mathematik, der als Kategorientheorie bezeichnet wird. Der Schlüssel zum Verständnis der funktionalen Programmierung ist das Verständnis der Kategorientheorie. Es ist eine komplexe Mathematik, die glaubt, dass alle konzeptionellen Systeme in der Welt zu "Kategorien" abstrahiert werden können.
1.1 Das Konzept der Kategorie
Was ist eine Kategorie? Eine Ein-Satz-Definition von Wikipedia lautet wie folgt: "In der Mathematik ist eine Kategorie eine algebraische Struktur, die 'Objekte' umfasst, die durch 'Pfeile' miteinander verbunden sind."
Das heißt, Konzepte, Dinge, Objekte usw., die bestimmte Beziehungen zueinander haben, bilden alle eine "Kategorie". Solange Sie die Beziehungen zwischen ihnen finden können, können Sie für alles eine "Kategorie" definieren.
Zum Beispiel bilden verschiedene Punkte und die Pfeile zwischen ihnen eine Kategorie. Die Pfeile stellen die Beziehungen zwischen den Mitgliedern der Kategorie dar, und der formale Name ist "Morphismus". Die Kategorientheorie glaubt, dass alle Mitglieder derselben Kategorie "Transformationen" in verschiedenen Zuständen sind. Durch "Morphismus" kann ein Mitglied in ein anderes transformiert werden.
1.2 Mathematisches Modell
Da eine "Kategorie" alle Objekte sind, die eine bestimmte Transformationsbeziehung erfüllen, kann ihr mathematisches Modell wie folgt zusammengefasst werden:
- Alle Mitglieder bilden eine Menge.
- Die Transformationsbeziehung ist eine Funktion.
Das heißt, die Kategorientheorie ist eine Abstraktion höherer Ebene der Mengenlehre. Ein einfaches Verständnis ist "Menge + Funktion". Theoretisch können durch Funktionen alle anderen Mitglieder der Kategorie aus einem Mitglied berechnet werden.
1.3 Kategorie und Container
Wir können uns eine "Kategorie" als einen Container vorstellen, der zwei Dinge enthält:
- Wert.
- Die Transformationsbeziehung des Wertes, das heißt, die Funktion.
Das Folgende ist der Code, um eine einfache Kategorie zu definieren:
class Category { constructor(val) { this.val = val; } addOne(x) { return x + 1; } }
Im obigen Code ist Category
eine Klasse und auch ein Container, der einen Wert (this.val
) und eine Transformationsbeziehung (addOne
) enthält. Sie haben vielleicht bemerkt, dass die Kategorie hier alle Zahlen sind, die sich um 1 unterscheiden.
Beachten Sie, dass in den folgenden Teilen dieses Artikels, wo immer "Container" erwähnt wird, er sich auf "Kategorie" bezieht.
1.4 Die Beziehung zwischen Kategorientheorie und funktionaler Programmierung
Die Kategorientheorie verwendet Funktionen, um die Beziehungen zwischen Kategorien auszudrücken. Mit der Entwicklung der Kategorientheorie wurde eine Reihe von Funktionsoperationsmethoden entwickelt. Diese Reihe von Methoden wurde anfangs nur für mathematische Operationen verwendet. Später implementierte sie jemand auf einem Computer, und sie wurde zur heutigen "funktionalen Programmierung".
Im Wesentlichen ist die funktionale Programmierung nur die Operationsmethode der Kategorientheorie. Sie ist die gleiche Art von Sache wie mathematische Logik, Infinitesimalrechnung und Determinanten. Sie sind alle mathematische Methoden. Es passiert nur, dass sie zum Schreiben von Programmen verwendet werden kann.
Verstehen Sie also, warum funktionale Programmierung erfordert, dass Funktionen rein und ohne Nebenwirkungen sind? Weil es eine mathematische Operation ist und ihr ursprünglicher Zweck darin besteht, Werte auszuwerten, ohne andere Dinge zu tun. Andernfalls kann sie die Funktionsoperationsregeln nicht erfüllen.
Kurz gesagt, in der funktionalen Programmierung ist eine Funktion wie ein Rohr. Ein Wert geht an einem Ende hinein, und ein neuer Wert kommt am anderen Ende heraus, ohne weitere Auswirkungen.
II. Funktionskomposition und Currying
Die funktionale Programmierung hat zwei grundlegendste Operationen: Komposition und Currying.
2.1 Funktionskomposition
Wenn ein Wert mehrere Funktionen durchlaufen muss, um ein anderer Wert zu werden, können alle Zwischenschritte zu einer Funktion kombiniert werden, die als "Funktionskomposition" bezeichnet wird.
Wenn beispielsweise die Transformationsbeziehung zwischen X
und Y
die Funktion f
ist und die Transformationsbeziehung zwischen Y
und Z
die Funktion g
ist, dann ist die Beziehung zwischen X
und Z
die zusammengesetzte Funktion g ∘ f
von g
und f
.
Das Folgende ist die Codeimplementierung (mit der JavaScript-Sprache). Beachten Sie, dass alle Beispielcodes in diesem Artikel vereinfacht sind. Der einfache Code zum Komponieren von zwei Funktionen lautet wie folgt:
const compose = function (f, g) { return function (x) { return f(g(x)); }; }
Die Funktionskomposition muss auch das Assoziativgesetz erfüllen:
compose(f, compose(g, h)) // ist äquivalent zu compose(compose(f, g), h) // ist äquivalent zu compose(f, g, h)
Die Komposition ist auch ein Grund, warum Funktionen rein sein müssen. Denn wie kann eine unsaubere Funktion mit anderen Funktionen komponiert werden? Wie können wir sicherstellen, dass sie nach verschiedenen Kompositionen das erwartete Verhalten erzielt?
Wie bereits erwähnt, ist eine Funktion wie ein Rohr für Daten. Dann ist die Funktionskomposition das Verbinden dieser Rohre, sodass Daten mehrere Rohre auf einmal durchlaufen können.
2.2 Currying
Die Komposition von f(x)
und g(x)
zu f(g(x))
hat die versteckte Prämisse, dass sowohl f
als auch g
nur einen Parameter akzeptieren können. Wenn sie mehrere Parameter akzeptieren können, wie z. B. f(x, y)
und g(a, b, c)
, wird die Funktionskomposition sehr mühsam.
Hier kommt das Currying ins Spiel. Das sogenannte "Currying" besteht darin, eine Funktion mit mehreren Parametern in eine Funktion mit einem einzigen Parameter umzuwandeln.
// Vor dem Currying function add(x, y) { return x + y; } add(1, 2) // 3 // Nach dem Currying function addX(y) { return function (x) { return x + y; }; } addX(2)(1) // 3
Mit Currying können wir sicherstellen, dass alle Funktionen nur einen Parameter akzeptieren. Sofern im folgenden Inhalt nicht anders angegeben, wird davon ausgegangen, dass Funktionen nur einen Parameter haben, nämlich den zu verarbeitenden Wert.
III. Funktor
Funktionen können nicht nur für die Transformation von Werten innerhalb derselben Kategorie verwendet werden, sondern auch für die Transformation einer Kategorie in eine andere. Dies beinhaltet den Funktor.
3.1 Das Konzept des Funktors
Der Funktor ist der wichtigste Datentyp in der funktionalen Programmierung und auch die grundlegende Einheit für Operationen und Funktionalität.
Er ist in erster Linie eine Kategorie, das heißt, ein Container, der Werte und Transformationsbeziehungen enthält. Das Besondere ist, dass seine Transformationsbeziehung nacheinander auf jeden Wert angewendet werden kann, wodurch der aktuelle Container in einen anderen Container transformiert wird.
Beispielsweise ist der linke Kreis ein Funktor, der die Kategorie der Namen von Personen darstellt. Wenn die externe Funktion f
übergeben wird, wird er in die Kategorie des Frühstücks auf der rechten Seite transformiert.
Allgemeiner gesagt, vervollständigt die Funktion f
die Transformation von Werten (a
zu b
). Wenn sie in den Funktor übergeben wird, kann die Transformation von Kategorien (Fa
zu Fb
) erreicht werden.
3.2 Die Codeimplementierung des Funktors
Jede Datenstruktur mit einer map
-Methode kann als Implementierung des Funktors angesehen werden.
class Functor { constructor(val) { this.val = val; } map(f) { return new Functor(f(this.val)); } }
Im obigen Code ist Functor
ein Funktor. Seine map
-Methode akzeptiert die Funktion f
als Parameter und gibt dann einen neuen Funktor zurück. Der darin enthaltene Wert ist der von f
verarbeitete Wert (f(this.val)
).
Es ist allgemein anerkannt, dass das Zeichen eines Funktors darin besteht, dass der Container eine map
-Methode hat. Diese Methode ordnet jeden Wert innerhalb des Containers einem anderen Container zu.
Im Folgenden sind einige Anwendungsbeispiele:
(new Functor(2)).map(function (two) { return two + 2; }); // Functor(4) (new Functor('flamethrowers')).map(function(s) { return s.toUpperCase(); }); // Functor('FLAMETHROWERS') (new Functor('bombs')).map(_.concat(' away')).map(_.prop('length')); // Functor(10)
Die obigen Beispiele zeigen, dass die Operationen in der funktionalen Programmierung alle durch Funktoren abgeschlossen werden, das heißt, die Operationen erfolgen nicht direkt an den Werten, sondern an den Containern dieser Werte - den Funktoren. Der Funktor selbst hat eine externe Schnittstelle (die map
-Methode), und verschiedene Funktionen sind Operatoren. Sie sind über die Schnittstelle mit dem Container verbunden und bewirken, dass die Werte innerhalb des Containers transformiert werden.
Daher ist das Erlernen der funktionalen Programmierung eigentlich das Erlernen verschiedener Operationen von Funktoren. Da die Operationsmethoden im Funktor gekapselt werden können, wurden verschiedene Arten von Funktoren abgeleitet. Es gibt so viele Arten von Funktoren wie es Operationen gibt. Die funktionale Programmierung wird zur Anwendung verschiedener Funktoren, um praktische Probleme zu lösen.
IV. Die of
-Methode
Sie haben vielleicht bemerkt, dass beim Generieren eines neuen Funktors oben der Befehl new
verwendet wurde. Dies ist wirklich nicht wie funktionale Programmierung, da der Befehl new
ein Symbol der objektorientierten Programmierung ist.
In der funktionalen Programmierung ist es allgemein anerkannt, dass ein Funktor eine of
-Methode hat, um einen neuen Container zu generieren.
Das Folgende ersetzt new
durch die of
-Methode:
Functor.of = function(val) { return new Functor(val); };
Dann kann das vorherige Beispiel wie folgt geändert werden:
Functor.of(2).map(function (two) { return two + 2; }); // Functor(4)
Dies ist eher wie funktionale Programmierung.
V. Maybe-Funktor
Der Funktor akzeptiert verschiedene Funktionen, um die Werte innerhalb des Containers zu verarbeiten. Hier kommt ein Problem. Der Wert innerhalb des Containers kann ein Nullwert sein (z. B. null
), und die externe Funktion hat möglicherweise keinen Mechanismus zum Verarbeiten von Nullwerten. Wenn ein Nullwert übergeben wird, ist es wahrscheinlich, dass ein Fehler verursacht wird.
Functor.of(null).map(function (s) { return s.toUpperCase(); }); // TypeError
Im obigen Code ist der Wert innerhalb des Funktors null
, und beim Konvertieren von Kleinbuchstaben in Großbuchstaben tritt ein Fehler auf.
Der Maybe-Funktor wurde entwickelt, um diese Art von Problem zu lösen. Einfach ausgedrückt, seine map
-Methode hat eine Nullwertprüfung.
class Maybe extends Functor { map(f) { return this.val? Maybe.of(f(this.val)) : Maybe.of(null); } }
Mit dem Maybe-Funktor verursacht die Verarbeitung von Nullwerten keinen Fehler.
Maybe.of(null).map(function (s) { return s.toUpperCase(); }); // Maybe(null)
VI. Either-Funktor
Die bedingte Operation if...else
ist eine der häufigsten Operationen. In der funktionalen Programmierung wird der Either-Funktor verwendet, um sie auszudrücken.
Der Either-Funktor hat zwei Werte im Inneren: den linken Wert (Left
) und den rechten Wert (Right
). Der rechte Wert ist der Wert, der unter normalen Umständen verwendet wird, und der linke Wert ist der Standardwert, der verwendet wird, wenn der rechte Wert nicht vorhanden ist.
class Either extends Functor { constructor(left, right) { this.left = left; this.right = right; } map(f) { return this.right? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right); } } Either.of = function (left, right) { return new Either(left, right); };
Das Folgende ist die Verwendung:
var addOne = function (x) { return x + 1; }; Either.of(5, 6).map(addOne); // Either(5, 7); Either.of(1, null).map(addOne); // Either(2, null);
Im obigen Code wird, wenn der rechte Wert einen Wert hat, der rechte Wert verwendet; andernfalls wird der linke Wert verwendet. Auf diese Weise drückt der Either-Funktor die bedingte Operation aus.
Eine übliche Verwendung des Either-Funktors ist die Bereitstellung eines Standardwerts. Das Folgende ist ein Beispiel:
Either .of({address: 'xxx'}, currentUser.address) .map(updateField);
Im obigen Code verwendet der Either-Funktor die Standardadresse im linken Wert, wenn der Benutzer keine Adresse angibt.
Eine andere Verwendung des Either-Funktors ist das Ersetzen von try...catch
, wobei der linke Wert verwendet wird, um einen Fehler darzustellen.
function parseJSON(json) { try { return Either.of(null, JSON.parse(json)); } catch (e: Error) { return Either.of(e, null); } }
Im obigen Code bedeutet, wenn der linke Wert leer ist, dass kein Fehler vorliegt; andernfalls enthält der linke Wert ein Fehlerobjekt e
. Im Allgemeinen können alle Operationen, die Fehler verursachen können, einen Either-Funktor zurückgeben.
VII. Ap-Funktor
Der in einem Funktor enthaltene Wert kann durchaus eine Funktion sein. Wir können uns eine Situation vorstellen, in der der Wert eines Funktors eine Zahl ist und der Wert eines anderen Funktors eine Funktion ist.
function addTwo(x) { return x + 2; } const A = Functor.of(2); const B = Functor.of(addTwo)
Im obigen Code ist der Wert innerhalb des Funktors A
2
und der Wert innerhalb des Funktors B
die Funktion addTwo
.
Manchmal möchten wir, dass die Funktion innerhalb des Funktors B
den Wert innerhalb des Funktors A
für die Berechnung verwenden kann. Hier kommt der Ap-Funktor ins Spiel.
ap
ist die Abkürzung für "applicative". Jeder Funktor, der die ap
-Methode bereitstellt, ist ein Ap-Funktor.
class Ap extends Functor { ap(F) { return Ap.of(this.val(F.val)); } }
Beachten Sie, dass der Parameter der ap
-Methode keine Funktion, sondern ein anderer Funktor ist.
Daher kann das vorherige Beispiel wie folgt geschrieben werden:
Ap.of(addTwo).ap(Functor.of(2)) // Ap(4)
Die Bedeutung des Ap-Funktors besteht darin, dass für Funktionen mit mehreren Parametern Werte aus mehreren Containern entnommen werden können, um verkettete Operationen von Funktoren zu erreichen.
function add(x) { return function (y) { return x + y; }; } Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3)); // Ap(5)
Im obigen Code ist die Funktion add
in der Curried-Form und benötigt insgesamt zwei Parameter. Durch den Ap-Funktor können wir Werte aus zwei Containern entnehmen. Es hat auch eine andere Art zu schreiben:
Ap.of(add(2)).ap(Maybe.of(3));
VIII. Monad-Funktor
Ein Funktor ist ein Container, der jeden Wert enthalten kann. Es ist völlig legal, dass ein Funktor einen anderen Funktor enthält. Dies führt jedoch zu einem verschachtelten Funktor.
Maybe.of( Maybe.of( Maybe.of({name: 'Mulburry', number: 8402}) ) )
Dieser Funktor hat drei verschachtelte Maybe
. Wenn Sie den internen Wert abrufen möchten, müssen Sie dreimal hintereinander this.val
verwenden. Dies ist natürlich sehr unpraktisch, daher entstand der Monad-Funktor.
Die Rolle des Monad-Funktors besteht darin, immer einen einschichtigen Funktor zurückzugeben. Er hat eine flatMap
-Methode, die die gleiche Funktion wie die map
-Methode hat. Der einzige Unterschied besteht darin, dass er den Wert im Inneren des letzteren extrahiert, wenn ein verschachtelter Funktor generiert wird, um sicherzustellen, dass das, was zurückgegeben wird, immer ein einschichtiger Container ist und keine verschachtelte Situation vorliegt.
class Monad extends Functor { join() { return this.val; } flatMap(f) { return this.map(f).join(); } }
Im obigen Code wird, wenn die Funktion f
einen Funktor zurückgibt, this.map(f)
einen verschachtelten Funktor generieren. Daher stellt die join
-Methode sicher, dass die flatMap
-Methode immer einen einschichtigen Funktor zurückgibt. Dies bedeutet, dass der verschachtelte Funktor abgeflacht wird.
IX. IO-Operationen
Eine wichtige Anwendung des Monad-Funktors ist die Implementierung von E/A-Operationen (Eingabe/Ausgabe).
E/A ist eine unsaubere Operation, und die normale funktionale Programmierung kann dies nicht verarbeiten. Zu diesem Zeitpunkt muss die E/A-Operation als Monad-Funktor geschrieben werden, um die Operation abzuschließen.
var fs = require('fs'); var readFile = function(filename) { return new IO(function() { return fs.readFileSync(filename, 'utf - 8'); }); }; var print = function(x) { return new IO(function() { console.log(x); return x; }); }
Im obigen Code sind das Lesen und Drucken von Dateien selbst unsaubere Operationen, aber readFile
und print
sind reine Funktionen, da sie immer den IO
-Funktor zurückgeben.
Wenn der IO
-Funktor ein Monad
mit einer flatMap
-Methode ist, können wir diese beiden Funktionen wie folgt aufrufen:
readFile('./user.txt') .flatMap(print)
Das Erstaunliche ist, dass der obige Code eine unsaubere Operation abschließt, aber da flatMap
einen IO
-Funktor zurückgibt, ist dieser Ausdruck rein. Wir schließen eine Operation mit Nebenwirkungen durch einen reinen Ausdruck ab, was die Rolle des Monad
ist.
Da das, was zurückgegeben wird, immer noch ein IO
-Funktor ist, können verkettete Operationen erreicht werden. Daher wird in den meisten Bibliotheken die flatMap
-Methode in chain
umbenannt.
var tail = function(x) { return new IO(function() { return x[x.length - 1]; }); } readFile('./user.txt') .flatMap(tail) .flatMap(print) // Äquivalent zu readFile('./user.txt') .chain(tail) .chain(print)
Der obige Code liest die Datei user.txt
und wählt und gibt dann die letzte Zeile aus.
Leapcell: Die Serverless-Plattform der nächsten Generation für Webhosting
Schließlich möchte ich die beste Plattform für die Bereitstellung von Diensten empfehlen: Leapcell
1. Unterstützung mehrerer Sprachen
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt viele Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein betrieblicher Aufwand - konzentrieren Sie sich einfach auf das Erstellen.

Erfahren Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ