Wenn Sie sich in der funktionalen Programmierung versuchen, werden Sie bald auf das Konzept der reinen Funktionen stoßen. Wenn Sie fortfahren, werden Sie feststellen, dass Programmierer, die einen funktionalen Stil bevorzugen, von diesen Funktionen besessen zu sein scheinen. Sie sagen, dass reine Funktionen es Ihnen ermöglichen, über Code zu sprechen. Sie sagen, dass reine Funktionen Einheiten sind, die wahrscheinlich nicht so unvorhersehbar funktionieren, dass sie zu einem thermonuklearen Krieg führen werden. Sie können auch von solchen Programmierern lernen, dass reine Funktionen referenzielle Transparenz bieten. Und so - bis ins Unendliche.
Funktionale Programmierer haben übrigens recht. Reine Funktionen sind gut. Aber es gibt ein Problem ...
Der Autor des Materials, dessen Übersetzung wir Ihnen zur Kenntnis bringen, möchte darüber sprechen, wie mit Nebenwirkungen in reinen Funktionen umgegangen werden kann.
Das Problem der reinen Funktionen
Eine reine Funktion ist eine Funktion, die keine Nebenwirkungen hat (tatsächlich ist dies keine vollständige Definition einer reinen Funktion, aber wir werden auf diese Definition zurückkommen). Wenn Sie jedoch zumindest etwas in der Programmierung verstehen, wissen Sie, dass das Wichtigste hier genau die Nebenwirkungen sind. Warum die Zahl Pi bis zur hundertsten Dezimalstelle berechnen, wenn niemand diese Zahl lesen kann? Um etwas auf dem Bildschirm anzuzeigen oder auf einem Drucker zu drucken oder es in einer anderen Form darzustellen, die für die Wahrnehmung zugänglich ist, müssen wir den entsprechenden Befehl aus dem Programm aufrufen. Und was nützen Datenbanken, wenn nichts darauf geschrieben werden kann? Um den Betrieb von Anwendungen sicherzustellen, müssen Sie Daten von Eingabegeräten lesen und Informationen von Netzwerkressourcen anfordern. All dies kann nicht ohne Nebenwirkungen durchgeführt werden. Trotz dieses Zustands basiert die funktionale Programmierung auf reinen Funktionen. Wie schaffen es Programmierer, die Programme in einem funktionalen Stil schreiben, dieses Paradoxon zu lösen?
Wenn Sie diese Frage auf den Punkt bringen, tun funktionale Programmierer dasselbe wie Mathematiker: Sie betrügen. Trotz dieser Anschuldigung muss gesagt werden, dass sie aus technischer Sicht einfach bestimmten Regeln folgen. Aber sie finden Lücken in diesen Regeln und erweitern sie auf unglaubliche Größen. Sie tun dies auf zwei Arten:
- Sie nutzen die Abhängigkeitsinjektion. Ich nenne es ein Problem über einen Zaun werfen.
- Sie benutzen Funktoren, was mir eine extreme Form des Aufschubs erscheint. Hierbei ist zu beachten, dass es in Haskell als "IO-Funktor" oder "IO-Monade" bezeichnet wird. In PureScript wird der Begriff "Effekt" verwendet, der meiner Meinung nach etwas besser für die Beschreibung des Wesens von Funktoren ist.
Abhängigkeitsinjektion
Die Abhängigkeitsinjektion ist der erste Weg, um mit Nebenwirkungen umzugehen. Mit diesem Ansatz nehmen wir alles, was den Code verschmutzt, und fügen ihn in die Parameter der Funktion ein. Dann können wir all dies als etwas betrachten, das Teil der Verantwortung einer anderen Funktion ist. Ich werde dies anhand des folgenden Beispiels erklären:
// logSomething :: String -> String function logSomething(something) { const dt = (new Date())toISOString(); console.log(`${dt}: ${something}`); return something; }
Hier möchte ich eine Notiz für diejenigen machen, die mit Typensignaturen vertraut sind. Wenn wir uns strikt an die Regeln halten würden, müssten wir hier die Nebenwirkungen berücksichtigen. Aber wir werden uns später darum kümmern.
Die Funktion
logSomething()
weist zwei Probleme auf, die verhindern, dass sie als sauber deklariert wird: Sie erstellt ein
Date
Objekt und gibt etwas an die Konsole aus. Das heißt, unsere Funktion führt nicht nur Eingabe-Ausgabe-Operationen aus, sondern erzeugt auch unterschiedliche Ergebnisse, wenn sie zu unterschiedlichen Zeiten aufgerufen wird.
Wie macht man diese Funktion sauber? Mit der Abhängigkeitsinjektionstechnik können wir alles, was die Funktion verschmutzt, zu Funktionsparametern machen. Anstatt einen Parameter zu akzeptieren, akzeptiert unsere Funktion daher drei Parameter:
// logSomething: Date -> Console -> String -> * function logSomething(d, cnsl, something) { const dt = d.toIsoString(); return cnsl.log(`${dt}: ${something}`); }
Um die Funktion aufzurufen, müssen wir nun alles darauf übertragen, was sie zuvor verschmutzt hat:
const something = "Curiouser and curiouser!" const d = new Date(); logSomething(d, console, something);
Hier denken Sie vielleicht, dass dies alles Unsinn ist, dass wir das Problem nur um eine Ebene nach oben verschoben haben und dass dies unserem Code keine Reinheit verlieh. Und Sie wissen, das sind die richtigen Gedanken. Dies ist eine Lücke in ihrer reinsten Form.
Dies ist wie ein Schein-Analphabetismus: „Ich wusste nicht, dass das Aufrufen der
log
des
cnsl
Objekts zur Ausführung der E / A-Anweisung führen würde. Jemand hat es mir gerade gegeben, aber ich weiß nicht, woher das alles kommt. " Diese Einstellung ist falsch.
Und tatsächlich ist das, was passiert, nicht so dumm, wie es auf den ersten Blick erscheinen mag. Schauen
logSomething()
sich die Funktionen der Funktion
logSomething()
. Wenn Sie etwas Unreines tun möchten, müssen Sie es selbst tun. Angenommen, Sie können dieser Funktion verschiedene Parameter übergeben:
const d = {toISOString: () => '1865-11-26T16:00:00.000Z'}; const cnsl = { log: () => { // }, }; logSomething(d, cnsl, "Off with their heads!"); // "Off with their heads!"
Jetzt macht unsere Funktion nichts (sie gibt nur den Parameter "Something" zurück). Aber sie ist völlig rein. Wenn Sie es mehrmals mit denselben Parametern aufrufen, wird jedes Mal dasselbe zurückgegeben. Und das ist der springende Punkt. Um diese Funktion unrein zu machen, müssen wir absichtlich bestimmte Aktionen ausführen. Oder anders ausgedrückt: Alles, von dem eine Funktion abhängt, befindet sich in ihrer Signatur. Es greift nicht auf globale Objekte wie
console
oder
Date
. Dies formalisiert alles.
Darüber hinaus ist zu beachten, dass wir andere Funktionen auf unsere Funktion übertragen können, die zuvor nicht sauber war. Schauen Sie sich ein anderes Beispiel an. Stellen Sie sich vor, dass es in irgendeiner Form einen Benutzernamen gibt und wir den Wert des entsprechenden Feldes dieser Form erhalten müssen:
// getUserNameFromDOM :: () -> String function getUserNameFromDOM() { return document.querySelector('#username').value; } const username = getUserNameFromDOM(); username; // "mhatter"
In diesem Fall versuchen wir, einige Informationen aus dem DOM zu laden. Reine Funktionen tun dies nicht, da das
document
ein globales Objekt ist, das sich jederzeit ändern kann. Eine Möglichkeit, eine solche Funktion zu bereinigen, besteht darin, ihr das globale
document
als Parameter zu übergeben. Sie können die Funktion
querySelector()
an
querySelector()
. Es sieht so aus:
// getUserNameFromDOM :: (String -> Element) -> String function getUserNameFromDOM($) { return $('#username').value; } // qs :: String -> Element const qs = document.querySelector.bind(document); const username = getUserNameFromDOM(qs); username; // "mhatter"
Auch hier könnten Sie auf den Gedanken kommen, dass dies dumm ist. Schließlich haben wir hier einfach die Funktion
getUsernameFromDOM()
, was es uns nicht erlaubt, sie als sauber zu bezeichnen. Wir haben dies jedoch nicht beseitigt, sondern nur den Aufruf an das DOM an eine andere Funktion,
qs()
. Es scheint, dass das einzige auffällige Ergebnis dieses Schritts war, dass der neue Code länger als der alte war. Anstelle einer unreinen Funktion haben wir jetzt zwei Funktionen, von denen eine noch unrein ist.
Warten Sie kurz. Stellen Sie sich vor, wir müssen einen Test für die Funktion
getUserNameFromDOM()
schreiben. Überlegen Sie nun beim Vergleich der beiden Optionen für diese Funktion, mit welcher Option Sie leichter arbeiten können. Damit die Dirty-Version der Funktion überhaupt funktioniert, benötigen wir ein globales Dokumentobjekt. Darüber hinaus sollte dieses Dokument ein Element mit der
username
ID enthalten. Wenn Sie eine ähnliche Funktion außerhalb des Browsers testen müssen, müssen Sie JSDOM oder einen Browser ohne Benutzeroberfläche verwenden. Bitte beachten Sie, dass dies alles nur benötigt wird, um eine kleine Funktion mit einer Länge von mehreren Zeilen zu testen. Um die zweite, saubere Version dieser Funktion zu testen, reicht Folgendes aus:
const qsStub = () => ({value: 'mhatter'}); const username = getUserNameFromDOM(qsStub); assert.strictEqual('mhatter', username, `Expected username to be ${username}`);
Dies bedeutet natürlich nicht, dass zum Testen solcher Funktionen Integrationstests, die in einem echten Browser durchgeführt werden (oder zumindest etwas wie JSDOM verwenden), nicht erforderlich sind. Dieses Beispiel zeigt jedoch eine sehr wichtige Sache: Die Funktion
getUserNameFromDOM()
ist jetzt vollständig vorhersehbar. Wenn wir
qsStub()
, wird immer
mhatter
. Die "Unvorhersehbarkeit" haben wir auf die kleine Funktion
qs()
verschoben.
Bei Bedarf können wir unvorhersehbare Mechanismen auf Ebenen bringen, die noch weiter von der Hauptfunktion entfernt sind. Infolgedessen können wir sie relativ gesehen in die „Grenzbereiche“ des Codes verschieben. Dies führt dazu, dass wir eine dünne Hülle aus unreinem Code haben, die einen gut getesteten und vorhersehbaren Kernel umgibt. Die Vorhersagbarkeit des Codes erweist sich als äußerst wertvoll, wenn die Größe der von Programmierern erstellten Projekte zunimmt.
▍ Nachteile des Abhängigkeitsinjektionsmechanismus
Mit der Abhängigkeitsinjektion können Sie eine große und komplexe Anwendung schreiben. Ich weiß das, da ich selbst eine
solche Bewerbung geschrieben habe . Mit diesem Ansatz wird das Testen vereinfacht und Funktionsabhängigkeiten werden deutlich sichtbar. Die Abhängigkeitsinjektion ist jedoch nicht ohne Mängel. Das wichtigste ist, dass bei Verwendung sehr lange Funktionssignaturen erhalten werden können:
function app(doc, con, ftch, store, config, ga, d, random) { // } app(document, console, fetch, store, config, ga, (new Date()), Math.random);
In der Tat ist das nicht so schlimm. Die Nachteile solcher Konstruktionen zeigen sich, wenn einige der Parameter an bestimmte Funktionen übergeben werden müssen, die sehr tief in andere Funktionen eingebettet sind. Es sieht so aus, als müssten Parameter über viele Ebenen von Funktionsaufrufen übergeben werden. Wenn die Anzahl solcher Ebenen zunimmt, beginnt es zu nerven. Beispielsweise kann es erforderlich sein, das Objekt, das das Datum darstellt, durch 5 Zwischenfunktionen zu übertragen, während keine der Zwischenfunktionen dieses Objekt verwendet. Obwohl natürlich nicht gesagt werden kann, dass eine solche Situation so etwas wie eine universelle Katastrophe ist. Darüber hinaus ist es möglich, die Abhängigkeiten von Funktionen klar zu erkennen. Wie dem auch sei, dies ist immer noch nicht so angenehm. Daher betrachten wir den folgenden Mechanismus.
▍ Faule Funktionen
Werfen wir einen Blick auf die zweite Lücke, die von Anhängern der funktionalen Programmierung genutzt wird. Es besteht aus der folgenden Idee: Eine Nebenwirkung ist keine Nebenwirkung, bis sie tatsächlich auftritt. Ich weiß, das klingt mysteriös. Um dies herauszufinden, betrachten Sie das folgende Beispiel:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; }
Ein Beispiel ist vielleicht ein dummes, das weiß ich. Wenn wir die Nummer 0 benötigen, geben Sie sie einfach an der richtigen Stelle im Code ein, damit sie angezeigt wird. Und ich weiß auch, dass Sie keinen JavaScript-Code zur Kontrolle von Atomwaffen schreiben werden. Wir benötigen diesen Code jedoch, um die betreffende Technologie zu veranschaulichen.
Hier ist ein Beispiel für eine unreine Funktion. Es gibt Daten an die Konsole aus und ist auch die Ursache des Atomkrieges. Stellen Sie sich jedoch vor, wir brauchen die Null, die diese Funktion zurückgibt. Stellen Sie sich ein Szenario vor, in dem wir nach dem Start einer Rakete etwas berechnen müssen. Angenommen, wir müssen möglicherweise einen Countdown-Timer oder ähnliches starten. In diesem Fall wäre es völlig natürlich, im Voraus über die Berechnungen nachzudenken. Und wir müssen sicherstellen, dass die Rakete genau bei Bedarf startet. Wir müssen die Berechnungen nicht so durchführen, dass sie versehentlich zum Start dieser Rakete führen könnten.
fZero()
wir uns also, was passiert, wenn wir die Funktion
fZero()
in eine andere Funktion
fZero()
, die sie einfach zurückgibt. Nehmen wir an, es wird so etwas wie ein Sicherheits-Wrapper sein:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; } // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() { return fZero; }
Sie können die Funktion
returnZeroFunc()
beliebig
returnZeroFunc()
aufrufen. In diesem Fall sind wir (theoretisch) sicher, bis die Implementierung dessen, was es zurückgibt, durchgeführt wird. In unserem Fall bedeutet dies, dass die Ausführung des folgenden Codes nicht zu einem Atomkrieg führt:
const zeroFunc1 = returnZeroFunc(); const zeroFunc2 = returnZeroFunc(); const zeroFunc3 = returnZeroFunc();
Gehen wir nun etwas strenger als zuvor zur Definition des Begriffs „reine Funktion“ über. Auf diese Weise können wir die Funktion
returnZeroFunc()
genauer untersuchen. Die Funktion ist also unter folgenden Bedingungen sauber:
- Keine beobachteten Nebenwirkungen.
- Transparenz verknüpfen. Das heißt, das Aufrufen einer solchen Funktion mit denselben Eingabewerten führt immer zu denselben Ergebnissen.
returnZeroFunc()
die Funktion
returnZeroFunc()
analysieren.
Hat sie irgendwelche Nebenwirkungen? Wir haben gerade herausgefunden, dass das Aufrufen von
returnZeroFunc()
keine Raketen
returnZeroFunc()
. Wenn Sie nicht aufrufen, was diese Funktion zurückgibt, geschieht nichts. Daher können wir schließen, dass diese Funktion keine Nebenwirkungen hat.
Ist diese Funktion referenziell transparent? Das heißt, gibt es immer dasselbe zurück, wenn dieselben Eingabedaten an es übergeben werden? Wir werden dies überprüfen und dabei die Tatsache ausnutzen, dass wir im obigen Codefragment diese Funktion mehrmals aufgerufen haben:
zeroFunc1 === zeroFunc2; // true zeroFunc2 === zeroFunc3; // true
Es sieht alles gut aus, aber die Funktion
returnZeroFunc()
ist noch nicht vollständig sauber. Sie bezieht sich auf eine Variable, die außerhalb ihres eigenen Bereichs liegt. Um dieses Problem zu lösen, schreiben wir die Funktion neu:
// returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() { function fZero() { console.log('Launching nuclear missiles'); // return 0; } return fZero; }
Jetzt kann die Funktion als sauber betrachtet werden. In dieser Situation spielen jedoch JavaScript-Regeln gegen uns. Wir können nämlich den Operator
===
nicht mehr verwenden, um die referenzielle Transparenz einer Funktion zu überprüfen. Dies liegt daran, dass
returnZeroFunc()
immer einen neuen Verweis auf die Funktion
returnZeroFunc()
. Die Transparenz der Links kann überprüft werden, indem der Code selbst überprüft wird. Eine solche Analyse zeigt, dass bei jedem Funktionsaufruf eine Verknüpfung zu derselben Funktion zurückgegeben wird.
Vor uns liegt eine nette kleine Lücke. Aber kann es in realen Projekten verwendet werden? Die Antwort auf diese Frage ist positiv. Bevor wir jedoch darüber sprechen, wie dies in der Praxis angewendet werden kann, werden wir unsere Idee ein wenig weiterentwickeln.
fZero()
nämlich zur gefährlichen Funktion
fZero()
:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; }
Wir werden versuchen, die von dieser Funktion zurückgegebene Null zu verwenden, aber wir werden es tun, damit (bisher) kein Atomkrieg beginnt. Erstellen Sie dazu eine Funktion, die die von der Funktion
fZero()
Null nimmt und eine hinzufügt:
// fIncrement :: (() -> Number) -> Number function fIncrement(f) { return f() + 1; } fIncrement(fZero); // // 1
Das ist Pech ... Wir haben versehentlich einen Atomkrieg begonnen. Versuchen wir es noch einmal, aber dieses Mal geben wir keine Nummer zurück. Stattdessen geben wir eine Funktion zurück, die eines Tages eine Zahl zurückgibt:
// fIncrement :: (() -> Number) -> (() -> Number) function fIncrement(f) { return () => f() + 1; } fIncrement(zero); // [Function]
Jetzt können Sie ruhig atmen. Die Katastrophe wird abgewendet. Wir setzen die Studie fort. Dank dieser beiden Funktionen können wir eine ganze Reihe von „möglichen Zahlen“ erstellen:
const fOne = fIncrement(zero); const fTwo = fIncrement(one); const fThree = fIncrement(two);
Darüber hinaus können wir viele Funktionen erstellen, deren Namen mit
f
beginnen (nennen wir sie
f*()
Funktionen), die für die Arbeit mit „möglichen Zahlen“ ausgelegt sind:
// fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number) function fMultiply(a, b) { return () => a() * b(); } // fPow :: (() -> Number) -> (() -> Number) -> (() -> Number) function fPow(a, b) { return () => Math.pow(a(), b()); } // fSqrt :: (() -> Number) -> (() -> Number) function fSqrt(x) { return () => Math.sqrt(x()); } const fFour = fPow(fTwo, fTwo); const fEight = fMultiply(fFour, fTwo); const fTwentySeven = fPow(fThree, fThree); const fNine = fSqrt(fTwentySeven); // , . !
Sehen Sie, was wir hier gemacht haben? Mit "möglichen Zahlen" können Sie dasselbe tun wie mit gewöhnlichen Zahlen. Mathematiker nennen dies
Isomorphismus . Eine gewöhnliche Zahl kann immer in eine "mögliche Zahl" umgewandelt werden, indem sie in eine Funktion eingefügt wird. Sie können die "mögliche Nummer" durch Aufrufen der Funktion erhalten. Mit anderen Worten, wir haben eine Zuordnung zwischen regulären Zahlen und "möglichen Zahlen". Dies ist in der Tat viel interessanter, als es scheinen mag. Bald werden wir auf diese Idee zurückkommen.
Die obige Technik unter Verwendung der Wrapper-Funktion ist eine gültige Strategie. Wir können uns so oft wie nötig hinter Funktionen verstecken. Und da wir noch keine dieser Funktionen aufgerufen haben, sind theoretisch alle rein. Und niemand beginnt einen Krieg. Im regulären Code (nicht raketenbezogen) brauchen wir am Ende tatsächlich Nebenwirkungen. Wenn wir alles, was wir brauchen, in eine Funktion einwickeln, können wir diese Effekte präzise steuern. Wir wählen den Zeitpunkt, zu dem diese Effekte auftreten.
Es sollte beachtet werden, dass es nicht sehr bequem ist, überall einheitliche Konstruktionen mit Haufen von Klammern zu verwenden, um Funktionen zu deklarieren. Das Erstellen neuer Versionen jeder Funktion ist ebenfalls keine angenehme Aktivität. JavaScript hat einige großartige integrierte Funktionen wie
Math.sqrt()
. Es wäre großartig, wenn es eine Möglichkeit gäbe, diese gewöhnlichen Funktionen mit unseren „ausstehenden Werten“ zu verwenden. Eigentlich werden wir jetzt darüber sprechen.
Funktoreffekt
Hier werden wir über Funktoren sprechen, die durch Objekte dargestellt werden, die unsere „verzögerten Funktionen“ enthalten. Zur Darstellung des Funktors verwenden wir das
Effect
. Wir werden unsere Funktion
fZero()
in ein solches Objekt
fZero()
. Aber bevor wir dies tun, werden wir diese Funktion ein wenig sicherer machen:
// zero :: () -> Number function fZero() { console.log('Starting with nothing'); // , , . // . return 0; }
Nun beschreiben wir die Konstruktorfunktion zum Erstellen von Objekten vom Typ
Effect
:
// Effect :: Function -> Effect function Effect(f) { return {}; }
Da hier nichts besonders Interessantes ist, werden wir an dieser Funktion arbeiten. Wir wollen also die übliche Funktion
fZero()
mit dem
fZero()
. Um ein solches Szenario bereitzustellen, werden wir eine Methode schreiben, die eine reguläre Funktion akzeptiert und sie eines Tages auf unseren „ausstehenden Wert“ anwendet. Und wir werden dies tun, ohne die
Effect
aufzurufen. Wir nennen eine solche Funktionskarte
map()
. Es hat einen solchen Namen, weil es eine Zuordnung zwischen der üblichen Funktion und der
Effect
. Es kann so aussehen:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); } } }
Wenn Sie nun genau beobachten, was passiert, haben Sie möglicherweise Fragen zur Funktion
map()
. Es sieht dem Lied verdächtig ähnlich. Wir werden später auf dieses Problem zurückkommen, aber jetzt werden wir testen, was wir im Moment in Aktion haben:
const zero = Effect(fZero); const increment = x => x + 1;
Also ... Jetzt haben wir keine Gelegenheit zu beobachten, was hier passiert ist. Ändern wir daher den
Effect
, um sozusagen die Möglichkeit zu erhalten, "den Abzug zu betätigen":
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } } } const zero = Effect(fZero); const increment = x => x + 1; // . const one = zero.map(increment); one.runEffects(); // // 1
Bei Bedarf können wir die
map()
-Funktion weiter aufrufen:
const double = x => x * 2; const cube = x => Math.pow(x, 3); const eight = Effect(fZero) .map(increment) .map(double) .map(cube); eight.runEffects();
Hier wird das, was gerade passiert, schon interessanter. Wir nennen es einen "Funktor". All dies bedeutet, dass das
Effect
eine
map()
Funktion hat und einige
Regeln befolgt. Dies sind jedoch keine Regeln, die etwas verbieten. In diesen Regeln geht es darum, was Sie tun können. Sie sind eher wie Privilegien. Da das
Effect
ein Funktor ist, befolgt es diese Regeln. Dies ist insbesondere die sogenannte „Kompositionsregel“.
Es sieht so aus:
Wenn es ein
e.map(g).map(f)
Namen
e
und zwei Funktionen
f
und
g
, entspricht
e.map(g).map(f)
e.map(x => f(g(x)))
.
Mit anderen Worten, zwei aufeinanderfolgende
map()
-Methoden entsprechen dem Zusammensetzen von zwei Funktionen. Dies bedeutet, dass ein Objekt vom Typ
Effect
ähnliche Aktionen ausführen kann (denken Sie an eines der obigen Beispiele):
const incDoubleCube = x => cube(double(increment(x)))
Wenn wir das tun, was hier gezeigt wird, erhalten wir garantiert das gleiche Ergebnis wie bei Verwendung einer Version dieses Codes mit einem dreifachen Aufruf von
map()
. Wir können dies verwenden, wenn wir den Code umgestalten, und wir können sicher sein, dass der Code korrekt funktioniert. In einigen Fällen kann das Ändern eines Ansatzes zu einem anderen sogar die Leistung verbessern.
Jetzt schlage ich vor, nicht mehr mit Zahlen zu experimentieren und darüber zu sprechen, wie der in realen Projekten verwendete Code eher aussieht.
▍Methode von ()
Der Konstruktor des
Effect
akzeptiert als Argument eine Funktion. Dies ist praktisch, da die meisten Nebenwirkungen, die wir verschieben möchten, Funktionen sind. Dies sind beispielsweise
Math.random()
und
console.log()
. Manchmal müssen Sie jedoch einen Wert in ein
Effect
einfügen, das keine Funktion ist. , ,
window
. , . , ( -, , , Haskell
pure
):
// of :: a -> Effect a Effect.of = function of(val) { return Effect(() => val); }
, , , -. , , . HTML- . , . . Zum Beispiel:
window.myAppConf = { selectors: { 'user-bio': '.userbio', 'article-list': '#articles', 'user-name': '.userfullname', }, templates: { 'greet': 'Pleased to meet you, {name}', 'notify': 'You have {n} alerts', } };
,
Effect.of()
,
Effect
:
const win = Effect.of(window); userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']);
▍ Effect
. ,
Effect
. ,
getElementLocator()
,
Effect
, . DOM,
document.querySelector()
— , . :
// $ :: String -> Effect DOMElement function $(selector) { return Effect.of(document.querySelector(s)); }
, ,
map()
:
const userBio = userBioLocator.map($);
, , .
div
,
map()
, , . ,
innerHTML
, :
const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));
, .
userBio
, . , , , . , ,
Effect('user-bio')
. , , , :
Effect(() => '.userbio');
— . :
Effect(() => window.myAppConf.selectors['user-bio']);
,
map()
, ( ). , ,
$
, :
Effect(() => $(window.myAppConf.selectors['user-bio']))
, :
Effect( () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio']))) );
Effect.of
, :
Effect( () => Effect( () => document.querySelector(window.myAppConf.selectors['user-bio']) ) );
, , , .
Effect
.
▍ join()
? ,
Effect
. , , .
Effect
.runEffect()
. . , - , , , , . , .
join()
.
Effect
,
runEffect()
, . , .
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } } }
, :
const userBioHTML = Effect.of(window) .map(x => x.myAppConf.selectors['user-bio']) .map($) .join() .map(x => x.innerHTML);
▍ chain()
,
.map()
,
.join()
, . , , . , ,
Effect
. ,
.map()
.join()
. , ,
Effect
:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } chain(g) { return Effect(f).map(g).join(); } } }
chain()
- , ,
Effect
( ,
). HTML- :
const userBioHTML = Effect.of(window) .map(x => x.myAppConf.selectors['user-bio']) .chain($) .map(x => x.innerHTML);
-. . ,
flatMap
. , , — , ,
join()
. Haskell, ,
bind
. , - , ,
chain
,
flatMap
bind
— .
▍ Effect
Effect
, . . , DOM, , ? , , , . , . —
.
// tpl :: String -> Object -> String const tpl = curry(function tpl(pattern, data) { return Object.keys(data).reduce( (str, key) => str.replace(new RegExp(`{${key}}`, data[key]), pattern ); });
. :
const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str});
, . . (
name
pattern
)
Effect
.
tpl()
, ,
Effect
.
,
map()
Effect
tpl()
:
pattern.map(tpl);
, .
map()
:
map :: Effect a ~> (a -> b) -> Effect b
:
tpl :: String -> Object -> String
,
map()
pattern
, ( ,
tpl()
)
Effect
.
Effect (Object -> String)
pattern
Effect
. .
Effect
, .
ap()
:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } chain(g) { return Effect(f).map(g).join(); } ap(eff) { // - ap, , eff ( ). // map , eff ( 'g') // g, f() return eff.map(g => g(f())); } } }
.ap()
:
const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str})); const pattern = win.map(w => w.myAppConfig.templates('greeting')); const greeting = name.ap(pattern.map(tpl));
, … , ,
.ap()
. , ,
map()
,
ap()
. , , .
. , . , , ,
Effect
,
ap()
. , :
// liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c) const liftA2 = curry(function liftA2(f, x, y) { return y.ap(x.map(f)); // : // return x.map(f).chain(g => y.map(g)); });
liftA2()
, , .
liftA3()
:
// liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d) const liftA3 = curry(function liftA3(f, a, b, c) { return c.ap(b.ap(a.map(f))); });
,
liftA2()
liftA3()
Effect
. , ,
ap()
.
liftA2()
:
const win = Effect.of(window); const user = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str}); const pattern = win.map(w => w.myAppConfig.templates['greeting']); const greeting = liftA2(tpl)(pattern, user);
?
, , , . ? ,
Effect
ap()
. , ? ?
: « , , ».
:
▍
— . , , , .
const pattern = window.myAppConfig.templates['greeting'];
, , , :
const pattern = Effect.of(window).map(w => w.myAppConfig.templates('greeting'));
— , , , , . . — , , . , , , , , , . , . — . , , , . , .
. .
▍ Effect
, , . -
Facebook
Gmail
. ? .
, . . CSV- . . , , , . , . , . , , , .
, . ,
map()
reduce()
, . . , . , , , . 4 (, , 8, 16, ). , , . , . , - .
, , . , . Ähnelt nichts? , , , . . , .
TensorFlow , .
TensorFlow, , . «». , , :
node1 = tf.constant(3.0, tf.float32) node2 = tf.constant(4.0, tf.float32) node3 = tf.add(node1, node2)
Python, JavaScript. ,
Effect
,
add()
, (
sess.run()
).
print("node3: ", node3) print("sess.run(node3): ", sess.run(node3)) # node3: Tensor("Add_2:0", shape=(), dtype=float32) # sess.run(node3): 7.0
, (7.0) ,
sess.run()
. , . , , , .
Zusammenfassung
, . , .
Effect
.
, , , , , . , , .
Effect
, , , . , .
— . , . , , . . . , .
Liebe Leser! ?