¿Por qué usar Excepción sobre (marcada)?

17

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.

Eyal Roth
fuente
Lea esto: mauricio.github.io/2014/02/17/…
Robert Harvey
@RobertHarvey Parece que la mayoría de los artículos discuten 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.
Eyal Roth
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 .
Robert Harvey
2
@RobertHarvey: Técnicamente hablando, 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.
Jörg W Mittag
@ JörgWMittag: Bueno, bien. Eso significa que puede usarlo por toda su bondad monádica, y no preocuparse particularmente por lo "limpio" que es el código resultante (aunque sospecho que el código de continuación de estilo funcional en realidad sería más limpio que la versión Exception).
Robert Harvey

Respuestas:

13

Si solo usa un bloque Eitherexactamente como un try-catchbloque imperativo , por supuesto, se verá como una sintaxis diferente para hacer exactamente lo mismo. Eithersson un valor . No tienen las mismas limitaciones. Usted puede:

  • Detenga su hábito de mantener sus bloques de prueba pequeños y contiguos. La parte de "captura" Eithersno necesita estar justo al lado de la parte de "probar". Incluso puede compartirse en una función común. Esto hace que sea mucho más fácil separar las preocupaciones adecuadamente.
  • Tienda Eithers en estructuras de datos. Puede recorrerlos y consolidar mensajes de error, encontrar el primero que no falló, evaluarlos perezosamente o similar.
  • Extiéndelos y personalízalos. No me gustan las repeticiones con las que te ves obligado a escribirEithers ? Puede escribir funciones de ayuda para que sea más fácil trabajar con ellas para sus casos de uso particulares. A diferencia de try-catch, no está permanentemente atascado con solo la interfaz predeterminada que idearon los diseñadores de idiomas.

Tenga en cuenta que para la mayoría de los casos de uso, a Trytiene una interfaz más simple, tiene la mayoría de los beneficios anteriores y, por lo general, también satisface sus necesidades. UnOption es aún más sencillo si lo único que importa es el éxito o el fracaso y no necesitan ninguna información de estado como mensajes de error sobre el caso fracaso.

En mi experiencia, Eithersno se prefieren realmente a Trysmenos que Leftsea ​​algo diferente a Exception, quizás un mensaje de error de validación, y desee agregar los Leftvalores. Por ejemplo, está procesando alguna entrada y desea recopilar todos los mensajes de error, no solo el error en el primero. Esas situaciones no surgen muy a menudo en la práctica, al menos para mí.

Mire más allá del modelo try-catch y encontrará Eithers mucho más agradable trabajar con él.

Actualización: esta pregunta todavía está recibiendo atención, y no había visto la actualización del OP aclarando la pregunta de sintaxis, así que pensé en agregar más.

Como ejemplo de romper con la mentalidad de excepción, así es como normalmente escribiría su código de ejemplo:

def compute(): Either[String, Int] = {
  Right(someSeq)
    .filterOrElse(someACondition,            "someACondition failed")
    .filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
    .map(_ map {_.toInt})
    .filterOrElse(_.sum <= 22, "Sum is greater than 22")
    .map(compute("good",_))
}

Aquí puede ver que la semántica de filterOrElsecapturar perfectamente lo que estamos haciendo principalmente en esta función: verificar condiciones y asociar mensajes de error con las fallas dadas. Dado que este es un caso de uso muy común,filterOrElse está en la biblioteca estándar, pero si no lo fuera, podría crearlo.

Este es el punto en el que a alguien se le ocurre un contraejemplo que no puede resolverse exactamente de la misma manera que el mío, pero el punto que estoy tratando de hacer es que no hay una sola forma de usar Eithers, a diferencia de las excepciones. Hay formas de simplificar el código para la mayoría de las situaciones de manejo de errores, si explora todas las funciones disponibles para usar Eithersy está abierto a la experimentación.

