Elegante JavaScript-Fehlerbehandlung mit der Entweder-Monade

Lassen Sie uns ein wenig darüber sprechen, wie wir mit Fehlern umgehen. In JavaScript verfügen wir über eine integrierte Sprachfunktion zum Arbeiten mit Ausnahmen. Wir fügen den problematischen Code in das Konstrukt try...catch . Auf diese Weise können Sie im try Abschnitt einen normalen Ausführungspfad angeben und dann alle Ausnahmen im catch Abschnitt behandeln. Keine schlechte Option. So können Sie sich auf die aktuelle Aufgabe konzentrieren, ohne über jeden möglichen Fehler nachzudenken. Auf jeden Fall besser, als Ihren Code mit endlosen Wenns zu verstopfen.

Ohne try...catch es schwierig, die Ergebnisse jedes Funktionsaufrufs auf unerwartete Werte zu überprüfen. Dies ist ein nützliches Design. Aber sie hat bestimmte Probleme. Und dies ist nicht die einzige Möglichkeit, mit Fehlern umzugehen. In diesem Artikel werden wir uns die Verwendung der Entweder-Monade als Alternative ansehen, um zu try...catch .

Bevor ich fortfahre, stelle ich einige Punkte fest. In dem Artikel wird davon ausgegangen, dass Sie bereits über Funktionszusammensetzung und Currying Bescheid wissen. Und eine Warnung. Wenn Sie noch nie auf Monaden gestoßen sind, können sie wirklich ... seltsam wirken. Die Arbeit mit solchen Werkzeugen erfordert ein Umdenken. Zuerst kann es schwer sein.

Machen Sie sich keine Sorgen, wenn Sie sofort verwirrt sind. Jeder hat es. Am Ende des Artikels habe ich einige Links aufgelistet, die helfen könnten. Gib nicht auf. Diese Dinge werden berauscht, sobald sie das Gehirn durchdringen.

Problembeispiel


Bevor wir die Probleme von Ausnahmen diskutieren, lassen Sie uns darüber sprechen, warum sie überhaupt existieren und warum try...catch erschienen. Schauen wir uns dazu ein Problem an, das ich zumindest teilweise realistisch machen wollte. Stellen Sie sich vor, wir schreiben eine Funktion zum Anzeigen einer Liste von Benachrichtigungen. Wir haben es (irgendwie) bereits geschafft, Daten vom Server zurückzugeben. Aus irgendeinem Grund haben die Backend-Ingenieure beschlossen, es im CSV-Format und nicht im JSON-Format zu senden. Rohdaten könnten ungefähr so ​​aussehen:

  Zeitstempel, Inhalt, angezeigt, href
 2018-10-27T05: 33: 34 + 00: 00, @ madhatter hat Sie zum Tee eingeladen, ungelesen, https: //example.com/invite/tea/3801
 2018-10-26T13: 47: 12 + 00: 00, @ Queenofhearts hat Sie in der Diskussion "Krocket-Turnier" erwähnt, angezeigt unter https: //example.com/discussions/croquet/1168
 2018-10-25T03: 50: 08 + 00: 00, @ cheshirecat hat Ihnen ein ungelesenes Grinsen geschickt, https: //example.com/interactions/grin/88 

Wir wollen es in HTML anzeigen. Es könnte ungefähr so ​​aussehen:

 <ul class="MessageList"> <li class="Message Message--viewed"> <a href="https://example.com/invite/tea/3801" class="Message-link">@madhatter invited you to tea</a> <time datetime="2018-10-27T05:33:34+00:00">27 October 2018</time> <li> <li class="Message Message--viewed"> <a href="https://example.com/discussions/croquet/1168" class="Message-link">@queenofhearts mentioned you in 'Croquet Tournament' discussion</a> <time datetime="2018-10-26T13:47:12+00:00">26 October 2018</time> </li> <li class="Message Message--viewed"> <a href="https://example.com/interactions/grin/88" class="Message-link">@cheshirecat sent you a grin</a> <time datetime="2018-10-25T03:50:08+00:00">25 October 2018</time> </li> </ul> 

