Kommentar des Übersetzers: Dies ist eine Übersetzung eines großartigen Artikels von Dan Abramov, einem React-Mitarbeiter. Seine Beispiele sind für JS geschrieben, aber sie werden Entwicklern in jeder Sprache gleichermaßen klar sein. Die Idee ist allen gemeinsam.
Haben Sie von algebraischen Effekten gehört?
Meine ersten Versuche herauszufinden, wer sie sind und warum sie mich aufregen sollten, waren erfolglos. Ich habe mehrere PDFs gefunden , aber sie haben mich noch mehr verwirrt. (Aus irgendeinem Grund schlafe ich beim Lesen von wissenschaftlichen Artikeln ein.)
Aber mein Kollege Sebastian nannte sie weiterhin das mentale Modell einiger Dinge, die wir in React tun. (Sebastian arbeitet im React-Team und hat viele Ideen eingebracht, einschließlich Hooks und Suspense.) Irgendwann wurde es ein lokales Mem im React-Team, und viele unserer Gespräche endeten mit den folgenden:
Es stellte sich heraus, dass algebraische Effekte ein cooles Konzept sind und es nicht so beängstigend ist, wie es mir nach dem Lesen dieser PDFs zunächst erschien. Wenn Sie nur React verwenden, müssen Sie nichts darüber wissen, aber wenn Sie wie ich interessiert sind, lesen Sie weiter.
(Haftungsausschluss: Ich bin kein Forscher auf dem Gebiet der Programmiersprachen und habe möglicherweise etwas in meiner Erklärung durcheinander gebracht. Lassen Sie mich wissen, wenn ich falsch liege!)
Es ist noch früh in der Produktion
Algebraische Effekte sind derzeit ein experimentelles Konzept aus dem Bereich des Studiums von Programmiersprachen. Dies bedeutet, dass Sie im Gegensatz zu async/await
for
async/await
oder sogar async/await
Ausdrücken diese derzeit höchstwahrscheinlich nicht in der Produktion verwenden können. Sie werden nur von wenigen Sprachen unterstützt , die speziell für das Studium dieser Idee entwickelt wurden. Es gibt Fortschritte bei der Implementierung in OCaml, die ... noch andauert . Mit anderen Worten, beobachten Sie, aber berühren Sie nicht mit Ihren Händen.
Warum sollte es mich stören?
Stellen Sie sich vor, Sie schreiben Code mit goto
und jemand erzählt Ihnen von der Existenz von if
und for
Konstrukten. Oder vielleicht stecken Sie in einer Rückruf-Hölle und jemand zeigt Ihnen async/await
. Ziemlich cool, nicht wahr?
Wenn Sie der Typ sind, der Programmierinnovationen ein paar Jahre bevor sie in Mode kommen, lernen möchte, ist es möglicherweise an der Zeit, sich für algebraische Effekte zu interessieren. Obwohl nicht notwendig. So spricht man über async/await
im Jahr 1999.
Was für Effekte sind das?
Der Name mag etwas verwirrend sein, aber die Idee ist einfach. Wenn Sie mit try/catch
Blöcken vertraut sind, werden Sie die algebraischen Effekte schnell verstehen.
Erinnern wir uns zuerst an try/catch
. Angenommen, Sie haben eine Funktion, die Ausnahmen auslöst. Möglicherweise gibt es mehrere verschachtelte Aufrufe zwischen ihm und dem catch
:
function getName(user) { let name = user.name; if (name === null) { throw new Error(' '); } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } catch (err) { console.log(", : ", err); }
Wir werfen eine Ausnahme in getName
, die jedoch über makeFriends
zum nächsten catch
"auftaucht". Dies ist die Haupteigenschaft von try/catch
. Für die Fehlerbehandlung ist kein Zwischencode erforderlich.
Im Gegensatz zu Fehlercodes in Sprachen wie C müssen Sie bei Verwendung von try/catch
keine Fehler manuell durch jede Zwischenebene leiten, um den Fehler auf der obersten Ebene zu behandeln. Ausnahmen werden automatisch angezeigt.
Was hat das mit algebraischen Effekten zu tun?
Im obigen Beispiel können wir das Programm nicht mehr ausführen, sobald ein Fehler auftritt. Wenn wir uns in einem catch
, wird die normale Programmausführung gestoppt.
Es ist alles vorbei. Es ist zu spät. Das Beste, was wir tun können, ist, uns von einem Fehler zu erholen und vielleicht irgendwie zu wiederholen, was wir getan haben, aber wir können nicht auf magische Weise dorthin zurückkehren, wo wir waren, und etwas anderes tun. Und mit algebraischen Effekten können wir.
Dies ist ein Beispiel, das in einem hypothetischen JavaScript-Dialekt geschrieben wurde (nennen wir es zum Spaß ES2025), mit dem wir nach dem fehlenden user.name
:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
(Ich entschuldige mich bei allen Lesern aus dem Jahr 2025, die im Internet nach „ES2025“ suchen und in diesen Artikel fallen. Wenn bis dahin algebraische Effekte Teil von JavaScript werden, werde ich den Artikel gerne aktualisieren!)
Anstelle von throw
wir das hypothetische Schlüsselwort perform
. In ähnlicher Weise verwenden wir anstelle von try/catch
das hypothetische try/handle
. Die genaue Syntax spielt hier keine Rolle - ich habe mir gerade etwas ausgedacht, um die Idee zu veranschaulichen.
Also, was ist hier los? Schauen wir uns das genauer an.
Anstatt einen Fehler zu werfen, führen wir den Effekt aus . So wie wir jedes Objekt werfen können, können wir hier einen Wert für die Verarbeitung übergeben . In diesem Beispiel übergebe ich eine Zeichenfolge, es kann sich jedoch um ein Objekt oder einen anderen Datentyp handeln:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; }
Wenn wir eine Ausnahme auslösen, sucht die Engine nach dem nächstgelegenen try/catch
Handler im Aufrufstapel. Wenn wir einen Effekt ausführen, sucht die Engine auf der Suche nach dem nächstgelegenen try/handle
Effekt-Handler oben auf dem Stapel:
try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
Dieser Effekt ermöglicht es uns zu entscheiden, wie mit der Situation umgegangen werden soll, wenn der Name nicht angegeben ist. Neu hier (im Vergleich zu Ausnahmen) ist der hypothetische resume with
:
try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
Dies ist etwas, was Sie mit try/catch
nicht tun können. Es ermöglicht uns , dorthin zurückzukehren, wo wir den Effekt ausgeführt haben, und etwas vom Handler zurückzugeben . : -O
function getName(user) { let name = user.name; if (name === null) {
Es dauert ein wenig, bis man es sich bequem gemacht hat, aber konzeptionell unterscheidet sich dies nicht wesentlich von try/catch
mit einer Rückkehr.
Beachten Sie jedoch, dass algebraische Effekte ein viel leistungsfähigeres Werkzeug sind als nur try/catch
. Die Fehlerbehebung ist nur einer von vielen möglichen Anwendungsfällen. Ich habe nur mit diesem Beispiel begonnen, weil es für mich am einfachsten zu verstehen war.
Funktion hat keine Farbe
Algebraische Effekte haben interessante Auswirkungen auf asynchronen Code.
In Sprachen mit async/await
Funktionen normalerweise eine „Farbe“ ( Russisch ). In JavaScript können wir beispielsweise getName
nicht einfach asynchron machen, ohne makeFriends
und seine aufrufenden Funktionen mit asynchron zu infizieren. Dies kann sehr schmerzhaft sein, wenn ein Teil des Codes manchmal synchron und manchmal asynchron sein muss.
JavaScript-Generatoren funktionieren ähnlich : Wenn Sie mit Generatoren arbeiten, sollte der gesamte Zwischencode auch über Generatoren Bescheid wissen.
Was hat das damit zu tun?
Vergessen wir für einen Moment async / await und kehren zu unserem Beispiel zurück:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
Was ist, wenn unser Effekt-Handler den "Ersatznamen" nicht synchron zurückgeben kann? Was ist, wenn wir es aus der Datenbank erhalten möchten?
Es stellt sich heraus, dass wir resume with
asynchron von unserem Effekthandler aus aufrufen können, ohne Änderungen an getName
oder makeFriends
:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { setTimeout(() => { resume with ' '; }, 1000); } }
In diesem Beispiel rufen wir den resume with
nur eine Sekunde später auf. Sie können einen resume with
Rückruf in Betracht ziehen, den Sie nur einmal aufrufen können. (Sie können sich auch Freunden zeigen, indem Sie dieses Ding "eine einmalig begrenzte Fortsetzung " nennen (der Begriff " begrenzte Trennung" hat noch keine stabile Übersetzung ins Russische erhalten - ca. Übersetzung).)
Jetzt sollte die Mechanik algebraischer Effekte etwas klarer sein. Wenn wir einen Fehler auslösen, dreht die JavaScript-Engine den Stapel, indem sie dabei lokale Variablen zerstört. Wenn wir jedoch den Effekt ausführen , erstellt unsere hypothetische Engine einen Rückruf (tatsächlich ein „Fortsetzungsrahmen“, ca. Transl.) Mit dem Rest unserer Funktion und setzt ihn mit fort.
Nochmals eine Erinnerung: Die spezifische Syntax und die spezifischen Schlüsselwörter werden nur für diesen Artikel vollständig erfunden. Es geht nicht darum, sondern um die Mechanik.
Hinweis zur Sauberkeit
Es ist erwähnenswert, dass algebraische Effekte als Ergebnis der Untersuchung der funktionalen Programmierung entstanden sind. Einige der Probleme, die sie lösen, gelten nur für die funktionale Programmierung. In Sprachen, die keine willkürlichen Nebenwirkungen zulassen (wie Haskell), sollten Sie beispielsweise Konzepte wie Monaden verwenden, um Effekte durch Ihr Programm zu ziehen. Wenn Sie jemals das Monaden-Tutorial gelesen haben, wissen Sie, dass es schwierig sein kann, es zu verstehen. Algebraische Effekte helfen dabei, etwas Ähnliches mit etwas weniger Aufwand zu erreichen.
Deshalb sind die meisten Diskussionen über algebraische Effekte für mich völlig unverständlich. (Ich kenne Haskell und seine "Freunde" nicht.) Ich denke jedoch, dass algebraische Effekte selbst in einer unreinen Sprache wie JavaScript ein sehr leistungsfähiges Werkzeug sein können, um das "Was" vom "Wie" in Ihrem Code zu trennen.
Mit ihnen können Sie Code schreiben, der beschreibt, was Sie tun:
function enumerateFiles(dir) { const contents = perform OpenDirectory(dir); perform Log('Enumerating files in ', dir); for (let file of contents.files) { perform HandleFile(file); } perform Log('Enumerating subdirectories in ', dir); for (let directory of contents.dir) {
Und wickeln Sie es später mit etwas ein, das beschreibt, wie Sie es tun:
let files = []; try { enumerateFiles('C:\\'); } handle(effect) { if (effect instanceof Log) { myLoggingLibrary.log(effect.message); resume; } else if (effect instanceof OpenDirectory) { myFileSystemImpl.openDir(effect.dirName, (contents) => { resume with contents; }); } else if (effect instanceof HandleFile) { files.push(effect.fileName); resume; } }
Was bedeutet, dass diese Teile eine Bibliothek werden können:
import { withMyLoggingLibrary } from 'my-log'; import { withMyFileSystem } from 'my-fs'; function ourProgram() { enumerateFiles('C:\\'); } withMyLoggingLibrary(() => { withMyFileSystem(() => { ourProgram(); }); });
Im Gegensatz zu Async / Warten oder Generatoren erfordern algebraische Effekte keine Komplikation von „Zwischenfunktionen“. Unser Aufruf von enumerateFiles
sich zwar tief in unserem Programm, aber solange es einen Effekt-Handler für jeden der Effekte gibt, die irgendwo oben ausgeführt werden können, funktioniert unser Code weiterhin.
Effekt-Handler ermöglichen es uns, die Programmlogik von bestimmten Implementierungen ihrer Effekte zu trennen, ohne unnötige Tänze und Boilerplate-Code. Zum Beispiel könnten wir das Verhalten in den Tests komplett neu definieren, um das gefälschte Dateisystem zu verwenden und Schnappschüsse von Protokollen zu erstellen, anstatt sie auf der Konsole anzuzeigen:
import { withFakeFileSystem } from 'fake-fs'; function withLogSnapshot(fn) { let logs = []; try { fn(); } handle(effect) { if (effect instanceof Log) { logs.push(effect.message); resume; } }
Da Funktionen keine „Farbe“ haben (der Zwischencode muss nichts über Effekte wissen) und Effekthandler zusammengestellt werden können (sie können verschachtelt sein), können Sie mit ihnen sehr ausdrucksstarke Abstraktionen erstellen.
Typen Hinweis
Da algebraische Effekte aus statisch typisierten Sprachen stammen, konzentriert sich der größte Teil der Debatte darüber darauf, wie sie in Typen ausgedrückt werden können. Dies ist zweifellos wichtig, kann aber auch das Verständnis des Konzepts erschweren. Deshalb geht es in diesem Artikel überhaupt nicht um Typen. Ich sollte jedoch beachten, dass normalerweise die Tatsache, dass eine Funktion einen Effekt ausführen kann, in einer Signatur ihres Typs codiert wird. Auf diese Weise sind Sie vor einer Situation geschützt, in der unvorhersehbare Effekte auftreten oder Sie nicht nachverfolgen können, woher sie stammen.
Hier kann man sagen, dass technisch algebraische Effekte Funktionen in statisch typisierten Sprachen „Farbe verleihen“, da Effekte Teil einer Typensignatur sind. Das ist tatsächlich so. Das Festlegen der Typanmerkung für eine Zwischenfunktion, um einen neuen Effekt einzuschließen, ist an sich keine semantische Änderung - im Gegensatz zum Hinzufügen von Async oder dem Verwandeln der Funktion in einen Generator. Typinferenz kann auch dazu beitragen, die Notwendigkeit kaskadierender Änderungen zu vermeiden. Ein wichtiger Unterschied besteht darin, dass Sie Effekte „unterdrücken“ können, indem Sie einen leeren Stub oder eine temporäre Implementierung einfügen (z. B. einen Synchronisationsaufruf für einen asynchronen Effekt), wodurch Sie bei Bedarf die Auswirkungen auf externen Code verhindern oder in einen anderen Effekt umwandeln können.
Benötige ich algebraische Effekte in JavaScript?
Ehrlich gesagt weiß ich es nicht. Sie sind sehr mächtig, und es kann argumentiert werden, dass sie für eine Sprache wie JavaScript zu mächtig sind.
Ich denke, dass sie für Sprachen sehr nützlich sein könnten, in denen Mutabilität selten ist und in denen die Standardbibliothek Effekte vollständig unterstützt. Wenn Sie zuerst perform Timeout(1000), perform Fetch('http://google.com')
und perform ReadFile('file.txt')
und Ihre Sprache dann über "Pattern Matching" und statische Typisierung für Effekte verfügt Dies kann eine sehr schöne Programmierumgebung sein.
Vielleicht wird diese Sprache sogar in JavaScript kompiliert!
Was hat das mit React zu tun?
Nicht sehr groß. Man kann sogar sagen, dass ich eine Eule auf einen Globus ziehe.
Wenn Sie meinen Vortrag über Time Slicing und Suspense gesehen haben, enthält der zweite Teil Komponenten, die Daten aus dem Cache lesen:
function MovieDetails({ id }) {
(Der Bericht verwendet eine etwas andere API, aber das ist nicht der Punkt.)
Dieser Code basiert auf der React-Funktion für Datenproben namens „ Suspense
“, die derzeit aktiv entwickelt wird. Das Interessante dabei ist natürlich, dass sich die Daten möglicherweise noch nicht im movieCache befinden. In diesem Fall müssen wir zuerst etwas tun, da wir die Ausführung nicht fortsetzen können. Technisch gesehen löst in diesem Fall der Aufruf von read () Promise aus (ja, Promise werfen - Sie müssen diese Tatsache schlucken). Dies unterbricht die Ausführung. React fängt dieses Versprechen ab und erinnert sich, dass das Rendern des Komponentenbaums wiederholt werden muss, nachdem das ausgelöste Versprechen erfüllt wurde.
Dies ist an sich kein algebraischer Effekt, obwohl die Erstellung dieses Tricks von ihnen inspiriert wurde . Mit diesem Trick wird das gleiche Ziel erreicht: Ein Teil des folgenden Codes im Aufrufstapel ist vorübergehend einem höheren Wert im Aufrufstapel unterlegen (in diesem Fall Reagieren), während nicht alle Zwischenfunktionen davon wissen oder durch Asynchronität oder Generatoren „vergiftet“ werden müssen. Natürlich können wir die Ausführung in JavaScript nicht "tatsächlich" fortsetzen, aber aus Sicht von React ist die erneute Anzeige des Komponentenbaums nach der Promise-Berechtigung fast identisch. Sie können betrügen, wenn Ihr Programmiermodell Idempotenz annimmt!
Hooks sind ein weiteres Beispiel, das Sie an algebraische Effekte erinnern kann. Eine der ersten Fragen ist: Woher weiß der useState-Aufruf, auf welche Komponente er sich bezieht?
function LikeButton() {
Ich habe dies bereits am Ende dieses Artikels erklärt : Im React-Objekt befindet sich ein veränderbarer Status "Current Dispatcher", der die Implementierung angibt, die Sie derzeit verwenden (z. B. in React react-dom
). Ebenso gibt es eine aktuelle Komponenteneigenschaft, die auf die interne Datenstruktur von LikeButton verweist. So findet useState heraus, was zu tun ist.
Bevor man sich daran gewöhnt, denken die Leute oft, dass es aus einem offensichtlichen Grund wie ein schmutziger Hack aussieht. Es ist falsch, sich auf einen allgemeinen veränderlichen Zustand zu verlassen. (Hinweis: Wie wird try / catch Ihrer Meinung nach in der JavaScript-Engine implementiert?)
Konzeptionell können Sie useState () jedoch als Effekt der Ausführung von State () betrachten, das von React verarbeitet wird, wenn Ihre Komponente ausgeführt wird. Dies „erklärt“, warum React (was Ihre Komponente aufruft) den Status bereitstellen kann (er ist höher im Aufrufstapel, sodass er einen Effekthandler bereitstellen kann). In der Tat ist die explizite Zustandsimplementierung eines der häufigsten Beispiele in Lehrbüchern über algebraische Effekte, auf die ich gestoßen bin.
Auch hier funktioniert React natürlich nicht so, da wir in JavaScript keine algebraischen Effekte haben. Stattdessen gibt es ein verstecktes Feld, in dem wir die aktuelle Komponente speichern, sowie ein Feld, das auf den aktuellen "Dispatcher" mit der useState-Implementierung verweist. Zur Leistungsoptimierung gibt es sogar separate useState-Implementierungen für Bereitstellungen und Aktualisierungen . Wenn Sie jetzt von diesem Code sehr verdreht sind, können Sie sie als normale Effekt-Handler betrachten.
Zusammenfassend können wir sagen, dass throw
in JavaScript als erste Annäherung für E / A-Effekte dienen kann (vorausgesetzt, der Code kann später sicher erneut ausgeführt werden und solange er nicht an die CPU gebunden ist), und das Variablenfeld „ Der in try / finally wiederhergestellte Dispatcher kann als grobe Annäherung für Synchroneffekt-Handler dienen.
Mit Generatoren können Sie eine viel qualitativ hochwertigere Implementierung von Effekten erzielen. Dies bedeutet jedoch, dass Sie die "transparente" Natur von JavaScript-Funktionen aufgeben und alles mit Generatoren tun müssen. Und das ist "gut, das ..."
Wo kann man mehr erfahren?
Persönlich war ich überrascht, wie viel Sinn algebraische Effekte für mich erlangt haben. Ich habe immer mein Bestes versucht, um abstrakte Konzepte wie Monaden zu verstehen, aber algebraische Effekte wurden einfach im Kopf aufgenommen und „eingeschaltet“. Ich hoffe, dass dieser Artikel ihnen hilft, mit Ihnen „mitzumachen“.
Ich weiß nicht, ob sie jemals in großen Mengen verwendet werden. Ich denke, dass ich enttäuscht sein werde, wenn sie bis 2025 in keiner der Hauptsprachen Wurzeln schlagen. Erinnern Sie mich daran, in fünf Jahren einzuchecken!
Ich bin sicher, dass Sie mit ihnen viel interessanter werden können, aber es ist wirklich schwierig, ihre Stärke zu spüren, bis Sie anfangen, Code zu schreiben und sie zu verwenden. Wenn dieser Beitrag Ihre Neugier geweckt hat, finden Sie hier einige weitere Ressourcen, die Sie ausführlicher lesen können:
Viele Leute haben auch darauf hingewiesen, dass, wenn Sie den Tippaspekt weglassen (wie ich es in diesem Artikel getan habe), Sie eine frühere Verwendung einer solchen Technik in einem Bedingungssystem in Common Lisp finden können. , , call/cc .