Karl Bielefeldt
fuente
1. Ser capaz de separar el tryy catches un argumento justo, pero ¿no podría lograrse esto fácilmente Try? 2. El almacenamiento Eitherde datos en estructuras de datos podría realizarse junto con el throwsmecanismo; ¿No es simplemente una capa adicional además de las fallas de manejo? 3. Definitivamente puede extender Exceptions. Claro, no puede cambiar la try-catchestructura, pero el manejo de las fallas es tan común que definitivamente quiero que el lenguaje me dé una referencia para eso (que no estoy obligado a usar). Eso es como decir que la comprensión es mala porque es una base para las operaciones monádicas.
Eyal Roth
Me acabo de dar cuenta de que dijiste que los Eithers pueden extenderse, donde claramente no pueden (siendo un sealed traitcon solo dos implementaciones, ambos final). Puedes profundizar sobre eso? Supongo que no quiso decir el significado literal de extender.
Eyal Roth
1
Quise decir extendido como en la palabra en inglés, no como la palabra clave de programación. Agregar funcionalidad creando funciones para manejar patrones comunes.
Karl Bielefeldt
8

Los dos son en realidad muy diferentes. EitherLa semántica es mucho más general que solo representar cálculos potencialmente fallidos.

Eitherle permite devolver uno de los dos tipos diferentes. Eso es.

Ahora, uno de esos dos tipos puede o no representar un error y el otro puede o no representar el éxito, pero ese es solo un posible caso de uso de muchos. Eitheres mucho, mucho más general que eso. También podría tener un método que lea la entrada del usuario al que se le permite ingresar un nombre o una fecha para devolver unEither[String, Date] .