Um die Aufgabe zu vereinfachen, konzentrieren Sie sich vorerst nur auf die Verarbeitung jeder Zeile von CSV-Daten. Beginnen wir mit einigen einfachen Funktionen zum Verarbeiten von Zeichenfolgen. Die erste unterteilt die Textzeichenfolge in Felder:

 function splitFields(row) { return row.split('","'); } 

Die Funktion wird hier vereinfacht, da es sich um Lehrmaterial handelt. Wir beschäftigen uns mit der Fehlerbehandlung, nicht mit der CSV-Analyse. Wenn eine der Nachrichten ein Komma enthält, ist dies alles furchtbar falsch. Bitte verwenden Sie diesen Code niemals, um echte CSV-Daten zu analysieren. Wenn Sie jemals CSV-Daten analysieren mussten, verwenden Sie die bewährte CSV-Parsing-Bibliothek .

Nach dem Aufteilen der Daten möchten wir ein Objekt erstellen. Und damit jeder Eigenschaftsname mit den CSV-Headern übereinstimmt. Angenommen, wir haben die Titelleiste bereits irgendwie analysiert (dazu später mehr). Wir sind an einem Punkt angelangt, an dem etwas schief gehen kann. Wir haben einen Fehler bei der Verarbeitung erhalten. Wir werfen einen Fehler aus, wenn die Länge der Zeichenfolge nicht mit der Titelleiste übereinstimmt. ( _.zipObject ist eine lodash-Funktion ).

 function zipRow(headerFields, fieldData) { if (headerFields.length !== fieldData.length) { throw new Error("Row has an unexpected number of fields"); } return _.zipObject(headerFields, fieldData); } 

