Talvez mônada vs exceções

Respostas:

57

Usar Maybe(ou o primo Eitherque funciona basicamente da mesma maneira, mas permite retornar um valor arbitrário no lugar de Nothing) serve a um propósito ligeiramente diferente das exceções. Em termos de Java, é como ter uma exceção verificada em vez de uma exceção de tempo de execução. Representa algo esperado com o qual você precisa lidar, em vez de um erro que você não esperava.

Portanto, uma função como indexOfretornaria um Maybevalor porque você espera a possibilidade de que o item não esteja na lista. É como retornar nullde uma função, exceto de uma maneira segura para o tipo, que o força a lidar com o nullcaso. Eitherfunciona da mesma maneira, exceto que você pode retornar informações associadas ao caso de erro; portanto, é realmente mais semelhante a uma exceção que Maybe.

Então, quais são as vantagens da abordagem Maybe/ Either? Por um lado, é um cidadão de primeira classe do idioma. Vamos comparar uma função usando Eithercom uma que lança uma exceção. Para o caso de exceção, seu único recurso real é uma try...catchdeclaração. Para a Eitherfunção, você pode usar os combinadores existentes para tornar o controle de fluxo mais claro. Aqui estão alguns exemplos:

Primeiro, digamos que você queira tentar várias funções que podem ter erros consecutivos até que você obtenha uma que não funcione. Se você não obtiver nenhum erro, deseje retornar uma mensagem de erro especial. Este é realmente um padrão muito útil, mas seria uma dor horrível try...catch. Felizmente, como Eitheré apenas um valor normal, você pode usar as funções existentes para tornar o código muito mais claro:

firstThing <|> secondThing <|> throwError (SomeError "error message")

Outro exemplo é ter uma função opcional. Digamos que você tenha várias funções para executar, incluindo uma que tenta otimizar uma consulta. Se isso falhar, você deseja que todo o resto seja executado. Você pode escrever um código como:

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

Ambos os casos são mais claros e mais curtos que o uso try..catche, mais importante, mais semânticos. Usar uma função como <|>ou optionaltorna suas intenções muito mais claras do que usar try...catchpara sempre lidar com exceções.

Observe também que você não precisa desarrumar seu código com linhas como if a == Nothing then Nothing else ...! O objetivo principal de tratar Maybee Eithercomo mônada é evitar isso. Você pode codificar a semântica de propagação na função de ligação para obter as verificações de nulo / erro gratuitamente. O único momento em que você precisa checar explicitamente é se deseja retornar algo diferente de Nothingum a Nothing, e mesmo assim é fácil: existem várias funções de biblioteca padrão para tornar esse código mais agradável.

Finalmente, outra vantagem é que um tipo Maybe/ Eitheré apenas mais simples. Não há necessidade de estender o idioma com palavras-chave adicionais ou estruturas de controle - tudo é apenas uma biblioteca. Como são apenas valores normais, isso simplifica o sistema de tipos - em Java, é necessário diferenciar entre tipos (por exemplo, o tipo de retorno) e efeitos (por exemplo, throwsinstruções) onde você não usaria Maybe. Eles também se comportam como qualquer outro tipo definido pelo usuário - não é necessário ter um código especial de tratamento de erros inserido no idioma.

Outra vitória é que Maybe/ Eithersão functores e mônadas, o que significa que eles podem tirar proveito das funções de fluxo de controle de mônadas existentes (das quais existe um número razoável) e, em geral, jogam bem junto com outras mônadas.

Dito isto, existem algumas ressalvas. Por um lado, Maybenem Eithersubstitua exceções não verificadas . Você desejará alguma outra maneira de lidar com coisas como dividir por 0 simplesmente porque seria doloroso fazer com que cada divisão retornasse um Maybevalor.

Outro problema é o retorno de vários tipos de erros (isso se aplica apenas a Either). Com exceções, você pode lançar diferentes tipos de exceções na mesma função. com Either, você obtém apenas um tipo. Isso pode ser superado com a sub-digitação ou um ADT contendo todos os diferentes tipos de erros como construtores (essa segunda abordagem é a que geralmente é usada no Haskell).

Ainda assim, prefiro a abordagem Maybe/ Eitherporque acho mais simples e flexível.

Tikhon Jelvis
fonte
Concordo principalmente, mas não acho que o exemplo "opcional" faça sentido - parece que você pode fazer o mesmo com exceções: void Opcional (Action act) {try {act (); } catch (Exceção) {}}
Erroratz
11
  1. Uma exceção pode conter mais informações sobre a origem de um problema. OpenFile()pode jogar FileNotFoundou NoPermissionou TooManyDescriptorsetc. A Nenhum não carrega essas informações.
  2. Exceções podem ser usadas em contextos que não possuem valores de retorno (por exemplo, com construtores em idiomas que os possuem).
  3. Uma exceção permite que você envie facilmente as informações para cima da pilha, sem muitas if None return Noneinstruções de estilo.
  4. O tratamento de exceções quase sempre traz maior impacto no desempenho do que apenas retornar um valor.
  5. Mais importante ainda, uma exceção e uma mônada Talvez têm objetivos diferentes - uma exceção é usada para significar um problema, enquanto uma Talvez não.

    "Enfermeira, se houver um paciente na sala 5, você pode pedir para ele esperar?"

    • Talvez a mônada: "Doutor, não há paciente no quarto 5."
    • Exceção: "Doutor, não há quarto 5!"

    (observe o "se" - isso significa que o médico está esperando uma mônada Talvez)

