Parlons un peu de la façon dont nous traitons les erreurs. En JavaScript, nous avons une fonction de langage intégrée pour travailler avec des exceptions. Nous enfermons le code problématique dans la construction
try...catch
. Cela vous permet de spécifier un chemin d'exécution normal dans la section
try
, puis de traiter toutes les exceptions dans la section
catch
. Pas une mauvaise option. Cela vous permet de vous concentrer sur la tâche en cours sans penser à toutes les erreurs possibles. Certainement mieux que de colmater votre code avec des ifs sans fin.
Sans
try...catch
il est difficile de vérifier les résultats de chaque appel de fonction pour des valeurs inattendues. Ceci est une conception utile. Mais elle a certains problèmes. Et ce n'est pas le seul moyen de gérer les erreurs. Dans cet article, nous verrons comment utiliser la
monade Either comme alternative pour
try...catch
.
Avant de poursuivre, je note quelques points. L'article suppose que vous connaissez déjà la composition des fonctions et le curry. Et un avertissement. Si vous n'avez jamais rencontré de monades auparavant, elles peuvent sembler vraiment ... étranges. Travailler avec de tels outils nécessite un changement de mentalité. Au début, cela peut être difficile.
Ne vous inquiétez pas si vous êtes immédiatement confus. Tout le monde l'a. À la fin de l'article, j'ai énuméré quelques liens qui pourraient vous aider. N'abandonnez pas. Ces choses s'enivrent dès qu'elles pénètrent dans le cerveau.
Exemple de problème
Avant de discuter des problèmes des exceptions, voyons pourquoi elles existent et pourquoi
try...catch
blocs
try...catch
sont apparus. Pour ce faire, regardons un problème que j'ai essayé de rendre au moins partiellement réaliste. Imaginez que nous écrivons une fonction pour afficher une liste de notifications. Nous avons déjà réussi (d'une manière ou d'une autre) à renvoyer des données du serveur. Mais pour une raison quelconque, les ingénieurs du backend ont décidé de l'envoyer au format CSV, pas JSON. Les données brutes peuvent ressembler à ceci:
horodatage, contenu, visualisé, href
2018-10-27T05: 33: 34 + 00: 00, @ madhatter vous a invité au thé, non lu, https: //example.com/invite/tea/3801
2018-10-26T13: 47: 12 + 00: 00, @ queenofhearts vous a mentionné dans la discussion sur le 'Tournoi de Croquet', consulté, https: //example.com/discussions/croquet/1168
2018-10-25T03: 50: 08 + 00: 00, @ cheshirecat vous a envoyé un sourire, non lu, https: //example.com/interactions/grin/88
Nous voulons l'afficher en HTML. Cela pourrait ressembler à ceci:
<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>
Pour simplifier la tâche, concentrez-vous simplement sur le traitement de chaque ligne de données CSV pour l'instant. Commençons par quelques fonctions simples pour le traitement des chaînes. Le premier divise la chaîne de texte en champs:
function splitFields(row) { return row.split('","'); }
La fonction est ici simplifiée car il s'agit de matériel pédagogique. Nous traitons la gestion des erreurs, pas l'analyse CSV. Si l'un des messages contient une virgule, tout cela sera terriblement faux. Veuillez ne jamais utiliser ce code pour analyser de vraies données CSV. Si vous avez déjà dû analyser des données CSV, utilisez la
bibliothèque d'analyse CSV bien testée .
Après avoir fractionné les données, nous voulons créer un objet. Et pour que chaque nom de propriété corresponde aux en-têtes CSV. Supposons que nous ayons déjà en quelque sorte analysé la barre de titre (plus à ce sujet plus tard). Nous sommes arrivés à un point où quelque chose peut mal tourner. Nous avons eu une erreur de traitement. Nous lançons une erreur si la longueur de la chaîne ne correspond pas à la barre de titre. (
_.zipObject
est
une fonction lodash ).
function zipRow(headerFields, fieldData) { if (headerFields.length !== fieldData.length) { throw new Error("Row has an unexpected number of fields"); } return _.zipObject(headerFields, fieldData); }
Après cela, ajoutez une date lisible par l'homme à l'objet afin de l'afficher dans notre modèle. Cela s'est avéré un peu bavard, car JavaScript n'a pas une prise en charge intégrée parfaite pour le formatage de la date. Et encore une fois, nous sommes confrontés à des problèmes potentiels. Si une date non valide est rencontrée, notre fonction génère une erreur.
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}; }
Enfin, prenez l'objet et passez-le dans
la fonction modèle pour obtenir la chaîne HTML.
const rowToMessage = _.template(`<li class="Message Message--<%= viewed %>"> <a href="<%= href %>" class="Message-link"><%= content %></a> <time datetime="<%= datestamp %>"><%= datestr %></time> <li>`);
Il serait également intéressant d'imprimer une erreur si elle se rencontrait:
const showError = _.template(`<li class="Error"><%= message %></li>`);
Lorsque tout est en place, vous pouvez créer une fonction pour traiter chaque ligne.
function processRow(headerFieldNames, row) { try { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); rowObjWithDate = addDateStr(rowObj); return rowToMessage(rowObj); } catch(e) { return showError(e); } }
La fonction est donc prête. Examinons de plus près comment il gère les exceptions.
Exceptions: la bonne partie
Alors, qu'est-ce qui est bon d'
try...catch
? Il convient de noter que dans l'exemple ci-dessus, l'une des étapes du bloc
try
peut provoquer une erreur. Dans
zipRow()
et
addDateStr()
nous lançons intentionnellement des erreurs. Et si un problème survient, attrapez simplement l'erreur et affichez n'importe quel message sur la page. Sans ce mécanisme, le code devient vraiment moche. Voici à quoi cela pourrait ressembler. Supposons que les fonctions ne génèrent pas d'erreurs, mais renvoient
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); }
Comme vous pouvez le voir, un grand nombre de modèles
if
. Le code est plus détaillé. Et il est difficile de suivre la logique de base. De plus,
null
ne nous dit pas grand-chose. Nous ne savons pas vraiment pourquoi l’appel de fonction précédent a échoué. Nous devons deviner. Nous créons un message d'erreur et appelons
showError()
. Un tel code est plus sale et plus déroutant.
Regardez à nouveau la version de gestion des exceptions. Il sépare clairement le chemin d'accès réussi du programme et le code de gestion des exceptions. La branche
try
est un bon moyen, et la branche
catch
est une erreur. Toute gestion des exceptions se produit en un seul endroit. Et les fonctions individuelles peuvent signaler pourquoi elles ont échoué. Dans l'ensemble, cela semble assez doux. Je pense que la majorité considère que le premier exemple est tout à fait approprié. Pourquoi une approche différente?
Problèmes de gestion des exceptions, essayez ... catch
Cette approche vous permet d'ignorer ces erreurs gênantes. Malheureusement,
try...catch
fait trop bien son travail. Vous jetez simplement une exception et passez à autre chose. Nous pouvons l'attraper plus tard. Et tout le monde a l'intention de toujours mettre de tels blocs, vraiment. Mais il n'est pas toujours évident où l'erreur va plus loin. Et le bloc est trop facile à oublier. Et avant de vous en rendre compte, votre application se bloque.
De plus, des exceptions polluent le code. Nous ne discuterons pas en détail de la pureté fonctionnelle ici. Mais regardons un petit aspect de la pureté fonctionnelle: la transparence référentielle. Une fonction transparente de lien renvoie toujours le même résultat pour une entrée particulière. Mais pour les fonctions avec des exceptions, nous ne pouvons pas dire cela. Ils peuvent lever une exception à tout moment au lieu de renvoyer une valeur. Cela complique la logique. Mais que faire si vous trouvez une option gagnant-gagnant - une façon propre de gérer les erreurs?
Nous trouvons une alternative
Les fonctions pures renvoient toujours une valeur (même si cette valeur est manquante). Par conséquent, notre code de gestion des erreurs doit supposer que nous renvoyons toujours une valeur. Donc, comme première tentative, que dois-je faire si, en cas d'échec, nous renvoyons un objet Error? Autrement dit, partout où nous commettons une erreur, nous renvoyons un tel objet. Cela pourrait ressembler à ceci:
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); }
Ce n'est pas une mise à niveau spéciale sans exception. Mais c’est mieux. Nous avons transféré la responsabilité des messages d'erreur aux fonctions individuelles. Mais nous avons toujours tous ces ifs. Ce serait bien d'encapsuler le modèle d'une manière ou d'une autre. En d'autres termes, si nous savons que nous avons un bogue, ne vous inquiétez pas du reste du code.
Polymorphisme
Comment faire C'est un problème difficile. Mais il peut être résolu à l'aide de la magie du
polymorphisme . Si vous n'avez jamais rencontré de polymorphisme auparavant, ne vous inquiétez pas. En substance, il s'agit de «fournir une interface unique pour des entités de types différents» (Straustrup, B. «Glossaire C ++ de Björn Straustrup»). En JavaScript, cela signifie que nous créons des objets avec les mêmes méthodes et signatures nommées. Mais un comportement différent. Un exemple classique est la journalisation des applications. Nous pouvons envoyer nos magazines à différents endroits en fonction de l'environnement dans lequel nous nous trouvons. Et si nous créons deux objets enregistreurs, par exemple?
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}); } };
Les deux objets définissent une fonction de journal qui attend un paramètre de chaîne unique. Mais ils se comportent différemment. La beauté est que nous pouvons écrire du code qui appelle
.log()
, quel que soit l'objet qu'il utilise. Il peut s'agir de
consoleLogger
ou
ajaxLogger
. Tout fonctionne quand même. Par exemple, le code ci-dessous fonctionnera aussi bien avec n'importe quel objet:
function log(logger, message) { logger.log(message); }
Un autre exemple est la méthode
.toString()
pour tous les objets JS. Nous pouvons écrire la méthode
.toString()
pour n'importe quelle classe que nous créons. Ensuite, vous pouvez créer deux classes qui implémentent la méthode
.toString()
différemment. Nous les nommerons
Left
et
Right
(un peu plus tard j'expliquerai les noms).
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})`; } }
Créez maintenant une fonction qui appelle
.toString()
sur ces deux objets:
function trace(val) { console.log(val.toString()); return val; } trace(new Left('Hello world'));
Code pas exceptionnel, je sais. Mais le fait est que nous avons deux types de comportement différents qui utilisent la même interface. C'est du polymorphisme. Mais faites attention à quelque chose d'intéressant. Combien de déclarations if avons-nous utilisées? Zéro Pas un seul. Nous avons créé deux types de comportement différents sans une seule instruction if. Peut-être que quelque chose comme ça peut être utilisé pour gérer les erreurs ...
Gauche et droite
Revenons à notre problème. Il est nécessaire de déterminer le chemin d'accès réussi et non réussi pour notre code. Sur un bon chemin, nous continuons simplement à exécuter calmement le code jusqu'à ce qu'une erreur se produise ou que nous le terminions. Si nous nous trouvons sur la mauvaise voie, nous n'essaierons plus d'exécuter le code. Nous pourrions nommer ces chemins Happy et Sad, mais essayez de suivre les conventions de dénomination utilisées par d'autres langages de programmation et bibliothèques. Alors, appelons le mauvais chemin à gauche, et le succès - à droite.
Créons une méthode qui exécute la fonction si nous sommes sur un bon chemin, mais ignorons-la sur un mauvais:
class Left { constructor(val) { this._val = val; } runFunctionOnlyOnHappyPath() {
class Right { constructor(val) { this._val = val; } runFunctionOnlyOnHappyPath(fn) { return fn(this._val); } toString() { const str = this._val.toString(); return `Right(${str})`; } }
Quelque chose comme ça:
const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); leftHello.runFunctionOnlyOnHappyPath(trace);
Diffusion
Nous approchons de quelque chose d'utile, mais pas encore tout à fait. Notre méthode
.runFunctionOnlyOnHappyPath()
renvoie la propriété
_val
. Tout va bien, mais trop gênant si nous voulons exécuter plus d'une fonction. Pourquoi? Parce que nous ne savons plus si nous sommes sur la bonne ou la mauvaise voie. Les informations disparaissent dès que nous prenons la valeur en dehors de Gauche et Droite. Donc, ce que nous pouvons faire, c'est retourner le chemin gauche ou droit avec le nouveau
_val
intérieur. Et nous raccourcirons le nom, puisque nous sommes ici. Ce que nous faisons, c'est traduire une fonction du monde des valeurs simples au monde de gauche et de droite. Par conséquent, nous appelons la méthode
map()
:
class Left { constructor(val) { this._val = val; } map() {
class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } toString() { const str = this._val.toString(); return `Right(${str})`; } }
Nous insérons cette méthode et utilisons Left ou Right dans la syntaxe libre:
const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); const helloToGreetings = str => str.replace(/Hello/, 'Greetings,'); leftHello.map(helloToGreetings).map(trace);
Nous avons créé deux voies d'exécution. Nous pouvons placer les données sur un chemin réussi en appelant
new Right()
, ou sur un chemin échoué en appelant
new Left()
.
Chaque classe représente un chemin: réussi ou échoué. J'ai volé cette métaphore de chemin de fer à Scott VlaschinaSi la
map
fonctionné sur un bon chemin, suivez-la et traitez les données. Si nous échouons, rien ne se passera. Continuez simplement à transmettre la valeur. Si, par exemple, nous plaçons Error sur ce chemin infructueux, nous obtiendrions quelque chose de très similaire pour
try…catch
.
Utilisez .map()
pour vous déplacer le long du cheminAu fur et à mesure que vous progressez, il devient un peu difficile tout le temps d'écrire Gauche ou Droite, appelons donc cette combinaison simplement Soit («soit»). Soit à gauche, soit à droite.
Raccourcis pour créer l'un ou l'autre des objets
Donc, l'étape suivante consiste à réécrire nos exemples de fonctions afin qu'elles renvoient Soit. Gauche pour l'erreur ou Droite pour la valeur. Mais avant de faire cela, amusez-vous. Écrivons quelques raccourcis. La première est une méthode statique appelée
.of()
. Il retourne juste une nouvelle gauche ou droite. Le code peut ressembler à ceci:
Left.of = function of(x) { return new Left(x); }; Right.of = function of(x) { return new Right(x); };
Honnêtement, même
Left.of()
et
Right.of()
fastidieux à écrire. Je penche donc vers des étiquettes encore plus courtes à
left()
et à
right()
:
function left(x) { return Left.of(x); } function right(x) { return Right.of(x); }
Avec ces raccourcis, nous commençons à réécrire les fonctions de l'application:
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}); }
Les fonctions modifiées ne sont pas si différentes des anciennes. Nous enveloppons simplement la valeur de retour à gauche ou à droite, selon qu'il y a une erreur.
Après cela, nous pouvons commencer à traiter la fonction principale qui traite une ligne. Pour commencer, placez la chaîne dans Soit avec
right()
, puis traduisez
splitFields
pour la diviser:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields);
Cela fonctionne très bien, mais le problème se produit si vous essayez de faire de même avec
zipRow()
:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow );
Le fait est que
zipRow()
attend deux paramètres. Mais les fonctions que nous transmettons à
.map()
n'obtiennent qu'une seule valeur de la propriété
._val
. La situation peut être corrigée en utilisant la version
zipRow()
de
zipRow()
. Cela pourrait ressembler à ceci:
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)); }; }
Ce petit changement simplifie la conversion de
zipRow
, donc cela fonctionnera bien avec
.map()
:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields));
Rejoignez
L'utilisation de
.map()
pour exécuter
splitFields()
est très bien, car
.splitFields()
ne renvoie aucun des deux. Mais lorsque vous devez exécuter
zipRow()
, un problème survient car il renvoie Soit. Donc, lorsque vous utilisez
.map()
nous
.map()
par courir dans Soit dans Soit. Si nous allons plus loin,
.map()
bloqués jusqu'à ce que nous exécutions
.map()
intérieur de
.map()
. Ça ne marchera pas non plus. Nous avons besoin d'un moyen de combiner ces deux éléments imbriqués.
.join()
donc une nouvelle méthode, que nous appellerons
.join()
:
class Left { constructor(val) { this._val = val; } map() {
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})`; } }
Nous pouvons désormais «déballer» nos actifs:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields)).join(); const rowObjWithDate = rowObj.map(addDateStr).join();
Chaîne
Nous avons parcouru un long chemin. Mais vous devez vous souvenir de l'appel
.join()
tout le temps, ce qui est ennuyeux. Cependant, nous avons un modèle d'appel consécutif commun
.map()
et
.join()
, alors créons une méthode d'accès rapide pour cela. Appelons-le
chain()
, car il lie les fonctions qui renvoient Left ou Right.
class Left { constructor(val) { this._val = val; } map() {
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})`; } }
Revenant à l'analogie ferroviaire,
.chain()
commute les rails si nous rencontrons une erreur. Cependant, il est plus facile de le montrer sur le diagramme.
Si une erreur se produit, la méthode .chain () vous permet de basculer vers le chemin de gauche. Veuillez noter que les commutateurs ne fonctionnent que dans un sens.Le code est devenu un peu plus propre:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.chain(zipRow(headerFields)); const rowObjWithDate = rowObj.chain(addDateStr);
Faites quelque chose avec des valeurs
La refactorisation de la fonction
processRow()
est presque terminée. Mais que se passe-t-il lorsque nous retournons la valeur? En fin de compte, nous voulons prendre différentes mesures en fonction du type de situation que nous avons: gauche ou droite. Par conséquent, nous allons écrire une fonction qui prendra les mesures appropriées:
function either(leftFunc, rightFunc, e) { return (e instanceof Left) ? leftFunc(e._val) : rightFunc(e._val); }
J'ai triché et utilisé les valeurs internes des objets Gauche ou Droite. Mais faites comme si vous ne l'aviez pas remarqué. Nous pouvons maintenant compléter notre fonction: 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); }
Et si nous nous sentons particulièrement intelligents, nous pouvons à nouveau utiliser la syntaxe gratuite: function processRow(headerFields, row) { const rowObjWithDate = right(row) .map(splitFields) .chain(zipRow(headerFields)) .chain(addDateStr); return either(showError, rowToMessage, rowObjWithDate); }
Les deux versions sont assez jolies. Pas de dessins try...catch
. Et aucune instruction if dans la fonction de niveau supérieur. S'il y a un problème avec une ligne particulière, nous affichons simplement un message d'erreur à la fin. Et notez que processRow()
nous mentionnons Gauche ou Droite la seule fois au tout début lorsque nous appelons right()
. Le reste ne sont que des méthodes utilisées .map()
et .chain()
pour la prochaine fonction.ap et ascenseur
Ça a l'air bien, mais il reste à considérer un dernier scénario. En suivant notre exemple, voyons comment il est possible de traiter toutes les données CSV, et pas seulement chaque ligne individuellement. Nous aurons besoin d'une fonction auxiliaire (helper) ou de trois: function splitCSVToRows(csvData) {
Nous avons donc un assistant qui divise CSV en lignes. Et nous revenons à l'option avec Soit. Vous pouvez maintenant utiliser .map()
certaines fonctions lodash pour extraire la barre de titre des lignes de données. Mais on se retrouve dans une situation intéressante ... function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail);
Nous avons des champs d'en-tête et des lignes de données prêts à être affichés avec processRows()
. Mais headerFields
aussi dataRows
enveloppé dans l'un ou l'autre. Nous avons besoin d'un moyen de convertir processRows()
en une fonction qui fonctionne avec Soit. Pour commencer, nous effectuons le curry processRows
. function processRows(headerFields) { return function processRowsWithHeaderFields(dataRows) {
Maintenant, tout est prêt pour l'expérience. Nous avons headerFields
, qui est Soit, enroulé autour d'un tableau. Que se passera-t-il si nous le prenons headerFields
et l'appelons .map()
avec processRows()
? function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail);
Avec .map (), une fonction externe est appelée ici processRows()
, mais pas une fonction interne. En d'autres termes, processRows()
renvoie une fonction. Et depuis .map()
, nous récupérons toujours Either. Ainsi, le résultat est une fonction dans Either, qui est appelée funcInEither
. Il prend un tableau de chaînes et renvoie un tableau d'autres chaînes. Nous devons en quelque sorte prendre cette fonction et l'appeler avec une valeur à l'intérieur dataRows
. Pour ce faire, ajoutez une autre méthode à nos classes Left et Right. Nous l'appellerons .ap()
conformément à la norme .Comme d'habitude, la méthode ne fait rien sur la piste de gauche:
Et pour la classe Right, on attend un autre Soit avec une fonction:
Nous pouvons maintenant remplir notre fonction principale: 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); }
L'essence de la méthode est .ap()
immédiatement un peu comprise (les spécifications de Fantasy Land la confondent, mais dans la plupart des autres langues, la méthode est utilisée dans l'autre sens). Si vous le décrivez plus facilement, vous dites: «J'ai une fonction qui prend généralement deux valeurs simples. Je veux en faire une fonction qui prend deux Soit. " Si disponible, .ap()
nous pouvons écrire une fonction qui fera exactement cela. Appelons-le liftA2()
, encore une fois conformément au nom standard. Elle prend une fonction simple qui attend deux arguments et la «soulève» pour qu'elle fonctionne avec des «applicatifs». (ce sont des objets qui contiennent à la fois une méthode .ap()
et une méthode .of()
). Ainsi, liftA2 est l'abréviation de «ascenseur applicatif, deux paramètres».Une fonction liftA2
pourrait donc ressembler à ceci: function liftA2(func) { return function runApplicativeFunc(a, b) { return b.ap(a.map(func)); }; }
Notre fonction de niveau supérieur l'utilisera comme suit: 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 sur CodePen .Non? C'est tout?
Vous vous demandez peut-être quoi de mieux que de simples exceptions? Ne me semble-t-il pas que c'est une manière trop compliquée de résoudre un problème simple? Voyons d'abord pourquoi nous aimons les exceptions. S'il n'y avait pas d'exceptions, vous auriez à écrire un grand nombre d'instructions if partout. Nous écrirons toujours du code selon le principe "si ce dernier fonctionne, continuez, sinon traitez l'erreur." Et nous devons gérer ces erreurs tout au long du code. Cela rend difficile de comprendre ce qui se passe. Les exceptions vous permettent de quitter le programme en cas de problème. Par conséquent, vous n'avez pas besoin d'écrire tous ces ifs. Vous pouvez vous concentrer sur un chemin d'exécution réussi.Mais il y a un hic. Les exceptions cachent trop. Lorsque vous lève une exception, vous transférez le problème de gestion des erreurs vers une autre fonction. Il est trop facile d'ignorer une exception qui apparaîtra au plus haut niveau. Le bon côté de Soit est qu'il vous permet de sauter du flux de programme principal, comme avec une exception. Et cela fonctionne honnêtement. Vous obtenez soit à droite ou à gauche. Vous ne pouvez pas prétendre que l'option gauche est impossible. En fin de compte, vous devez retirer la valeur avec un appel comme either()
.Je sais que cela ressemble à une sorte de complexité. Mais regardez le code que nous avons écrit (pas les classes, mais les fonctions qui les utilisent). Il n'y a pas beaucoup de code de gestion des exceptions. Il est presque absent, à l'exception d'un appel either()
à la fin csvToMessages()
etprocessRow()
. Voilà le point. Avec Soit, vous avez une gestion des erreurs propre qui ne peut pas être accidentellement oubliée. Sans l'un ou l'autre, tamponnez le code et ajoutez du rembourrage partout.Cela ne signifie pas que vous ne devez jamais l'utiliser try...catch
. Parfois, c'est le bon outil, et c'est normal. Mais ce n'est pas le seul outil. Soit vous donne des avantages que vous n'avez pas try...catch
. Alors donnez une chance à cette monade. Même si c'est difficile au début, je pense que vous l'aimerez. Veuillez ne pas utiliser l'implémentation de cet article. Essayez l'une des célèbres bibliothèques telles que Crocks , Sanctuary , Folktale ou Monet . Ils sont mieux servis. Et ici, pour plus de simplicité, j'ai raté quelque chose.Ressources supplémentaires