Fügen Sie danach dem Objekt ein für Menschen lesbares Datum hinzu, um es in unserer Vorlage anzuzeigen. Es stellte sich als etwas ausführlich heraus, da JavaScript keine perfekte integrierte Unterstützung für die Datumsformatierung bietet. Und wieder stehen wir vor potenziellen Problemen. Wenn ein ungültiges Datum festgestellt wird, gibt unsere Funktion einen Fehler aus.

 function addDateStr(messageObj) { const errMsg = 'Unable to parse date stamp in message object'; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const d = new Date(messageObj.datestamp); if (isNaN(d)) { throw new Error(errMsg); } const datestr = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`; return {datestr, ...messageObj}; } 

Nehmen Sie zum Schluss das Objekt und übergeben Sie es der Vorlagenfunktion , um die HTML-Zeichenfolge abzurufen.

 const rowToMessage = _.template(`<li class="Message Message--<%= viewed %>"> <a href="<%= href %>" class="Message-link"><%= content %></a> <time datetime="<%= datestamp %>"><%= datestr %></time> <li>`); 

Es wäre auch schön, einen Fehler auszudrucken, wenn er erfüllt wäre:

 const showError = _.template(`<li class="Error"><%= message %></li>`); 

Wenn alles vorhanden ist, können Sie eine Funktion zusammenstellen, um jede Zeile zu verarbeiten.

 function processRow(headerFieldNames, row) { try { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); rowObjWithDate = addDateStr(rowObj); return rowToMessage(rowObj); } catch(e) { return showError(e); } } 

Damit ist die Funktion fertig. Schauen wir uns genauer an, wie Ausnahmen behandelt werden.

Ausnahmen: der gute Teil


Also, was ist gut daran, zu try...catch ? Es ist zu beachten, dass im obigen Beispiel jeder der Schritte im try Block einen Fehler verursachen kann. In zipRow() und addDateStr() werfen wir absichtlich Fehler aus. Und wenn ein Problem auftritt, fangen Sie einfach den Fehler ab und zeigen Sie eine Meldung auf der Seite an. Ohne diesen Mechanismus wird der Code wirklich hässlich. So könnte es aussehen. Angenommen, die Funktionen werfen keine Fehler aus, sondern geben null .

 function processRowWithoutExceptions(headerFieldNames, row) { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); if (rowObj === null) { return showError(new Error('Encountered a row with an unexpected number of items')); } rowObjWithDate = addDateStr(rowObj); if (rowObjWithDate === null) { return showError(new Error('Unable to parse date in row object')); } return rowToMessage(rowObj); } 

Wie Sie sehen können, eine große Anzahl von Vorlagen, if . Der Code ist ausführlicher. Und es ist schwer, der Grundlogik zu folgen. Außerdem sagt uns null nicht viel. Wir wissen nicht genau, warum der vorherige Funktionsaufruf fehlgeschlagen ist. Wir müssen raten. Wir erstellen eine Fehlermeldung und rufen showError() . Ein solcher Code ist schmutziger und verwirrender.

Schauen Sie sich noch einmal die Version zur Ausnahmebehandlung an. Der erfolgreiche Pfad des Programms und der Code für die Ausnahmebehandlung werden klar voneinander getrennt. Der try Zweig ist ein guter Weg, und der catch Zweig ist ein Fehler. Die gesamte Ausnahmebehandlung erfolgt an einem Ort. Und einzelne Funktionen können melden, warum sie fehlgeschlagen sind. Alles in allem scheint das ziemlich süß. Ich denke, dass die Mehrheit das erste Beispiel für durchaus geeignet hält. Warum ein anderer Ansatz?

Probleme beim Umgang mit Ausnahmen versuchen ... fangen


Mit diesem Ansatz können Sie diese lästigen Fehler ignorieren. Leider try...catch macht seinen Job zu gut. Sie werfen einfach eine Ausnahme und fahren fort. Wir können ihn später fangen. Und jeder hat wirklich vor, immer solche Blöcke zu setzen. Es ist jedoch nicht immer offensichtlich, wo der Fehler weitergeht. Und der Block ist zu leicht zu vergessen. Und bevor Sie dies bemerken, stürzt Ihre Anwendung ab.

Außerdem verschmutzen Ausnahmen den Code. Wir werden hier nicht im Detail auf funktionale Reinheit eingehen. Aber schauen wir uns einen kleinen Aspekt der funktionalen Reinheit an: referentielle Transparenz. Eine linktransparente Funktion gibt für eine bestimmte Eingabe immer das gleiche Ergebnis zurück. Aber für Funktionen mit Ausnahmen können wir das nicht sagen. Sie können jederzeit eine Ausnahme auslösen, anstatt einen Wert zurückzugeben. Dies verkompliziert die Logik. Aber was ist, wenn Sie eine Win-Win-Option finden - eine saubere Möglichkeit, mit Fehlern umzugehen?

Wir haben eine Alternative gefunden


Reine Funktionen geben immer einen Wert zurück (auch wenn dieser Wert fehlt). Daher sollte unser Fehlerbehandlungscode davon ausgehen, dass wir immer einen Wert zurückgeben. Was soll ich also als ersten Versuch tun, wenn wir bei einem Fehler ein Fehlerobjekt zurückgeben? Das heißt, wo immer wir einen Fehler machen, geben wir ein solches Objekt zurück. Es könnte ungefähr so ​​aussehen:

 function processRowReturningErrors(headerFieldNames, row) { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); if (rowObj instanceof Error) { return showError(rowObj); } rowObjWithDate = addDateStr(rowObj); if (rowObjWithDate instanceof Error) { return showError(rowObjWithDate); } return rowToMessage(rowObj); } 

Dies ist ausnahmslos kein spezielles Upgrade. Aber es ist besser. Wir haben die Verantwortung für Fehlermeldungen wieder auf einzelne Funktionen übertragen. Aber wir haben immer noch all diese Wenns. Es wäre schön, die Vorlage irgendwie zu kapseln. Mit anderen Worten, wenn wir wissen, dass wir einen Fehler haben, machen Sie sich keine Sorgen um den Rest des Codes.

Polymorphismus


Wie kann man das machen? Dies ist ein schwieriges Problem. Aber es kann mit Hilfe der Magie des Polymorphismus gelöst werden. Machen Sie sich keine Sorgen, wenn Sie noch nie auf Polymorphismus gestoßen sind. Im Wesentlichen handelt es sich um „eine einzige Schnittstelle für Entitäten unterschiedlicher Typen“ (Straustrup, B. „C ++ - Glossar von Björn Straustrup“). In JavaScript bedeutet dies, dass wir Objekte mit denselben benannten Methoden und Signaturen erstellen. Aber anderes Verhalten. Ein klassisches Beispiel ist die Anwendungsprotokollierung. Wir können unsere Magazine je nach Umgebung an verschiedene Orte senden. Was ist, wenn wir zum Beispiel zwei Logger-Objekte erstellen?

 const consoleLogger = { log: function log(msg) { console.log('This is the console logger, logging:', msg); } }; const ajaxLogger = { log: function log(msg) { return fetch('https://example.com/logger', {method: 'POST', body: msg}); } }; 

Beide Objekte definieren eine Protokollfunktion, die einen einzelnen Zeichenfolgenparameter erwartet. Aber sie verhalten sich anders. Das Schöne ist, dass wir Code schreiben können, der .log() , unabhängig davon, welches Objekt verwendet wird. Es kann consoleLogger oder ajaxLogger . Alles funktioniert trotzdem. Der folgende Code funktioniert beispielsweise mit jedem Objekt gleich gut:

 function log(logger, message) { logger.log(message); } 

Ein weiteres Beispiel ist die .toString() -Methode für alle JS-Objekte. Wir können die .toString() -Methode für jede Klasse schreiben, die wir erstellen. Als Nächstes können Sie zwei Klassen erstellen, die die Methode .toString() unterschiedlich implementieren. Wir werden sie Left und Right benennen (etwas später werde ich die Namen erklären).

 class Left { constructor(val) { this._val = val; } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 class Right { constructor(val) { this._val = val; } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

Erstellen Sie nun eine Funktion, die .toString() für diese beiden Objekte aufruft:

 function trace(val) { console.log(val.toString()); return val; } trace(new Left('Hello world')); // ⦘ Left(Hello world) trace(new Right('Hello world')); // ⦘ Right(Hello world); 

Kein herausragender Code, ich weiß. Tatsache ist jedoch, dass wir zwei verschiedene Verhaltensweisen haben, die dieselbe Schnittstelle verwenden. Das ist Polymorphismus. Aber achten Sie auf etwas Interessantes. Wie viele if-Anweisungen haben wir verwendet? Null Keiner. Wir haben zwei verschiedene Verhaltensweisen ohne eine einzige if-Anweisung erstellt. Vielleicht kann so etwas verwendet werden, um Fehler zu behandeln ...

Links und rechts


Zurück zu unserem Problem. Es ist notwendig, den erfolgreichen und erfolglosen Pfad für unseren Code zu bestimmen. Auf einem guten Weg führen wir den Code einfach ruhig weiter aus, bis ein Fehler auftritt oder wir ihn beenden. Wenn wir uns auf dem falschen Weg befinden, werden wir nicht länger versuchen, den Code auszuführen. Wir könnten diese Pfade Happy und Sad nennen, aber versuchen, die Namenskonventionen zu befolgen, die andere Programmiersprachen und Bibliotheken verwenden. Nennen wir also den schlechten Weg links und den erfolgreichen - rechts.

Lassen Sie uns eine Methode erstellen, die die Funktion ausführt, wenn wir uns auf einem guten Pfad befinden, sie jedoch auf einem schlechten Pfad ignorieren:

 /** * Left represents the sad path. */ class Left { constructor(val) { this._val = val; } runFunctionOnlyOnHappyPath() { // Left is the sad path. Do nothing } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 /** * Right represents the happy path. */ class Right { constructor(val) { this._val = val; } runFunctionOnlyOnHappyPath(fn) { return fn(this._val); } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

So etwas wie das:

 const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); leftHello.runFunctionOnlyOnHappyPath(trace); // does nothing rightHello.runFunctionOnlyOnHappyPath(trace); // ⦘ Hello world // ← "Hello world" 

Sendung


Wir nähern uns etwas Nützlichem, aber noch nicht ganz. Unsere .runFunctionOnlyOnHappyPath() -Methode gibt die _val Eigenschaft zurück. Alles ist in Ordnung, aber zu unpraktisch, wenn wir mehr als eine Funktion ausführen möchten. Warum? Weil wir nicht mehr wissen, ob wir auf dem richtigen oder dem falschen Weg sind. Informationen verschwinden, sobald wir den Wert außerhalb von Links und Rechts annehmen. Wir können also den linken oder rechten Pfad mit dem neuen _val . Und wir werden den Namen verkürzen, da wir hier sind. Wir übersetzen eine Funktion aus der Welt der einfachen Werte in die Welt der Linken und Rechten. Deshalb rufen wir die map() -Methode auf:

 /** * Left represents the sad path. */ class Left { constructor(val) { this._val = val; } map() { // Left is the sad path // so we do nothing return this; } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 /** * Right represents the happy path */ class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

Wir fügen diese Methode ein und verwenden Links oder Rechts in der freien Syntax:

 const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); const helloToGreetings = str => str.replace(/Hello/, 'Greetings,'); leftHello.map(helloToGreetings).map(trace); // Doesn't print any thing to the console // ← Left(Hello world) rightHello.map(helloToGreetings).map(trace); // ⦘ Greetings, world // ← Right(Greetings, world) 

Wir haben zwei Ausführungspfade erstellt. Wir können die Daten auf einen erfolgreichen Pfad setzen, indem wir new Right() aufrufen, oder auf einen fehlgeschlagenen Pfad, indem wir new Left() aufrufen.


Jede Klasse repräsentiert einen Pfad: erfolgreich oder erfolglos. Ich habe diese Eisenbahnmetapher von Scott Vlaschina gestohlen

Wenn die map auf einem guten Pfad funktioniert hat, gehen Sie diesen entlang und verarbeiten Sie die Daten. Wenn wir keinen Erfolg haben, wird nichts passieren. Übergeben Sie den Wert einfach weiter. Wenn wir zum Beispiel Fehler auf diesen erfolglosen Pfad setzen, erhalten wir etwas sehr Ähnliches, try…catch zu try…catch .


Verwenden Sie .map() , um sich entlang des Pfads zu bewegen

Im weiteren Verlauf wird es immer schwieriger, Links oder Rechts zu schreiben. Nennen wir diese Kombination also einfach Entweder („entweder“). Entweder links oder rechts.

Verknüpfungen zum Erstellen von Objekten


Der nächste Schritt besteht also darin, unsere Beispielfunktionen so umzuschreiben, dass sie entweder zurückgeben. Links für Fehler oder rechts für Wert. Aber bevor wir das tun, haben Sie Spaß. Schreiben wir ein paar Abkürzungen. Die erste ist eine statische Methode namens .of() . Es wird nur ein neues Links oder Rechts zurückgegeben. Der Code könnte folgendermaßen aussehen:

 Left.of = function of(x) { return new Left(x); }; Right.of = function of(x) { return new Right(x); }; 

Ehrlich gesagt ist sogar Left.of() und Right.of() mühsam zu schreiben. Also neige ich zu noch kürzeren left() und right() Labels:

 function left(x) { return Left.of(x); } function right(x) { return Right.of(x); } 

Mit diesen Verknüpfungen beginnen wir, die Anwendungsfunktionen neu zu schreiben:

 function zipRow(headerFields, fieldData) { const lengthMatch = (headerFields.length == fieldData.length); return (!lengthMatch) ? left(new Error("Row has an unexpected number of fields")) : right(_.zipObject(headerFields, fieldData)); } function addDateStr(messageObj) { const errMsg = 'Unable to parse date stamp in message object'; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const d = new Date(messageObj.datestamp); if (isNaN(d)) { return left(new Error(errMsg)); } const datestr = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`; return right({datestr, ...messageObj}); } 

