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 Eithers en lugar de (marcado) Exceptions. 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 Eithertiene sobre el Exceptionmejor rendimiento; un Exceptionnecesita construir una pila-trace y está siendo lanzada. Sin embargo, hasta donde yo entiendo, tirar Exceptionno es la parte exigente, sino construir el rastro de la pila.
Pero entonces, uno siempre puede construir / heredar Exceptions con scala.util.control.NoStackTrace, y aún más, veo muchos casos en los que el lado izquierdo de un Eitheres en realidad un Exception(renunciando al aumento del rendimiento).
Otra ventaja Eitheres 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 Exceptionestilo 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 Eithero try-catch) es bastante claro en ambos casos.
Entonces mi pregunta es: ¿ Eitherpor 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 Exceptioncon Try, o usar el catchpara envolver la excepción con Left.
Mi principal problema con Either/ Tryviene 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 Exceptions mediante el uso return(que de hecho es otro "tabú" en Scala). El código sería aún más claro que el Eitherenfoque, y aunque es un poco menos limpio que el Exceptionestilo, 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 Leftreemplazo 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 Eithervs Exceptionsimplemente establece que Eithers 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 Eithers si no fuera por la sobrecarga de sintaxis que presentan.
EitherA mí me parece una mónada. Úselo cuando necesite los beneficios de composición funcional que proporcionan las mónadas. O tal vez no .
Eitherpor sí solo no es una mónada. La proyección hacia el lado izquierdo o hacia el lado derecho es una mónada, pero Eitherpor 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. EitherOriginalmente, 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 Eithersino más bien un resultado de ser sesgada.