No hace mucho tiempo comencé a usar Scala en lugar de Java. Parte del proceso de "conversión" entre los idiomas para mí fue aprender a usar Either
s en lugar de (marcado) Exception
s. He estado codificando de esta manera por un tiempo, pero recientemente comencé a preguntarme si esa es realmente una mejor manera de hacerlo.
Una ventaja importante Either
tiene sobre el Exception
mejor rendimiento; un Exception
necesita construir una pila-trace y está siendo lanzada. Sin embargo, hasta donde yo entiendo, tirar Exception
no es la parte exigente, sino construir el rastro de la pila.
Pero entonces, uno siempre puede construir / heredar Exception
s con scala.util.control.NoStackTrace
, y aún más, veo muchos casos en los que el lado izquierdo de un Either
es en realidad un Exception
(renunciando al aumento del rendimiento).
Otra ventaja Either
es la seguridad del compilador; el compilador de Scala no se quejará de s no manejados Exception
(a diferencia del compilador de Java). Pero si no me equivoco, esta decisión se razona con el mismo razonamiento que se está discutiendo en este tema, así que ...
En términos de sintaxis, siento que el Exception
estilo es mucho más claro. Examine los siguientes bloques de código (ambos logran la misma funcionalidad):
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 = ???
Este último me parece mucho más limpio, y el código que maneja la falla (ya sea coincidencia de patrones Either
o try-catch
) es bastante claro en ambos casos.
Entonces mi pregunta es: ¿ Either
por qué usar más (marcado) Exception
?
Actualizar
Después de leer las respuestas, me di cuenta de que podría no haber presentado el núcleo de mi dilema. Mi preocupación no es la falta de try-catch
; uno puede "atrapar" un Exception
con Try
, o usar el catch
para envolver la excepción con Left
.
Mi principal problema con Either
/ Try
viene cuando escribo código que puede fallar en muchos puntos en el camino; En estos escenarios, cuando encuentro una falla, tengo que propagar esa falla en todo mi código, haciendo que el código sea mucho más engorroso (como se muestra en los ejemplos mencionados anteriormente).
En realidad, hay otra forma de romper el código sin Exception
s mediante el uso return
(que de hecho es otro "tabú" en Scala). El código sería aún más claro que el Either
enfoque, y aunque es un poco menos limpio que el Exception
estilo, no habría temor a los mensajes no capturados Exception
.
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))
}
}
Básicamente, el return Left
reemplazo throw new Exception
, y el método implícito en cualquiera de los dos rightOrReturn
, es un complemento para la propagación automática de excepciones en la pila.
Try
. La parte sobre Either
vs Exception
simplemente establece que Either
s debe usarse cuando el otro caso del método es "no excepcional". Primero, esta es una definición muy, muy vaga, en mi humilde opinión. Segundo, ¿realmente vale la pena la pena de sintaxis? Quiero decir, realmente no me importaría usar Either
s si no fuera por la sobrecarga de sintaxis que presentan.
Either
A mí me parece una mónada. Úselo cuando necesite los beneficios de composición funcional que proporcionan las mónadas. O tal vez no .
Either
por sí solo no es una mónada. La proyección hacia el lado izquierdo o hacia el lado derecho es una mónada, pero Either
por sí sola no lo es. Sin embargo, puede convertirlo en una mónada "sesgándolo" al lado izquierdo o al lado derecho. Sin embargo, entonces impartes una cierta semántica en ambos lados de un Either
. Either
Originalmente, Scala era imparcial, pero fue sesgado recientemente, de modo que hoy en día, de hecho, es una mónada, pero la "mónada" no es una propiedad inherente Either
sino más bien un resultado de ser sesgada.