Geänderte Funktionen unterscheiden sich nicht so sehr von den alten. Wir setzen den Rückgabewert einfach entweder in Links oder Rechts, je nachdem, ob ein Fehler vorliegt.

Danach können wir mit der Verarbeitung der Hauptfunktion beginnen, die eine Zeile verarbeitet. splitFields Sie zunächst die Zeichenfolge mit right() in Entweder ein und übersetzen splitFields dann splitFields , um sie zu teilen:

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); // … } 

Dies funktioniert zipRow() , aber das zipRow() , wenn Sie versuchen, dasselbe mit zipRow() zu tun:

  function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow /* wait. this isn't right */); // ... } 

Tatsache ist, dass zipRow() zwei Parameter erwartet. Die Funktionen, die wir an .map() erhalten jedoch nur einen Wert aus der Eigenschaft ._val . Die Situation kann mit der zipRow() Version von zipRow() korrigiert werden. Es könnte ungefähr so ​​aussehen:

 function zipRow(headerFields) { return function zipRowWithHeaderFields(fieldData) { const lengthMatch = (headerFields.length == fieldData.length); return (!lengthMatch) ? left(new Error("Row has an unexpected number of fields")) : right(_.zipObject(headerFields, fieldData)); }; } 

Diese kleine Änderung vereinfacht die Konvertierung von zipRow , sodass sie mit .map() gut .map() :

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields)); // ... But now we have another problem ... } 

