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.
Try
. A parte sobre Either
vs Exception
simplesmente afirma que Either
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 usar Either
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, mas Either
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 um Either
. O Scala's Either
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.