Vamos falar um pouco sobre como lidamos com erros. Em JavaScript, temos uma função de linguagem integrada para trabalhar com exceções. Colocamos o código problemático na construção
try...catch
. Isso permite que você especifique um caminho de execução normal na seção
try
e lide com todas as exceções na seção
catch
. Não é uma má opção. Isso permite que você se concentre na tarefa atual sem pensar em todos os erros possíveis. Definitivamente melhor do que entupir seu código com infinitos ifs.
Sem
try...catch
é difícil verificar os resultados de cada chamada de função em busca de valores inesperados. Este é um design útil. Mas ela tem certos problemas. E essa não é a única maneira de lidar com erros. Neste artigo, veremos como usar a
mônada Either como uma alternativa para
try...catch
.
Antes de continuar, observo alguns pontos. O artigo pressupõe que você já saiba sobre composição de funções e currying. E um aviso. Se você nunca encontrou mônadas antes, elas podem parecer realmente ... estranhas. Trabalhar com essas ferramentas requer uma mudança de pensamento. No começo, pode ser difícil.
Não se preocupe se você estiver imediatamente confuso. Todo mundo tem. No final do artigo, listei alguns links que podem ajudar. Não desista. Essas coisas ficam intoxicadas assim que penetram no cérebro.
Exemplo de problema
Antes de discutir os problemas das exceções, vamos falar sobre por que eles existem e por que
try...catch
blocos de
try...catch
apareceram. Para fazer isso, vejamos um problema que tentei tornar pelo menos parcialmente realista. Imagine que estamos escrevendo uma função para exibir uma lista de notificações. Já conseguimos (de alguma forma) retornar dados do servidor. Mas, por alguma razão, os engenheiros de back-end decidiram enviá-lo no formato CSV, não JSON. Os dados brutos podem se parecer com isso:
registro de data e hora, conteúdo, visualizado, href
2018-10-27T05: 33: 34 + 00: 00, @ madhatter convidou você para um chá, não lido, https: //example.com/invite/tea/3801
2018-10-26T13: 47: 12 + 00: 00, @ queenofhearts mencionou você na discussão 'Croquet Tournament', visualizada em https: //example.com/discussions/croquet/1168
2018-10-25T03: 50: 08 + 00: 00, @ cheshirecat enviou-lhe um sorriso, não lido, https: //example.com/interactions/grin/88
Queremos exibi-lo em HTML. Pode ser algo como isto:
<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 a tarefa, concentre-se no processamento de cada linha de dados CSV por enquanto. Vamos começar com algumas funções simples para o processamento de strings. O primeiro divide a sequência de texto em campos:
function splitFields(row) { return row.split('","'); }
A função é simplificada aqui porque é material educacional. Lidamos com o tratamento de erros, não com a análise CSV. Se uma das mensagens contiver vírgula, tudo isso estará terrivelmente errado. Nunca use esse código para analisar dados CSV reais. Se você já teve que analisar dados CSV, use a
biblioteca de análise de CSV bem testada .
Depois de dividir os dados, queremos criar um objeto. E para que cada nome de propriedade corresponda aos cabeçalhos CSV. Suponha que já tenhamos analisado a barra de título (mais sobre isso mais tarde). Chegamos a um ponto em que algo pode dar errado. Ocorreu um erro no processamento. Geramos um erro se o comprimento da string não corresponder à barra de título. (
_.zipObject
é
uma função lodash ).
function zipRow(headerFields, fieldData) { if (headerFields.length !== fieldData.length) { throw new Error("Row has an unexpected number of fields"); } return _.zipObject(headerFields, fieldData); }
Depois disso, adicione uma data legível por humanos ao objeto para exibi-la em nosso modelo. Ficou um pouco detalhado, já que o JavaScript não possui suporte interno perfeito para formatação de data. E, novamente, enfrentamos problemas em potencial. Se uma data inválida for encontrada, nossa função gera um erro.
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}; }
Por fim, pegue o objeto e passe-o pela
função de modelo para obter a string HTML.
const rowToMessage = _.template(`<li class="Message Message--<%= viewed %>"> <a href="<%= href %>" class="Message-link"><%= content %></a> <time datetime="<%= datestamp %>"><%= datestr %></time> <li>`);
Também seria bom imprimir um erro se encontrasse:
const showError = _.template(`<li class="Error"><%= message %></li>`);
Quando tudo estiver no lugar, você poderá montar uma função para processar cada linha.
function processRow(headerFieldNames, row) { try { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); rowObjWithDate = addDateStr(rowObj); return rowToMessage(rowObj); } catch(e) { return showError(e); } }
Então a função está pronta. Vamos dar uma olhada em como ele lida com exceções.
Exceções: a parte boa
Então, o que é bom em
try...catch
? Observe que no exemplo acima, qualquer uma das etapas no bloco
try
pode causar um erro. Em
zipRow()
e
addDateStr()
lançamos intencionalmente erros. E se surgir um problema, apenas pegue o erro e exiba qualquer mensagem na página. Sem esse mecanismo, o código se torna realmente feio. Aqui está como isso pode parecer. Suponha que as funções não gerem erros, mas retornem
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 você pode ver, um grande número de modelos
if
. O código é mais detalhado. E é difícil seguir a lógica básica. Além disso,
null
não nos diz muita coisa. Realmente não sabemos por que a chamada de função anterior falhou. Temos que adivinhar. Criamos uma mensagem de erro e chamamos
showError()
. Esse código é mais sujo e mais confuso.
Veja novamente a versão de tratamento de exceções. Separa claramente o caminho bem-sucedido do programa e o código de tratamento de exceções. A ramificação
try
é uma boa maneira, e a ramificação
catch
é um erro. Todo o tratamento de exceções ocorre em um só lugar. E funções individuais podem relatar por que falharam. Em suma, isso parece muito doce. Penso que a maioria considera o primeiro exemplo bastante adequado. Por que uma abordagem diferente?
Problemas ao lidar com exceção try ... catch
Essa abordagem permite que você ignore esses erros irritantes. Infelizmente,
try...catch
faz seu trabalho muito bem. Você apenas lança uma exceção e segue em frente. Podemos pegá-lo mais tarde. E todo mundo pretende sempre colocar esses blocos, realmente. Mas nem sempre é óbvio onde o erro vai mais longe. E o bloco é muito fácil de esquecer. E antes que você perceba isso, seu aplicativo falha.
Além disso, as exceções poluem o código. Não discutiremos a pureza funcional em detalhes aqui. Mas vamos olhar para um pequeno aspecto da pureza funcional: transparência referencial. Uma função transparente de link sempre retorna o mesmo resultado para uma entrada específica. Mas para funções com exceções, não podemos dizer isso. Eles podem lançar uma exceção a qualquer momento, em vez de retornar um valor. Isso complica a lógica. Mas e se você encontrar uma opção ganha-ganha - uma maneira limpa de lidar com erros?
Nós criamos uma alternativa
As funções puras sempre retornam um valor (mesmo que esse valor esteja ausente). Portanto, nosso código de tratamento de erros deve assumir que sempre retornamos um valor. Portanto, como primeira tentativa, o que devo fazer se, com falha, retornarmos um objeto Error? Ou seja, sempre que cometemos um erro, retornamos esse objeto. Pode ser algo como isto:
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 não é uma atualização especial sem exceção. Mas é melhor. Transferimos a responsabilidade pelas mensagens de erro de volta às funções individuais. Mas ainda temos todos esses ifs. Seria bom encapsular o modelo de alguma forma. Em outras palavras, se soubermos que temos um bug, não se preocupe com o restante do código.
Polimorfismo
Como fazer isso? Este é um problema difícil. Mas isso pode ser resolvido com a ajuda da magia do
polimorfismo . Se você não encontrou polimorfismo antes, não se preocupe. Em essência, está "fornecendo uma interface única para entidades de diferentes tipos" (Straustrup, B. "C ++ Glossary of Björn Straustrup"). Em JavaScript, isso significa que criamos objetos com os mesmos métodos e assinaturas nomeados. Mas comportamento diferente. Um exemplo clássico é o log de aplicativos. Podemos enviar nossas revistas para lugares diferentes, dependendo do ambiente em que estamos. E se criarmos dois objetos de logger, por exemplo?
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 os objetos definem uma função de log que espera um único parâmetro de sequência. Mas eles se comportam de maneira diferente. O bom é que podemos escrever um código que chame
.log()
, não importa qual objeto ele use. Pode ser
consoleLogger
ou
ajaxLogger
. Tudo funciona de qualquer maneira. Por exemplo, o código abaixo funcionará igualmente bem com qualquer objeto:
function log(logger, message) { logger.log(message); }
Outro exemplo é o método
.toString()
para todos os objetos JS. Podemos escrever o método
.toString()
para qualquer classe que criamos. Em seguida, você pode criar duas classes que implementam o método
.toString()
diferente. Vamos chamá-los de
Left
e
Right
(um pouco mais tarde vou explicar os nomes).
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})`; } }
Agora crie uma função que chama
.toString()
nesses dois objetos:
function trace(val) { console.log(val.toString()); return val; } trace(new Left('Hello world'));
Código não excepcional, eu sei. Mas o fato é que temos dois tipos diferentes de comportamento que usam a mesma interface. Isso é polimorfismo. Mas preste atenção em algo interessante. Quantas declarações if usamos? Zero Nem um único. Criamos dois tipos diferentes de comportamento sem uma única instrução if. Talvez algo assim possa ser usado para lidar com erros ...
Esquerda e Direita
Voltando ao nosso problema. É necessário determinar o caminho bem-sucedido e malsucedido do nosso código. Em um bom caminho, simplesmente continuamos a executar o código com calma até que um erro ocorra ou o concluímos. Se nos encontrarmos no caminho errado, não tentaremos mais executar o código. Poderíamos nomear esses caminhos Happy e Sad, mas tente seguir as convenções de nomenclatura usadas por outras linguagens de programação e bibliotecas. Então, vamos chamar o caminho ruim de esquerda e o bem-sucedido - direita.
Vamos criar um método que execute a função se estivermos em um bom caminho, mas ignorá-lo em um caminho ruim:
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})`; } }
Algo assim:
const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); leftHello.runFunctionOnlyOnHappyPath(trace);
Difusão
Estamos nos aproximando de algo útil, mas ainda não. Nosso método
.runFunctionOnlyOnHappyPath()
retorna a propriedade
_val
. Está tudo bem, mas é muito inconveniente se queremos executar mais de uma função. Porque Porque não sabemos mais se estamos no caminho certo ou errado. As informações desaparecem assim que obtemos o valor fora de esquerda e direita. Então, o que podemos fazer é retornar o caminho Esquerdo ou Direito com o novo
_val
dentro. E encurtaremos o nome já que estamos aqui. O que fazemos é traduzir uma função do mundo dos valores simples para o mundo da esquerda e da direita. Portanto, chamamos o método
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})`; } }
Nós inserimos esse método e usamos Left ou Right na sintaxe livre:
const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); const helloToGreetings = str => str.replace(/Hello/, 'Greetings,'); leftHello.map(helloToGreetings).map(trace);
Criamos dois caminhos de execução. Podemos colocar os dados em um caminho bem-sucedido chamando
new Right()
, ou em um caminho com falha chamando
new Left()
.
Cada classe representa um caminho: bem-sucedido ou malsucedido. Eu roubei essa metáfora ferroviária de Scott VlaschinaSe o
map
funcionou em um bom caminho, siga-o e processe os dados. Se não conseguirmos, nada acontecerá. Apenas continue passando o valor ainda mais. Se, por exemplo, colocarmos Error nesse caminho malsucedido, obteríamos algo muito semelhante para
try…catch
.
Use .map()
para percorrer o caminhoÀ medida que você progride, fica um pouco difícil escrever a esquerda ou a direita, então vamos chamar essa combinação simplesmente de Qualquer um ("qualquer um"). Esquerda ou direita.
Atalhos para criar objetos
Portanto, o próximo passo é reescrever nossas funções de exemplo para que elas retornem qualquer um. Esquerda por erro ou Direita por valor. Mas antes de fazer isso, divirta-se. Vamos escrever alguns atalhos. O primeiro é um método estático chamado
.of()
. Apenas retorna uma nova esquerda ou direita. O código pode ficar assim:
Left.of = function of(x) { return new Left(x); }; Right.of = function of(x) { return new Right(x); };
Honestamente, mesmo
Left.of()
e
Right.of()
tediosos para escrever. Então, estou inclinado para rótulos
left()
e
right()
ainda mais curtos:
function left(x) { return Left.of(x); } function right(x) { return Right.of(x); }
Com esses atalhos, começamos a reescrever as funções do aplicativo:
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}); }
Funções modificadas não são tão diferentes das antigas. Simplesmente envolvemos o valor de retorno em Esquerda ou Direita, dependendo da existência de um erro.
Depois disso, podemos começar a processar a função principal que processa uma linha. Para começar, coloque a string em Either com
right()
e depois traduza
splitFields
para dividi-la:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields);
Isso funciona muito bem, mas o problema acontece se você tentar fazer o mesmo com
zipRow()
:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow );
O fato é que
zipRow()
espera dois parâmetros. Mas as funções que passamos para
.map()
obtêm apenas um valor da propriedade
._val
. A situação pode ser corrigida usando a versão
zipRow()
de
zipRow()
. Pode ser algo como isto:
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)); }; }
Essa pequena alteração simplifica a conversão do
zipRow
, para que funcione bem com
.map()
:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields));
Aderir
Usar
.map()
para executar
splitFields()
é bom, pois
.splitFields()
também não retorna. Mas quando você precisa executar o
zipRow()
, surge um problema porque ele retorna Ou. Portanto, ao usar
.map()
, acabamos rodando para o Either dentro do Either. Se formos além, ficaremos presos até
.map()
dentro de
.map()
. Isso também não vai funcionar. Precisamos de alguma maneira de combinar esses aninhados. Então, vamos escrever um novo método, que chamaremos de
.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})`; } }
Agora podemos "descompactar" nossos ativos:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields)).join(); const rowObjWithDate = rowObj.map(addDateStr).join();
Cadeia
Percorremos um longo caminho. Mas você deve se lembrar da chamada
.join()
o tempo todo, o que é irritante. No entanto, temos um padrão de chamada consecutiva comum
.map()
e
.join()
, então vamos criar um método de acesso rápido para ele. Vamos chamá-lo de
chain()
, porque une funções que retornam à esquerda ou à direita.
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})`; } }
Voltando à analogia da ferrovia,
.chain()
muda os trilhos se encontrarmos um erro. No entanto, é mais fácil mostrar no diagrama.
Se ocorrer um erro, o método .chain () permite que você alterne para o caminho esquerdo. Observe que os comutadores funcionam apenas de uma maneira.O código ficou um pouco mais limpo:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.chain(zipRow(headerFields)); const rowObjWithDate = rowObj.chain(addDateStr);
Faça algo com valores
A refatoração da função
processRow()
está quase concluída. Mas o que acontece quando retornamos o valor? No final, queremos tomar ações diferentes, dependendo do tipo de situação que temos: esquerda ou direita. Portanto, escreveremos uma função que tomará as medidas apropriadas:
function either(leftFunc, rightFunc, e) { return (e instanceof Left) ? leftFunc(e._val) : rightFunc(e._val); }
Eu trapacei e usei os valores internos dos objetos Esquerdo ou Direito. Mas finja que você não percebeu isso. Agora podemos concluir nossa função: 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); }
E se nos sentirmos particularmente inteligentes, poderemos novamente usar a sintaxe livre: function processRow(headerFields, row) { const rowObjWithDate = right(row) .map(splitFields) .chain(zipRow(headerFields)) .chain(addDateStr); return either(showError, rowToMessage, rowObjWithDate); }
Ambas as versões são bastante bonitas. Sem desenhos try...catch
. E nenhuma instrução if na função de nível superior. Se houver um problema com uma linha específica, simplesmente exibimos uma mensagem de erro no final. E note que processRow()
mencionamos Esquerda ou Direita a única hora no início em que chamamos right()
. O resto são apenas métodos utilizados .map()
e .chain()
para o uso da função seguinte.ap e levante
Parece bom, mas resta considerar um último cenário. Seguindo o nosso exemplo, vamos ver como é possível processar todos os dados CSV, e não apenas cada linha individualmente. Vamos precisar de uma função auxiliar (auxiliar) ou três: function splitCSVToRows(csvData) {
Portanto, temos um ajudante que divide o CSV em linhas. E voltamos à opção com Ou. Agora você pode usar .map()
algumas funções lodash para extrair a barra de título das linhas de dados. Mas nos encontramos em uma situação interessante ... function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail);
Temos campos de cabeçalho e linhas de dados prontos para exibição processRows()
. Mas headerFields
também dataRows
embrulhado em qualquer um. Precisamos de alguma maneira de converter processRows()
para uma função que funcione com Either. Para começar, realizamos o curry processRows
. function processRows(headerFields) { return function processRowsWithHeaderFields(dataRows) {
Agora tudo está pronto para o experimento. Conosco headerFields
, que é um dos dois, envolvido em uma matriz. O que vai acontecer se levarmos headerFields
e chamada em que .map()
a processRows()
? function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail);
Com .map (), uma função externa é chamada aqui processRows()
, mas não interna. Em outras palavras, processRows()
retorna uma função. E desde então .map()
, ainda voltamos. Assim, o resultado é uma função dentro de Ou, que é chamada funcInEither
. Ele pega uma matriz de strings e retorna uma matriz de outras strings. De alguma forma, precisamos pegar essa função e chamá-la com um valor interno dataRows
. Para fazer isso, adicione outro método às nossas classes Esquerda e Direita. Vamos chamá-lo .ap()
de acordo com o padrão .Como sempre, o método não faz nada na faixa esquerda:
E para a classe Right, esperamos outro Ou com uma função:
Agora podemos concluir nossa função 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); }
A essência do método é .ap()
entender um pouco imediatamente (as especificações do Fantasy Land o confundem, mas na maioria das outras línguas o método é usado ao contrário). Se você o descreve mais facilmente, diz: “Eu tenho uma função que geralmente usa dois valores simples. Eu quero transformá-lo em uma função que leva dois ". Se disponível, .ap()
podemos escrever uma função que fará exatamente isso. Vamos chamá-lo liftA2()
, novamente de acordo com o nome padrão. Ela pega uma função simples que espera dois argumentos e a "levanta" para trabalhar com "aplicativos". (esses são objetos que contêm um método .ap()
e um método .of()
). Portanto, liftA2 é a abreviação de "lift aplicativo, dois parâmetros".Portanto, uma função liftA2
pode ser algo como isto: function liftA2(func) { return function runApplicativeFunc(a, b) { return b.ap(a.map(func)); }; }
Nossa função de nível superior a utilizará da seguinte maneira: 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 no CodePen .Certo? Isso é tudo?
Você pode perguntar: o que é melhor do que simples exceções? Não me parece que essa seja uma solução muito complicada para um problema simples? Vamos pensar primeiro por que gostamos de exceções. Se não houvesse exceções, você teria que escrever muitas instruções if em todos os lugares. Sempre escreveremos o código de acordo com o princípio "se o último funcionar, continue, caso contrário, processe o erro". E devemos lidar com esses erros em todo o código. Isso dificulta a compreensão do que está acontecendo. Exceções permitem que você saia do programa se algo der errado. Portanto, você não precisa escrever todos esses ifs. Você pode se concentrar em um caminho de execução bem-sucedido.Mas há um problema. Exceções ocultam demais. Quando você lança uma exceção, transfere o problema de manipulação de erros para outra função. É muito fácil ignorar uma exceção que aparecerá no nível mais alto. O lado bom do Either é que ele permite que você pule para fora do fluxo principal do programa, como se fosse uma exceção. E isso funciona honestamente. Você obtém direita ou esquerda. Você não pode fingir que a opção Esquerda é impossível. No final, você precisa extrair o valor com uma chamada como either()
.Eu sei que parece algum tipo de complexidade. Mas dê uma olhada no código que escrevemos (não nas classes, mas nas funções que os utilizam). Não há muito código de manipulação de exceção. Está quase ausente, com exceção de uma chamada either()
no final csvToMessages()
eprocessRow()
. Esse é o ponto. Com Ou, você tem um tratamento de erros limpo que não pode ser esquecido acidentalmente. Sem qualquer um, carimbar o código e adicionar preenchimento em todos os lugares.Isso não significa que você nunca deve usá-lo try...catch
. Às vezes, é a ferramenta certa e é normal. Mas essa não é a única ferramenta. Ou oferece alguns benefícios que você não tem try...catch
. Então dê uma chance a essa mônada. Mesmo que seja difícil no começo, acho que você vai gostar. Por favor, não use a implementação deste artigo. Experimente uma das bibliotecas famosas, como Crocks , Santuário , Folktale ou Monet . Eles estão melhor servidos. E aqui, por simplicidade, eu perdi alguma coisa.Recursos Adicionais