Mitmachen


Die Verwendung von .map() zum Ausführen von splitFields() ist in Ordnung, da .splitFields() auch nicht .splitFields() . Wenn Sie jedoch zipRow() ausführen zipRow() , tritt ein Problem auf, da es entweder zurückgibt. Wenn wir also .map() verwenden, .map() wir in .map() entweder. Wenn wir weiter gehen, bleiben Sie stecken, bis wir .map() in .map() . Das wird auch nicht funktionieren. Wir brauchen eine Möglichkeit, diese verschachtelten Entweder zu kombinieren. Schreiben wir also eine neue Methode, die wir .join() :

 /** *Left represents the sad path. */ class Left { constructor(val) { this._val = val; } map() { // Left is the sad path // so we do nothing return this; } join() { // On the sad path, we don't // do anything with join return this; } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 /** * Right represents the happy path */ class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } join() { if ((this._val instanceof Left) || (this._val instanceof Right)) { return this._val; } return this; } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

Jetzt können wir unser Vermögen „auspacken“:

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields)).join(); const rowObjWithDate = rowObj.map(addDateStr).join(); // Slowly getting better... but what do we return? } 

Kette


Wir haben einen langen Weg zurückgelegt. Aber Sie müssen sich die .join() Zeit an den Aufruf von .join() erinnern, was ärgerlich ist. Wir haben jedoch ein gemeinsames aufeinanderfolgendes .map() und .join() . Erstellen wir also eine Schnellzugriffsmethode dafür. Nennen wir es chain() , weil es Funktionen miteinander verbindet, die Left oder Right zurückgeben.

 /** *Left represents the sad path. */ class Left { constructor(val) { this._val = val; } map() { // Left is the sad path // so we do nothing return this; } join() { // On the sad path, we don't // do anything with join return this; } chain() { // Boring sad path, // do nothing. return this; } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 /** * Right represents the happy path */ class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } join() { if ((this._val instanceof Left) || (this._val instanceof Right)) { return this._val; } return this; } chain(fn) { return fn(this._val); } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

Zurück zur Eisenbahnanalogie: .chain() wechselt die Schienen, wenn ein Fehler .chain() . Es ist jedoch einfacher, auf dem Diagramm zu zeigen.


Wenn ein Fehler auftritt, können Sie mit der .chain () -Methode zum linken Pfad wechseln. Bitte beachten Sie, dass die Schalter nur in eine Richtung funktionieren.

Der Code wurde etwas sauberer:

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.chain(zipRow(headerFields)); const rowObjWithDate = rowObj.chain(addDateStr); // Slowly getting better... but what do we return? } 

Mach etwas mit Werten


Das Refactoring der processRow() -Funktion ist fast abgeschlossen. Aber was passiert, wenn wir den Wert zurückgeben? Am Ende wollen wir verschiedene Maßnahmen ergreifen, je nachdem, in welcher Situation wir uns befinden: Links oder Rechts. Daher werden wir eine Funktion schreiben, die geeignete Maßnahmen ergreift:

 function either(leftFunc, rightFunc, e) { return (e instanceof Left) ? leftFunc(e._val) : rightFunc(e._val); } 

Ich habe betrogen und die internen Werte von linken oder rechten Objekten verwendet. Aber tun Sie so, als hätten Sie das nicht bemerkt. Jetzt können wir unsere Funktion erfüllen:

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.chain(zipRow(headerFields)); const rowObjWithDate = rowObj.chain(addDateStr); return either(showError, rowToMessage, rowObjWithDate); } 

Und wenn wir uns besonders schlau fühlen, können wir wieder die freie Syntax verwenden:

 function processRow(headerFields, row) { const rowObjWithDate = right(row) .map(splitFields) .chain(zipRow(headerFields)) .chain(addDateStr); return either(showError, rowToMessage, rowObjWithDate); } 

Beide Versionen sind ziemlich hübsch. Keine Designs try...catch. Und keine if-Anweisungen in der Top-Level-Funktion. Wenn es ein Problem mit einer bestimmten Zeile gibt, wird am Ende einfach eine Fehlermeldung angezeigt. Und beachten Sie, dass processRow()wir links oder rechts das einzige Mal ganz am Anfang erwähnen, wenn wir anrufen right(). Der Rest sind nur Methoden .map()und .chain()für die nächste Funktion.

ap und heben


Es sieht gut aus, aber es bleibt noch ein letztes Szenario zu betrachten. Lassen Sie uns anhand unseres Beispiels sehen, wie alle CSV-Daten und nicht nur jede Zeile einzeln verarbeitet werden können. Wir benötigen eine oder drei Hilfsfunktionen (Helfer):

 function splitCSVToRows(csvData) { // There should always be a header row... so if there's no // newline character, something is wrong. return (csvData.indexOf('\n') < 0) ? left('No header row found in CSV data') : right(csvData.split('\n')); } function processRows(headerFields, dataRows) { // Note this is Array map, not Either map. return dataRows.map(row => processRow(headerFields, row)); } function showMessages(messages) { return `<ul class="Messages">${messages.join('\n')}</ul>`; } 

Wir haben also einen Helfer, der CSV in Zeilen aufteilt. Und wir kehren mit Entweder zu der Option zurück. Jetzt können Sie .map()einige lodash-Funktionen verwenden, um die Titelleiste aus Datenzeilen zu extrahieren. Aber wir befinden uns in einer interessanten Situation ...

 function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); // What's next? } 

Wir haben Header-Felder und Datenzeilen zur Anzeige bereit processRows(). Aber headerFieldsauch dataRowsin Entweder eingewickelt. Wir brauchen eine Möglichkeit, processRows()in eine Funktion zu konvertieren , die mit Entweder funktioniert. Zu Beginn führen wir Currying durch processRows.

 function processRows(headerFields) { return function processRowsWithHeaderFields(dataRows) { // Note this is Array map, not Either map. return dataRows.map(row => processRow(headerFields, row)); }; } 

Jetzt ist alles bereit für das Experiment. Wir haben headerFieldsentweder ein Array umwickelt. Was passiert , wenn wir nehmen headerFieldsund fordern ihn .map()auf processRows()?

 function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); // How will we pass headerFields and dataRows to // processRows() ? const funcInEither = headerFields.map(processRows); } 

Mit .map () wird hier eine externe Funktion aufgerufen processRows(), aber keine interne. Mit anderen Worten, processRows()gibt eine Funktion zurück. Und seitdem .map()bekommen wir entweder noch zurück. Das Ergebnis ist also eine Funktion in Entweder, die aufgerufen wird funcInEither. Es nimmt ein Array von Zeichenfolgen und gibt ein Array anderer Zeichenfolgen zurück. Wir müssen diese Funktion irgendwie übernehmen und sie mit einem Wert im Inneren aufrufen dataRows. Fügen Sie dazu unseren Klassen Links und Rechts eine weitere Methode hinzu. Wir werden es .ap()in Übereinstimmung mit dem Standard nennen .

Wie üblich macht die Methode nichts auf der linken Spur:

  // In Left (the sad path) ap() { return this; } 

Und für die richtige Klasse erwarten wir ein weiteres Entweder mit einer Funktion:

  // In Right (the happy path) ap(otherEither) { const functionToRun = otherEither._val; return this.map(functionToRun); } 

Jetzt können wir unsere Hauptfunktion erfüllen:

  function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); const funcInEither = headerFields.map(processRows); const messagesArr = dataRows.ap(funcInEither); return either(showError, showMessages, messagesArr); } 