O piense en los literales lambda en C♯: o evalúan a an Funco an Expression, es decir, evalúan a una función anónima o evalúan a un AST. En C♯, esto sucede "mágicamente", pero si quisieras darle un tipo apropiado, sería Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>. Sin embargo, ninguna de las dos opciones es un error, y ninguna de las dos es "normal" o "excepcional". Son solo dos tipos diferentes que pueden devolverse desde la misma función (o en este caso, atribuidos al mismo literal). [Nota: en realidad, Functiene que dividirse en otros dos casos, porque C♯ no tiene un Unittipo, por lo que algo que no devuelve nada no puede representarse de la misma manera que algo que devuelve algo,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>

Ahora, Either se puede usar para representar un tipo de éxito y un tipo de falla. Y si está de acuerdo con un orden consistente de los dos tipos, incluso puede inclinarse Either para preferir uno de los dos tipos, lo que permite propagar fallas a través del encadenamiento monádico. Pero en ese caso, está impartiendo una cierta semántica Eitherque no necesariamente posee por sí sola.

Un Eitherque tiene uno de sus dos tipos fijos para ser un tipo de error y está sesgado hacia un lado para propagar errores, generalmente se llama Errormónada, y sería una mejor opción para representar un cálculo potencialmente fallido. En Scala, este tipo de tipo se llama Try.

Tiene mucho más sentido comparar el uso de excepciones con Tryque con Either.

Entonces, esta es la razón # 1 por la que Eitherpodría usarse sobre excepciones marcadas:Either es mucho más general que las excepciones. Se puede usar en casos donde las excepciones ni siquiera se aplican.

Sin embargo, probablemente estaba preguntando sobre el caso restringido en el que solo usa Either(o más probablemente Try) como un reemplazo para las excepciones. En ese caso, todavía hay algunas ventajas, o más bien posibles enfoques diferentes.

La principal ventaja es que puede diferir el manejo del error. Simplemente almacena el valor de retorno, sin siquiera mirar si es un éxito o un error. (Manejarlo más tarde.) O pasarlo a otro lugar. (Deja que alguien más se preocupe por eso). Colecciónalos en una lista. (Manejarlos todos a la vez). Puede usar el encadenamiento monádico para propagarlos a lo largo de una tubería de procesamiento.

La semántica de las excepciones está integrada en el lenguaje. En Scala, las excepciones desenrollan la pila, por ejemplo. Si los dejas aparecer como un controlador de excepciones, entonces el controlador no puede volver a donde sucedió y solucionarlo. OTOH, si lo atrapa más abajo en la pila antes de desenrollarlo y destruir el contexto necesario para solucionarlo, entonces podría estar en un componente que es demasiado bajo para tomar una decisión informada sobre cómo proceder.

Esto es diferente en Smalltalk, por ejemplo, donde las excepciones no desenrollan la pila y son reanudables, y puede atrapar la excepción en la parte superior de la pila (posiblemente tan alta como el depurador), arregle el error waaaaayyyyy down en el apilar y reanudar la ejecución desde allí. O las condiciones de CommonLisp donde la elevación, la captura y el manejo son tres cosas diferentes en lugar de dos (elevación y captura-manejo) como con excepciones.

El uso de un tipo de retorno de error real en lugar de una excepción le permite hacer cosas similares, y más, sin tener que modificar el idioma.

Y luego está el elefante obvio en la habitación: las excepciones son un efecto secundario. Los efectos secundarios son malvados. Nota: No estoy diciendo que esto sea necesariamente cierto. Pero es un punto de vista que algunas personas tienen, y en ese caso, la ventaja de un tipo de error sobre las excepciones es obvia. Por lo tanto, si desea hacer una programación funcional, puede usar tipos de error por la única razón de que son el enfoque funcional. (Tenga en cuenta que Haskell tiene excepciones. Tenga en cuenta también que a nadie le gusta usarlas).

Jörg W Mittag
fuente
Me encanta la respuesta, aunque todavía no veo por qué debería renunciar Exception. Eitherparece una gran herramienta, pero sí, estoy preguntando específicamente sobre el manejo de fallas, y prefiero usar una herramienta mucho más específica para mi tarea que una poderosa. Aplazar el manejo de fallas es un argumento justo, pero uno simplemente puede usar Tryun método que arroje un Exceptionsi no desea manejarlo en este momento. ¿El desbobinado de seguimiento de pila no afecta Either/ Trytambién? Puede repetir los mismos errores al propagarlos incorrectamente; saber cuándo manejar una falla no es trivial en ambos casos.
Eyal Roth
Y realmente no puedo discutir con un razonamiento "puramente funcional", ¿verdad?
Eyal Roth
0

Lo bueno de las excepciones es que el código de origen ya ha clasificado las circunstancias excepcionales para usted. La diferencia entre an IllegalStateExceptiony a BadParameterExceptiones mucho más fácil de diferenciar en lenguajes mecanografiados más fuertes. Si intenta analizar "bueno" y "malo", no está aprovechando la herramienta extremadamente poderosa a su disposición, es decir, el compilador Scala. Sus propias extensiones Throwablepueden incluir tanta información como necesite, además de la cadena de mensaje de texto.

Esta es la verdadera utilidad del Tryentorno de Scala, Throwableya que actúa como la envoltura de los elementos que quedan para facilitar la vida.

BobDalgleish
fuente
2
No entiendo tu punto.
Eyal Roth
Eithertiene problemas de estilo porque no es una verdadera mónada (?); La arbitrariedad del leftvalor se aleja del mayor valor agregado cuando se incluye información sobre condiciones excepcionales en una estructura adecuada. Por lo tanto, comparar el uso de Eithercadenas de retorno en un caso en el lado izquierdo, try/catchen comparación con el otro caso, Throwables no es correcto usar el tipo correcto. Debido al valor de la Throwablesde proporcionar información, y la manera concisa que Trylas manijas Throwables, Tryes una apuesta mejor que cualquiera ... Eitherotry/catch
BobDalgleish
En realidad no estoy preocupado por el try-catch. Como se dijo en la pregunta, este último me parece mucho más limpio, y el código que maneja la falla (ya sea coincidencia de patrones en Either o try-catch) es bastante claro en ambos casos.
Eyal Roth