Elegante manejo de errores de JavaScript con la mónada Either

Hablemos un poco sobre cómo manejamos los errores. En JavaScript, tenemos una función de lenguaje incorporada para trabajar con excepciones. Adjuntamos el código problemático en la construcción try...catch . Esto le permite especificar una ruta de ejecución normal en la sección de try , y luego tratar con todas las excepciones en la sección de catch . No es una mala opción. Esto le permite concentrarse en la tarea actual sin pensar en cada posible error. Definitivamente mejor que obstruir su código con ifs interminables.

Sin try...catch es difícil verificar los resultados de cada llamada de función para valores inesperados. Este es un diseño útil. Pero ella tiene ciertos problemas. Y esta no es la única forma de manejar los errores. En este artículo, veremos el uso de la mónada Either como una alternativa para try...catch .

Antes de continuar, noto un par de puntos. El artículo asume que ya sabes acerca de la composición de funciones y el curry. Y una advertencia. Si no te has encontrado con mónadas antes, pueden parecer realmente ... extrañas. Trabajar con tales herramientas requiere un cambio de pensamiento. Al principio puede ser difícil.

No se preocupe si está inmediatamente confundido. Todos lo tienen. Al final del artículo, enumeré algunos enlaces que podrían ayudar. No te rindas. Estas cosas se intoxican tan pronto como penetran en el cerebro.

Ejemplo de problema


Antes de discutir los problemas de las excepciones, hablemos sobre por qué existen y por qué try...catch aparecieron bloques de try...catch . Para hacer esto, veamos un problema que intenté hacer al menos parcialmente realista. Imagine que estamos escribiendo una función para mostrar una lista de notificaciones. Ya hemos logrado (de alguna manera) devolver datos del servidor. Pero por alguna razón, los ingenieros de backend decidieron enviarlo en formato CSV, no JSON. Los datos sin procesar pueden verse así:

  marca de tiempo, contenido, visto, href
 2018-10-27T05: 33: 34 + 00: 00, @ madhatter te invitó a tomar el té, sin leer, https: //example.com/invite/tea/3801
 2018-10-26T13: 47: 12 + 00: 00, @ queenofhearts te mencionó en la discusión del 'Torneo de croquet', visto, https: //example.com/discussions/croquet/1168
 2018-10-25T03: 50: 08 + 00: 00, @ cheshirecat le envió una sonrisa, no leída, https: //example.com/interactions/grin/88 

Queremos mostrarlo en HTML. Puede verse más o menos así:

 <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> 

Para simplificar la tarea, solo enfóquese en procesar cada fila de datos CSV por ahora. Comencemos con algunas funciones simples para procesar cadenas. El primero divide la cadena de texto en campos:

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

La función se simplifica aquí porque es material educativo. Nos ocupamos del manejo de errores, no del análisis CSV. Si uno de los mensajes contiene una coma, todo esto estará terriblemente mal. Nunca use este código para analizar datos CSV reales. Si alguna vez ha tenido que analizar datos CSV, use la bien probada biblioteca de análisis CSV .

Después de dividir los datos, queremos crear un objeto. Y para que cada nombre de propiedad coincida con los encabezados CSV. Supongamos que ya de alguna manera analizamos la barra de título (más sobre eso más adelante). Hemos llegado a un punto en el que algo puede salir mal. Recibimos un error de procesamiento. Lanzamos un error si la longitud de la cadena no coincide con la barra de título. ( _.zipObject es una función lodash ).

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

Después de eso, agregue una fecha legible por humanos al objeto para mostrarlo en nuestra plantilla. Resultó un poco detallado, ya que JavaScript no tiene una compatibilidad integrada perfecta para el formato de fecha. Y nuevamente, enfrentamos problemas potenciales. Si se encuentra una fecha no válida, nuestra función arroja un error.

 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}; } 

Finalmente, tome el objeto y páselo a través de la función de plantilla para obtener la cadena HTML.

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

También sería bueno imprimir un error si se cumple:

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

Cuando todo esté en su lugar, puede armar una función para procesar cada línea.

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