Das Wesentliche der Methode wird .ap()sofort ein wenig verstanden (die Spezifikationen von Fantasy Land verwirren es, aber in den meisten anderen Sprachen wird die Methode umgekehrt verwendet). Wenn Sie es einfacher beschreiben, sagen Sie: „Ich habe eine Funktion, die normalerweise zwei einfache Werte annimmt. Ich möchte daraus eine Funktion machen, die entweder zwei benötigt. " Wenn verfügbar, können .ap()wir eine Funktion schreiben, die genau das tut. Nennen wir es liftA2()wieder in Übereinstimmung mit dem Standardnamen. Sie nimmt eine einfache Funktion, die zwei Argumente erwartet, und "hebt" sie auf, um mit "Applikativen" zu arbeiten. (Dies sind Objekte, die sowohl eine Methode .ap()als auch eine Methode enthalten. .of()) LiftA2 ist also die Abkürzung für "Applicative Lift, zwei Parameter".

Eine Funktion liftA2könnte also ungefähr so ​​aussehen:

 function liftA2(func) { return function runApplicativeFunc(a, b) { return b.ap(a.map(func)); }; } 

Unsere Top-Level-Funktion wird es wie folgt verwenden:

 function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); const processRowsA = liftA2(processRows); const messagesArr = processRowsA(headerFields, dataRows); return either(showError, showMessages, messagesArr); } 

