Há pouco tempo, comecei a usar o Scala em vez do Java. Parte do processo de "conversão" entre os idiomas para mim foi aprender a usar Either
s em vez de (marcado) Exception
s. Eu tenho codificado dessa maneira por um tempo, mas recentemente comecei a me perguntar se esse é realmente o melhor caminho a percorrer.
Uma grande vantagem Either
tem mais Exception
é melhor desempenho; um Exception
precisa criar um rastreamento de pilha e está sendo lançado. Até onde eu entendo, jogar a Exception
parte não é a parte mais exigente, mas construir o rastreamento da pilha é.
Mas então, sempre é possível construir / herdar Exception
s com scala.util.control.NoStackTrace
, e mais ainda, vejo muitos casos em que o lado esquerdo de um Either
é de fato um Exception
(renunciando ao aumento de desempenho).
Uma outra vantagem Either
é a segurança do compilador; o compilador Scala não se queixará de Exception
s não manipulados (ao contrário do compilador do Java). Mas se não me engano, essa decisão é fundamentada com o mesmo raciocínio discutido neste tópico, então ...
Em termos de sintaxe, sinto que o Exception
estilo é muito mais claro. Examine os seguintes blocos de código (ambos obtendo a mesma funcionalidade):
Either
estilo:
def compute(): Either[String, Int] = {
val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")
val bEithers: Iterable[Either[String, Int]] = someSeq.map {
item => if (someCondition(item)) Right(item.toInt) else Left("bad")
}
for {
a <- aEither.right
bs <- reduce(bEithers).right
ignore <- validate(bs).right
} yield compute(a, bs)
}
def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code
def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()
def compute(a: String, bs: Iterable[Int]): Int = ???
Exception
estilo:
@throws(classOf[ComputationException])
def compute(): Int = {
val a = if (someCondition) "good" else throw new ComputationException("bad")
val bs = someSeq.map {
item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
}
if (bs.sum > 22) throw new ComputationException("bad")
compute(a, bs)
}
def compute(a: String, bs: Iterable[Int]): Int = ???
O último parece muito mais limpo para mim, e o código que trata da falha (com correspondência de padrões Either
ou try-catch
) é bastante claro nos dois casos.
Então, minha pergunta é - por que usar Either
over (marcado) Exception
?
Atualizar
Depois de ler as respostas, percebi que poderia não ter apresentado o cerne do meu dilema. Minha preocupação não é com a falta de try-catch
; pode-se "pegar" um Exception
com Try
ou usar o catch
para quebrar a exceção com Left
.
Meu principal problema com Either
/ Try
surge quando escrevo código que pode falhar em muitos pontos ao longo do caminho; nesses cenários, ao encontrar uma falha, tenho que propagar essa falha por todo o meu código, tornando o código muito mais complicado (como mostra os exemplos mencionados acima).
Na verdade, existe outra maneira de quebrar o código sem Exception
s usando return
(que na verdade é outro "tabu" no Scala). O código ainda seria mais claro que a Either
abordagem e, embora um pouco menos limpo que o Exception
estilo, não haveria medo de Exception
s não capturados .
def compute(): Either[String, Int] = {
val a = if (someCondition) "good" else return Left("bad")
val bs: Iterable[Int] = someSeq.map {
item => if (someCondition(item)) item.toInt else return Left("bad")
}
if (bs.sum > 22) return Left("bad")
val c = computeC(bs).rightOrReturn(return _)
Right(computeAll(a, bs, c))
}
def computeC(bs: Iterable[Int]): Either[String, Int] = ???
def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???
implicit class ConvertEither[L, R](either: Either[L, R]) {
def rightOrReturn(f: (Left[L, R]) => R): R = either match {
case Right(r) => r
case Left(l) => f(Left(l))
}
}
Basicamente, as return Left
substituições throw new Exception
e o método implícito em qualquer uma delas rightOrReturn
são um complemento para a propagação automática de exceções na pilha.
fonte
Try
. A parte sobreEither
vsException
simplesmente afirma queEither
s deve ser usado quando o outro caso do método for "não excepcional". Primeiro, essa é uma definição muito, muito vaga. Segundo, vale mesmo a pena a sintaxe? Quero dizer, eu realmente não me importaria de usarEither
s se não fosse pela sobrecarga de sintaxe que eles apresentam.Either
parece uma mônada para mim. Use-o quando precisar dos benefícios de composição funcional que as mônadas fornecem. Ou talvez não .Either
por si só não é uma mônada. A projeção para o lado esquerdo ou direito é uma mônada, masEither
por si só não é. Você pode torná- la uma mônada, "influenciando" para o lado esquerdo ou o lado direito. No entanto, você transmite uma certa semântica nos dois lados de umEither
. O Scala'sEither
era originalmente imparcial, mas era tendencioso recentemente, de modo que atualmente é de fato uma mônada, mas a "monadness" não é uma propriedade inerente,Either
mas sim um resultado de ser tendenciosa.Respostas:
Se você usar apenas um bloco
Either
exatamente como um imperativotry-catch
, é claro que parecerá uma sintaxe diferente para fazer exatamente a mesma coisa.Eithers
são um valor . Eles não têm as mesmas limitações. Você pode:Eithers
não precisa estar ao lado da parte "try". Pode até ser compartilhado em uma função comum. Isso torna muito mais fácil separar as preocupações adequadamente.Eithers
em estruturas de dados. Você pode percorrê-las e consolidar mensagens de erro, encontrar a primeira que não falhou, avaliá-las preguiçosamente ou semelhante.Eithers
? Você pode escrever funções auxiliares para facilitar o trabalho para seus casos de uso específicos. Ao contrário do try-catch, você não fica permanentemente preso apenas à interface padrão criada pelos designers de idiomas.Observe que na maioria dos casos de uso, a
Try
possui uma interface mais simples, possui a maioria dos benefícios acima e geralmente também atende às suas necessidades. UmOption
é ainda mais simples se você só se importa com sucesso ou falha e não precisa de nenhuma informação de estado, como mensagens de erro sobre o caso de falha.Na minha experiência,
Eithers
não são realmente preferidos, aTrys
menos queLeft
seja algo diferente de umaException
, talvez uma mensagem de erro de validação, e você queira agregar osLeft
valores. Por exemplo, você está processando alguma entrada e deseja coletar todas as mensagens de erro, e não apenas erros na primeira. Essas situações não surgem com muita frequência na prática, pelo menos para mim.Olhe além do modelo try-catch e você encontrará
Eithers
muito mais agradável para trabalhar.Atualização: esta pergunta ainda está recebendo atenção e eu não tinha visto a atualização do OP esclarecendo a questão da sintaxe, então pensei em adicionar mais.
Como um exemplo de quebra da mentalidade de exceção, é assim que eu normalmente escreveria seu código de exemplo:
Aqui você pode ver que a semântica de
filterOrElse
capturar perfeitamente o que estamos fazendo principalmente nesta função: verificar condições e associar mensagens de erro às falhas especificadas. Como esse é um caso de uso muito comum,filterOrElse
está na biblioteca padrão, mas se não fosse, você poderia criá-lo.Este é o ponto em que alguém cria um contra-exemplo que não pode ser resolvido exatamente da mesma maneira que o meu, mas o que estou tentando destacar é que não há apenas uma maneira de usar
Eithers
, ao contrário das exceções. Existem maneiras de simplificar o código para quase todas as situações de tratamento de erros, se você explorar todas as funções disponíveis para usoEithers
e estiver aberto à experimentação.fonte
try
ecatch
é um argumento justo, mas isso não poderia ser facilmente alcançado comTry
? 2. O armazenamento deEither
s nas estruturas de dados pode ser feito juntamente com othrows
mecanismo; não é simplesmente uma camada extra além das falhas de manipulação? 3. Você pode definitivamente estenderException
s. Claro, você não pode mudar atry-catch
estrutura, mas lidar com falhas é tão comum que eu definitivamente quero que o idioma me forneça um padrão para isso (que não sou obrigado a usar). É como dizer que a compreensão é ruim porque é um clichê para operações monádicas.Either
s podem ser estendidos, onde claramente eles não podem (sendo umsealed trait
com apenas duas implementações, ambasfinal
). Você pode elaborar sobre isso? Estou assumindo que você não quis dizer o significado literal de estender.Os dois são realmente muito diferentes.
Either
A semântica é muito mais geral do que apenas representar cálculos com potencial de falha.Either
permite retornar um dos dois tipos diferentes. É isso aí.Agora, um desses dois tipos pode ou não representar um erro e o outro pode ou não representar sucesso, mas esse é apenas um possível caso de uso de muitos.
Either
é muito, muito mais geral que isso. Você também pode ter um método que leia a entrada do usuário que tem permissão para inserir um nome ou retornar uma dataEither[String, Date]
.Ou, pense nos literais lambda em C♯: eles avaliam para um
Func
ou umExpression
, ou seja, avaliam para uma função anônima ou avaliam para um AST. Em C♯, isso acontece "magicamente", mas se você quisesse dar um tipo adequado, seriaEither<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>
. No entanto, nenhuma das duas opções é um erro e nenhuma das duas é "normal" ou "excepcional". Eles são apenas dois tipos diferentes que podem ser retornados da mesma função (ou, neste caso, atribuídos ao mesmo literal). [Nota: na verdade,Func
deve ser dividido em outros dois casos, porque C♯ não possui umUnit
tipo e, portanto, algo que não retorna nada não pode ser representado da mesma maneira que algo que retorna algo,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>
Agora,
Either
pode ser usado para representar um tipo de sucesso e um tipo de falha. E se você concordar com uma ordenação consistente dos dois tipos, você pode até mesmo viésEither
a preferir um dos dois tipos, tornando assim possível para propagar falhas através de encadeamento monádico. Mas nesse caso, você está dando uma certa semânticaEither
que ela não possui necessariamente por si só.Um
Either
que tem um de seus dois tipos corrigidos para ser um tipo de erro e é inclinado para um lado para propagar erros, geralmente é chamado deError
mônada e seria uma opção melhor para representar uma computação com potencial de falha. Em Scala, esse tipo de tipo é chamadoTry
.Faz muito mais sentido comparar o uso de exceções do
Try
que comEither
.Portanto, esta é a razão nº 1 porque
Either
pode ser usada sobre exceções verificadas:Either
é muito mais geral do que exceções. Pode ser usado nos casos em que as exceções nem se aplicam.No entanto, você provavelmente estava perguntando sobre o caso restrito em que apenas usa
Either
(ou mais provávelTry
) como um substituto para exceções. Nesse caso, ainda existem algumas vantagens, ou melhor, diferentes abordagens possíveis.A principal vantagem é que você pode adiar a manipulação do erro. Você apenas armazena o valor de retorno, sem sequer examinar se é um sucesso ou um erro. (Lide com isso mais tarde.) Ou passe para outro lugar. (Deixe alguém se preocupar com isso.) Colete-os em uma lista. (Manipule todos eles de uma vez.) Você pode usar o encadeamento monádico para propagá-los ao longo de um pipeline de processamento.
A semântica das exceções é conectada à linguagem. No Scala, as exceções desenrolam a pilha, por exemplo. Se você permitir que eles atinjam um manipulador de exceção, ele não poderá voltar ao local onde ocorreu e corrigi-lo. OTOH, se você o pegar mais baixo na pilha antes de desenrolá-lo e destruir o contexto necessário para corrigi-lo, poderá estar em um componente de nível muito baixo para tomar uma decisão informada sobre como proceder.
Este é diferente em Smalltalk, por exemplo, onde exceções não desanuviar a pilha e são resumable, e você pode pegar o alto exceção para cima na pilha (possivelmente tão alto quanto o depurador), arrumar o erro waaaaayyyyy para baixo na empilhar e retomar a execução a partir daí. Ou as condições CommonLisp em que aumentar, capturar e manipular são três coisas diferentes em vez de duas (manipular e capturar e manipular), como as exceções.
O uso de um tipo de retorno de erro real em vez de uma exceção permite que você faça coisas semelhantes e muito mais, sem precisar modificar o idioma.
E depois há o elefante óbvio na sala: exceções são um efeito colateral. Os efeitos colaterais são maus. Nota: não estou dizendo que isso seja necessariamente verdade. Mas é um ponto de vista que algumas pessoas têm e, nesse caso, a vantagem de um tipo de erro sobre exceções é óbvia. Portanto, se você quiser fazer programação funcional, poderá usar tipos de erro pelo único motivo de que eles são a abordagem funcional. (Observe que Haskell tem exceções. Observe também que ninguém gosta de usá-las.)
fonte
Exception
.Either
parece ser uma ótima ferramenta, mas sim, estou perguntando especificamente sobre como lidar com falhas e prefiro usar uma ferramenta muito mais específica para minha tarefa do que uma ferramenta totalmente poderosa. Adiar o tratamento de falhas é um argumento justo, mas pode-se simplesmente usarTry
um método lançando umException
se ele não quiser lidar com isso no momento. O desenrolamento de rastreamento de pilha não afetaEither
/Try
também? Você pode repetir os mesmos erros ao propagá-los incorretamente; saber quando lidar com uma falha não é trivial nos dois casos.O bom das exceções é que o código de origem já classificou a circunstância excepcional para você. A diferença entre an
IllegalStateException
e aBadParameterException
é muito mais fácil de diferenciar em idiomas tipificados mais fortes. Se você tentar analisar "bom" e "ruim", não estará aproveitando a ferramenta extremamente poderosa à sua disposição, a saber, o compilador Scala. Suas próprias extensõesThrowable
podem incluir o máximo de informações necessárias, além da sequência de mensagens de texto.Essa é a verdadeira utilidade do
Try
ambiente Scala,Throwable
pois atua como o invólucro dos itens à esquerda para facilitar a vida.fonte
Either
tem problemas de estilo porque não é uma mônada verdadeira (?); a arbitrariedade doleft
valor se afasta do maior valor agregado quando você coloca adequadamente informações sobre condições excepcionais em uma estrutura adequada. Portanto, comparar o uso deEither
um caso retornando cadeias no lado esquerdo,try/catch
em comparação com o outro caso, usando o tipo digitado corretamenteThrowables
não é adequado. Por causa do valor deThrowables
pelo fornecimento de informações, ea forma concisa queTry
alçasThrowables
,Try
é uma aposta melhor do que qualquer um ...Either
outry/catch
try-catch
. Como dito na pergunta, o último parece muito mais limpo para mim, e o código que lida com a falha (correspondência de padrão no Either ou try-catch) é bastante claro nos dois casos.