Entonces la función está lista. Echemos un vistazo más de cerca a cómo maneja las excepciones.

Excepciones: la buena parte


Entonces, ¿qué tiene de bueno try...catch ? Cabe señalar que en el ejemplo anterior, cualquiera de los pasos en el bloque try puede causar un error. En zipRow() y addDateStr() arrojamos errores intencionalmente. Y si surge un problema, simplemente detecte el error y muestre cualquier mensaje en la página. Sin este mecanismo, el código se vuelve realmente feo. Así es como podría verse. Suponga que las funciones no arrojan errores, pero devuelven 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); } 

Como puede ver, una gran cantidad de plantillas if . El código es más detallado. Y es difícil seguir la lógica básica. Además, null no nos dice mucho. Realmente no sabemos por qué falló la llamada a la función anterior. Tenemos que adivinar. Creamos un mensaje de error y llamamos a showError() . Tal código es más sucio y más confuso.

Mire nuevamente la versión de manejo de excepciones. Claramente separa la ruta exitosa del programa y el código de manejo de excepciones. La rama try es una buena manera, y la rama catch es un error. Todo el manejo de excepciones ocurre en un solo lugar. Y las funciones individuales pueden informar por qué fallaron. En general, esto parece bastante dulce. Creo que la mayoría considera que el primer ejemplo es bastante adecuado. ¿Por qué un enfoque diferente?

Problemas al manejar la excepción intentar ... captura


Este enfoque le permite ignorar estos errores molestos. Desafortunadamente, try...catch hace su trabajo demasiado bien. Simplemente lanzas una excepción y sigues adelante. Podemos atraparlo más tarde. Y todos tienen la intención de poner siempre esos bloques, de verdad. Pero no siempre es obvio dónde el error va más allá. Y el bloque es demasiado fácil de olvidar. Y antes de darse cuenta de esto, su aplicación se bloquea.

Además, las excepciones contaminan el código. No discutiremos la pureza funcional en detalle aquí. Pero veamos un pequeño aspecto de la pureza funcional: la transparencia referencial. Una función de enlace transparente siempre devuelve el mismo resultado para una entrada particular. Pero para funciones con excepciones, no podemos decir eso. Pueden lanzar una excepción en cualquier momento en lugar de devolver un valor. Esto complica la lógica. Pero, ¿qué sucede si encuentra una opción de ganar-ganar, una forma limpia de manejar los errores?

Se nos ocurre una alternativa


Las funciones puras siempre devuelven un valor (incluso si falta ese valor). Por lo tanto, nuestro código de manejo de errores debe suponer que siempre devolvemos un valor. Entonces, como primer intento, ¿qué debo hacer si, en caso de falla, devolvemos un objeto Error? Es decir, cada vez que cometemos un error, devolvemos dicho objeto. Puede verse más o menos así:

 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); } 

Esta no es una actualización especial sin excepción. Pero es mejor. Hemos transferido la responsabilidad de los mensajes de error a las funciones individuales. Pero todavía tenemos todos estos ifs. Sería bueno encapsular la plantilla de alguna manera. En otras palabras, si sabemos que tenemos un error, no se preocupe por el resto del código.

Polimorfismo


Como hacerlo Este es un problema difícil. Pero se puede resolver con la ayuda de la magia del polimorfismo . Si no ha encontrado polimorfismo antes, no se preocupe. En esencia, es "proporcionar una interfaz única para entidades de diferentes tipos" (Straustrup, B. "C ++ Glosario de Björn Straustrup"). En JavaScript, esto significa que creamos objetos con los mismos métodos y firmas con el mismo nombre. Pero comportamiento diferente. Un ejemplo clásico es el registro de aplicaciones. Podemos enviar nuestras revistas a diferentes lugares según el entorno en el que nos encontremos. ¿Qué pasa si creamos dos objetos registradores, por ejemplo?

 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}); } }; 