Carvalho
fonte
7
Não concordo com os pontos 2 e 3. Em particular, 2 não representa um problema em idiomas que usam mônadas porque esses idiomas têm convenções que não geram o dilema de ter que lidar com falhas durante a construção. Em relação a 3, idiomas com mônadas têm sintaxe apropriada para manipular mônadas de maneira transparente que não requer verificação explícita (por exemplo, os Nonevalores podem ser apenas propagados). Seu ponto 5 é apenas meio certo ... a questão é: quais situações são inequivocamente excepcionais? Como se vê ... não muitos .
Konrad Rudolph
3
Em relação a (2), você pode escrever bindde tal maneira que o teste Nonenão implique uma sobrecarga sintática no entanto. Um exemplo muito simples, o C # sobrecarrega os Nullableoperadores adequadamente. Nenhuma verificação é Nonenecessária, mesmo ao usar o tipo. É claro que a verificação ainda está concluída (é do tipo seguro), mas nos bastidores e não atrapalha seu código. O mesmo se aplica, em certo sentido, à sua objeção à minha objeção a (5), mas concordo que nem sempre isso se aplica.
Konrad Rudolph
4
@ Oak: Em relação a (3), todo o ponto de tratar Maybecomo uma mônada é tornar a propagação Noneimplícita. Isso significa que, se você deseja retornar um Nonedado None, não precisa escrever nenhum código especial. O único momento em que você precisa corresponder é se deseja fazer algo especial None. Você nunca precisa de um if None then Nonetipo de afirmação.
Tikhon Jelvis
5
@ Oak: isso é verdade, mas esse não era realmente o meu ponto. O que estou dizendo é que você começa a nullverificar exatamente assim (por exemplo if Nothing then Nothing) de graça, porque Maybeé uma mônada. Está codificado na definição de bind ( >>=) para Maybe.
Tikhon Jelvis
2
Além disso, em relação a (1): você pode escrever facilmente uma mônada que pode transportar informações de erro (por exemplo Either) que se comportam da mesma maneira Maybe. Alternar entre os dois é realmente bastante simples, porque Maybeé realmente apenas um caso especial de Either. (Em Haskell, você poderia pensar Maybecomo Either ().)
Tikhon Jelvis
6

"Talvez" não substitui exceções. Exceções devem ser usadas em casos excepcionais (por exemplo: abrir uma conexão db e o servidor db não existe, embora deva existir). "Talvez" é para modelar uma situação em que você pode ou não ter um valor válido; digamos que você esteja obtendo um valor de um dicionário para uma chave: ele pode estar lá ou não - não há nada de "excepcional" em nenhum desses resultados.

Nemanja Trifunovic
fonte
2
Na verdade, eu tenho um pouco de código envolvendo dicionários em que uma chave ausente significa que alguma coisa deu errado em algum momento, provavelmente um bug no meu código ou no código que converteu a entrada no que for usado naquele momento.
3
@ delnan: Eu não estou dizendo que um valor ausente nunca pode ser um sinal de um estado excepcional - ele simplesmente não precisa ser. Talvez realmente modele uma situação em que uma variável pode ou não ter um valor válido - pense em tipos anuláveis ​​em C #.
Nemanja Trifunovic
6

Segundo a resposta de Tikhon, mas acho que há um ponto prático muito importante que todo mundo está perdendo:

  1. Mecanismos de manipulação de exceção, pelo menos nos idiomas principais, estão intimamente acoplados a threads individuais.
  2. O Eithermecanismo não está acoplado a threads.

Então, algo que estamos vendo hoje em dia na vida real é que muitas soluções de programação assíncronas estão adotando uma variante do Eitherestilo de manipulação de erros. Considere as promessas de Javascript , conforme detalhado em qualquer um desses links:

O conceito de promessas permite que você escreva códigos assíncronos como este (extraídos do último link):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

Basicamente, uma promessa é um objeto que:

  1. Representa o resultado de uma computação assíncrona, que pode ou não ter sido concluída ainda;
  2. Permite encadear operações adicionais para executar seu resultado, que será acionado quando esse resultado estiver disponível e cujos resultados, por sua vez, estão disponíveis como promessas;
  3. Permite conectar um manipulador de falhas que será chamado se o cálculo da promessa falhar. Se não houver manipulador, o erro será propagado para manipuladores posteriores na cadeia.