Code auf CodePen .

Richtig? Das ist alles?


Sie fragen sich vielleicht, was ist besser als einfache Ausnahmen? Scheint es mir nicht zu kompliziert, ein einfaches Problem zu lösen? Lassen Sie uns zunächst darüber nachdenken, warum wir Ausnahmen mögen. Wenn es keine Ausnahmen gäbe, müssten Sie überall viele if-Anweisungen schreiben. Wir werden immer Code nach dem Prinzip schreiben: "Wenn letzteres funktioniert, fahren Sie fort, andernfalls verarbeiten Sie den Fehler." Und wir müssen diese Fehler im gesamten Code behandeln. Dies macht es schwierig zu verstehen, was passiert. Mit Ausnahmen können Sie das Programm beenden, wenn ein Fehler aufgetreten ist. Daher müssen Sie nicht alle diese ifs schreiben. Sie können sich auf einen erfolgreichen Ausführungspfad konzentrieren.

Aber es gibt einen Haken. Ausnahmen verbergen zu viel. Wenn Sie eine Ausnahme auslösen, übertragen Sie das Fehlerbehandlungsproblem auf eine andere Funktion. Es ist zu einfach, eine Ausnahme zu ignorieren, die auf der höchsten Ebene angezeigt wird. Das Schöne an beiden ist, dass Sie wie mit einer Ausnahme aus dem Hauptprogramm-Stream springen können. Und es funktioniert ehrlich. Sie erhalten entweder rechts oder links. Sie können nicht so tun, als wäre die Option Links unmöglich. Am Ende müssen Sie den Wert mit einem Aufruf wie herausziehen either().