Ambos objetos definen una función de registro que espera un único parámetro de cadena. Pero se comportan de manera diferente. Lo bueno es que podemos escribir código que llame a .log() , sin importar qué objeto use. Puede ser consoleLogger o ajaxLogger . Todo funciona de todos modos. Por ejemplo, el siguiente código funcionará igualmente bien con cualquier objeto:

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

Otro ejemplo es el método .toString() para todos los objetos JS. Podemos escribir el método .toString() para cualquier clase que creamos. A continuación, puede crear dos clases que implementen el método .toString() diferente. Los nombraremos Left y Right (un poco más adelante explicaré los nombres).

 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})`; } } 

Ahora cree una función que llame a .toString() en estos dos objetos:

 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); 

No es un código pendiente, lo sé. Pero el hecho es que tenemos dos tipos diferentes de comportamiento que usan la misma interfaz. Esto es polimorfismo. Pero presta atención a algo interesante. ¿Cuántos enunciados if usamos? Cero Ni uno solo. Creamos dos tipos diferentes de comportamiento sin una sola declaración if. Quizás algo como esto se pueda usar para manejar errores ...

Izquierda y derecha


Volviendo a nuestro problema. Es necesario determinar la ruta exitosa y no exitosa para nuestro código. En un buen camino, simplemente seguimos ejecutando el código con calma hasta que se produce un error o lo terminamos. Si nos encontramos en el camino equivocado, ya no intentaremos ejecutar el código. Podríamos nombrar estos caminos Happy y Sad, pero trate de seguir las convenciones de nombres que usan otros lenguajes de programación y bibliotecas. Entonces, llamemos al mal camino Izquierda, y al exitoso: Derecha.

Creemos un método que ejecute la función si estamos en un buen camino, pero ignórelo en uno malo:

 /** * 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})`; } } 

Algo como esto:

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

Broadcast