Basicamente, como o suporte a exceções nativas do idioma não funciona quando a computação está acontecendo em vários segmentos, uma implementação promissora deve fornecer um mecanismo de tratamento de erros, e eles acabam sendo mônadas semelhantes aos tipos Maybe/ de Haskell Either.

sacundim
fonte
Não tem nada a ver com threads. O JavaScript no navegador sempre é executado em um thread e não em vários threads. Mas você ainda não pode usar a exceção porque não sabe quando sua função será chamada no futuro. Assíncrono não significa automaticamente um envolvimento de threads. Essa também é a razão pela qual você não pode trabalhar com exceções. Você só pode buscar uma exceção se chamar uma função e imediatamente ela for executada. Mas todo o objetivo do Asynchronous é que ele seja executado no futuro, geralmente quando algo terminar, e não imediatamente. É por isso que você não pode usar exceções lá.
David Raab
1

O sistema do tipo Haskell exigirá que o usuário reconheça a possibilidade de a Nothing, enquanto as linguagens de programação geralmente não exigem que uma exceção seja capturada. Isso significa que saberemos, em tempo de compilação, que o usuário verificou um erro.

chrisaycock
fonte
1
Há exceções verificadas em Java. E as pessoas frequentemente os tratavam mal.
Vladimir
1
@ DairT'arg ButJava requer verificações de erros de E / S (como exemplo), mas não para NPEs. Em Haskell, é o contrário: o equivalente nulo (Maybe) está marcado, mas os erros de E / S simplesmente lançam exceções (não verificadas). Não tenho certeza da diferença que isso faz, mas não ouço Haskellers reclamando de Maybe e acho que muitas pessoas gostariam que as NPEs fossem verificadas.
1
@delnan As pessoas querem que o NPE seja verificado? Eles devem estar loucos. E eu quero dizer isso. Tornar o NPE verificado apenas faz sentido se você tiver uma maneira de evitá- lo em primeiro lugar (tendo referências não nulos, que o Java não possui).
27530 Konrad Rudolph
5
@KonradRudolph Sim, as pessoas provavelmente não querem que seja verificado no sentido em que terão que adicionar um throws NPEa cada assinatura e catch(...) {throw ...} a cada corpo de método. Mas acredito que há um mercado para checagem no mesmo sentido que em Talvez: a nulidade é opcional e rastreada no sistema de tipos.
1
@ delnan Ah, então eu concordo.
Konrad Rudolph
0

Talvez a mônada seja basicamente o mesmo que o uso mais comum da linguagem da verificação de "nulo significa erro" (exceto que exige que o nulo seja verificado) e possui as mesmas vantagens e desvantagens.

Telastyn
fonte
8
Bem, ele não tem as mesmas desvantagens, pois pode ser verificado estaticamente quando usado corretamente. Não há equivalente a uma exceção de ponteiro nulo ao usar talvez mônadas (novamente, assumindo que elas sejam usadas corretamente).
Konrad Rudolph
De fato. Como você acha que isso deve ser esclarecido?
Telastyn
@ Konrad Isso porque, para usar o valor (possivelmente) no Talvez, você deve verificar Nenhum (embora, é claro, haja a capacidade de automatizar grande parte dessa verificação). Outras línguas mantêm esse tipo de coisa manual (principalmente por razões filosóficas, acredito).
Donal Fellows
2
@Donal Sim, mas observe que a maioria dos idiomas que implementam mônadas fornece açúcar sintático apropriado, de modo que essa verificação pode muitas vezes ser ocultada completamente. Por exemplo, você pode simplesmente adicionar dois Maybenúmeros escrevendo a + b sem a necessidade de verificar None, e o resultado é mais uma vez um valor opcional.
Konrad Rudolph
2
A comparação com tipos anuláveis ​​é verdadeira para o Maybe tipo , mas o uso Maybecomo mônada adiciona açúcar de sintaxe que permite expressar a lógica de nulidade muito mais elegante.
Tdammers
0

O tratamento de exceções pode ser uma verdadeira dor de fatoração e teste. Eu sei que o python fornece boa sintaxe "com" que permite interceptar exceções sem o rígido bloco "try ... catch". Mas em Java, por exemplo, os blocos try catch são grandes, padronizados, detalhados ou extremamente detalhados e difíceis de quebrar. Além disso, o Java adiciona todo o ruído em torno das exceções verificadas versus não verificadas.

Se, em vez disso, sua mônada captura exceções e as trata como uma propriedade do espaço monádico (em vez de alguma anomalia de processamento), você pode misturar e combinar funções vinculadas a esse espaço, independentemente do que elas lançam ou capturam.

Se, melhor ainda, sua mônada evita condições em que exceções podem ocorrer (como enviar uma verificação nula para Maybe), é ainda melhor. se ... então é muito, muito mais fácil fatorar e testar do que tentar ... capturar.

Pelo que vi, Go está adotando uma abordagem semelhante, especificando que cada função retorna (resposta, erro). É o mesmo que "elevar" a função para um espaço de mônada, em que o tipo de resposta principal é decorado com uma indicação de erro e efetivamente lança exceções.

Roubar
fonte