Ich weiß, es klingt nach einer Art Komplexität. Aber schauen Sie sich den Code an, den wir geschrieben haben (keine Klassen, sondern Funktionen, die sie verwenden). Es gibt nicht viel Code für die Ausnahmebehandlung. Es fehlt fast, mit Ausnahme eines Anrufs either()am Ende csvToMessages()undprocessRow(). Das ist der springende Punkt. Mit Entweder haben Sie eine saubere Fehlerbehandlung, die nicht versehentlich vergessen werden kann. Stempeln Sie ohne Beides den Code durch und fügen Sie überall Polster hinzu.

Dies bedeutet nicht, dass Sie es niemals verwenden sollten try...catch. Manchmal ist es das richtige Werkzeug und es ist normal. Dies ist jedoch nicht das einzige Werkzeug. Entweder gibt Ihnen einige Vorteile, die Sie nicht haben try...catch. Geben Sie dieser Monade eine Chance. Auch wenn es zunächst schwierig ist, denke ich, dass es Ihnen gefallen wird. Bitte, bitte verwenden Sie nicht die Implementierung aus diesem Artikel. Probieren Sie eine der berühmten Bibliotheken wie Crocks , Sanctuary , Folktale oder Monet . Sie sind besser bedient. Und hier habe ich der Einfachheit halber etwas verpasst.

Zusätzliche Ressourcen


Source: https://habr.com/ru/post/de457098/


All Articles