Nos estamos acercando a algo útil, pero todavía no. Nuestro método .runFunctionOnlyOnHappyPath() devuelve la propiedad _val . Todo está bien, pero es demasiado inconveniente si queremos ejecutar más de una función. Por qué Porque ya no sabemos si estamos en el camino correcto o incorrecto. La información desaparece tan pronto como tomamos el valor fuera de Izquierda y Derecha. Entonces, lo que podemos hacer es devolver la ruta Izquierda o Derecha con el nuevo _val dentro. Y acortaremos el nombre, ya que estamos aquí. Lo que hacemos es traducir una función del mundo de los valores simples al mundo de izquierda y derecha. Por lo tanto, llamamos al método map() :

 /** * 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})`; } } 

Insertamos este método y usamos Izquierda o Derecha en la sintaxis 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); // Doesn't print any thing to the console // ← Left(Hello world) rightHello.map(helloToGreetings).map(trace); // ⦘ Greetings, world // ← Right(Greetings, world) 

Hemos creado dos caminos de ejecución. Podemos poner los datos en una ruta exitosa llamando a la new Right() , o en una ruta fallida llamando a la new Left() .


Cada clase representa un camino: exitoso o no exitoso. Robé esta metáfora del ferrocarril de Scott Vlaschina

Si el map funcionó en un buen camino, continúe y procese los datos. Si nos encontramos sin éxito, no sucederá nada. Solo sigue pasando el valor aún más. Si, por ejemplo, colocamos Error en esta ruta fallida, obtendríamos algo muy similar para try…catch .


Use .map() para moverse a lo largo del camino

A medida que avanza, se vuelve un poco difícil escribir Izquierda o Derecha, así que llamemos a esta combinación simplemente Cualquiera de los dos ("cualquiera"). Ya sea izquierda o derecha.

Atajos para crear cualquiera de los objetos


Entonces, el siguiente paso es reescribir nuestras funciones de ejemplo para que devuelvan cualquiera. Izquierda por error o Derecha por valor. Pero antes de hacer esto, diviértete. Escribamos un par de atajos. El primero es un método estático llamado .of() . Simplemente devuelve una nueva izquierda o derecha. El código puede verse así:

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

Honestamente, incluso Left.of() y Right.of() tedioso de escribir. Así que me inclino hacia etiquetas aún más cortas left() y right() :

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

Con estos atajos, comenzamos a reescribir las funciones de la aplicación:

 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}); } 

Las funciones modificadas no son tan diferentes de las antiguas. Simplemente ajustamos el valor de retorno en Izquierda o Derecha, dependiendo de si hay un error.

Después de eso, podemos comenzar a procesar la función principal que procesa una línea. Para comenzar, coloque la cadena en O bien con right() , y luego traduzca splitFields para dividirlo:

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

Esto funciona bien, pero el problema ocurre si intenta hacer lo mismo con zipRow() :

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

El hecho es que zipRow() espera dos parámetros. Pero las funciones que pasamos a .map() obtienen solo un valor de la propiedad ._val . La situación puede corregirse utilizando la versión zipRow() de zipRow() . Puede verse más o menos así:

 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)); }; } 

Este pequeño cambio simplifica la conversión de zipRow , por lo que funcionará bien con .map() :

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

Unirse


Usar .map() para ejecutar splitFields() está bien, ya que .splitFields() tampoco devuelve ninguno. Pero cuando tiene que ejecutar zipRow() , se produce un problema porque devuelve Either. Entonces, cuando usamos .map() terminamos encontrándonos con Either dentro de Either. Si vamos más allá, quedamos atascados hasta que .map() dentro de .map() . Eso tampoco funcionará. Necesitamos alguna forma de combinar estos anidados. Así que escribamos un nuevo método, que llamaremos .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})`; } } 

Ahora podemos "desempaquetar" nuestros activos:

 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? } 

Cadena


Hemos recorrido un largo camino. Pero tienes que recordar la llamada .join() todo el tiempo, lo cual es molesto. Sin embargo, tenemos un patrón de llamada consecutivo común .map() y .join() , así que .join() un método de acceso rápido para ello. Llamémosle chain() , porque une funciones que devuelven Left o Right.

 /** *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})`; } } 

Volviendo a la analogía del ferrocarril, .chain() cambia los rieles si encontramos un error. Sin embargo, es más fácil de mostrar en el diagrama.


Si se produce un error, el método .chain () le permite cambiar a la ruta izquierda. Tenga en cuenta que los interruptores solo funcionan de una manera.

El código se puso un poco más limpio:

 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? } 

Haz algo con valores


La refactorización de la función processRow() está casi completa. Pero, ¿qué sucede cuando devolvemos el valor? Al final, queremos tomar diferentes medidas según el tipo de situación que tengamos: izquierda o derecha. Por lo tanto, escribiremos una función que tomará las medidas apropiadas:

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

Hice trampa y usé los valores internos de los objetos Izquierdo o Derecho. Pero finja que no se dio cuenta de esto. Ahora podemos completar nuestra función:

 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); } 

Y si nos sentimos particularmente inteligentes, entonces podemos usar nuevamente la sintaxis gratuita:

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

Ambas versiones son bastante bonitas. No se diseña try...catch. Y no hay sentencias if en la función de nivel superior. Si hay un problema con una línea en particular, simplemente mostramos un mensaje de error al final. Y tenga en cuenta que en processRow()mencionamos Izquierda o Derecha la única vez al principio cuando llamamos right(). El resto son sólo los métodos usados .map()y .chain()para el uso de la siguiente función.

ap y ascensor


Se ve bien, pero queda por considerar un último escenario. Siguiendo nuestro ejemplo, veamos cómo es posible procesar todos los datos CSV, y no solo cada fila individualmente. Necesitaremos una función auxiliar (auxiliar) o tres:

 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>`; } 

Entonces, tenemos un ayudante que divide el CSV en líneas. Y volvemos a la opción con Either. Ahora puede usar .map()algunas funciones lodash para extraer la barra de título de las líneas de datos. Pero nos encontramos en una situación interesante ...

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

Tenemos campos de encabezado y filas de datos listos para mostrar processRows(). Pero headerFieldstambién dataRowsenvuelto en cualquiera. Necesitamos alguna forma de convertir processRows()a una función que funcione con Either. Para comenzar, realizamos curry processRows.

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

Ahora todo está listo para el experimento. Tenemos headerFields, que es Oither, envuelto alrededor de una matriz. ¿Qué pasará si tomamos headerFieldsy llamada en lo .map()que 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); } 

Con .map (), aquí se llama una función externa processRows(), pero no interna. En otras palabras, processRows()devuelve una función. Y desde eso .map(), todavía recuperamos a Oither. Por lo tanto, el resultado es una función dentro de Either, que se llama funcInEither. Toma una matriz de cadenas y devuelve una matriz de otras cadenas. Necesitamos de alguna manera tomar esta función y llamarla con un valor dentro dataRows. Para hacer esto, agregue otro método a nuestras clases Izquierda y Derecha. Lo llamaremos .ap()de acuerdo con el estándar .

Como de costumbre, el método no hace nada en la pista izquierda:

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

Y para la clase Right, esperamos otro Either con una función:

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

Ahora podemos completar nuestra función principal:

  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); } 

La esencia del método se .ap()comprende un poco de inmediato (las especificaciones de Fantasy Land lo confunden, pero en la mayoría de los otros idiomas el método se usa al revés). Si lo describe más fácilmente, entonces dice: “Tengo una función que generalmente toma dos valores simples. Quiero convertirlo en una función que requiera dos O ". Si está disponible, .ap()podemos escribir una función que haga exactamente eso. Llamémoslo liftA2(), nuevamente de acuerdo con el nombre estándar. Ella toma una función simple que espera dos argumentos, y la "levanta" para que funcione con "candidatos". (estos son objetos que contienen tanto un método .ap()como un método .of()). Entonces, liftA2 es la abreviatura de "elevación aplicativa, dos parámetros".

Entonces, una función liftA2podría verse así:

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

Nuestra función de nivel superior lo usará de la siguiente manera:

 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); } 

Código en CodePen .

Derecho? ¿Eso es todo?


Puede preguntar, ¿qué es mejor que simples excepciones? ¿No me parece que esta es una forma demasiado complicada de resolver un problema simple? Primero pensemos por qué nos gustan las excepciones. Si no hubiera excepciones, tendría que escribir muchas declaraciones if en todas partes. Siempre escribiremos código de acuerdo con el principio "si este último funciona, continúe, de lo contrario procese el error". Y debemos manejar estos errores en todo el código. Esto hace que sea difícil entender lo que está sucediendo. Las excepciones le permiten salir del programa si algo salió mal. Por lo tanto, no necesita escribir todos estos ifs. Puede centrarse en una ruta de ejecución exitosa.

Pero hay un inconveniente. Las excepciones ocultan demasiado. Cuando lanza una excepción, transfiere el problema de manejo de errores a alguna otra función. Es demasiado fácil ignorar una excepción que aparecerá en el nivel más alto. El lado bueno de Either es que le permite saltar fuera de la secuencia principal del programa, como con una excepción. Y funciona honestamente. Tienes derecho o izquierda. No puede pretender que la opción Izquierda es imposible. Al final, debe extraer el valor con una llamada como either().

Sé que suena como algún tipo de complejidad. Pero eche un vistazo al código que escribimos (no clases, sino funciones que los usan). No hay mucho código de manejo de excepciones. Está casi ausente, con la excepción de una llamada either()al final csvToMessages()yprocessRow(). Ese es todo el punto. Con Oither, tiene un manejo limpio de errores que no puede olvidarse accidentalmente. Sin ninguno de los dos, sella el código y agrega relleno en todas partes.

Esto no significa que nunca debas usarlo try...catch. A veces es la herramienta correcta, y es normal. Pero esta no es la única herramienta. Cualquiera de los dos le brinda algunos beneficios que no tiene try...catch. Así que dale una oportunidad a esta mónada. Incluso si es difícil al principio, creo que te gustará. Por favor, no use la implementación de este artículo. Pruebe una de las bibliotecas famosas como Crocks , Sanctuary , Folktale o Monet . Están mejor servidos. Y aquí, por simplicidad, me perdí algo.

Recursos Adicionales


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


All Articles