Hay varias formas de manejar errores en lenguajes de programaci贸n:
- excepciones est谩ndar para muchos lenguajes (Java, Scala y otras JVM, python y muchos otros)
- c贸digos de estado o banderas (Go, bash)
- Varias estructuras de datos algebraicos, cuyos valores pueden ser resultados exitosos y descripciones de errores (Scala, Haskell y otros lenguajes funcionales)
Las excepciones se usan ampliamente, por otro lado, a menudo se dice que son lentas. Pero los oponentes de un enfoque funcional a menudo apelan al rendimiento.
Recientemente, he estado trabajando con Scala, donde puedo usar igualmente las excepciones y varios tipos de datos para el manejo de errores, por lo que me pregunto qu茅 enfoque ser谩 m谩s conveniente y m谩s r谩pido.
Inmediatamente descartaremos el uso de c贸digos y marcas, ya que este enfoque no se acepta en los idiomas JVM y, en mi opini贸n, es demasiado propenso a errores (lo siento por el juego de palabras). Por lo tanto, compararemos las excepciones y los diferentes tipos de ADT. Adem谩s, el ADT puede considerarse como el uso de c贸digos de error en un estilo funcional.
ACTUALIZACI脫N : se a帽aden excepciones sin trazas de pila a la comparaci贸n
Concursantes
Un poco m谩s sobre los tipos de datos algebraicosPara aquellos que no est谩n demasiado familiarizados con ADT ( ADT ), un tipo algebraico consta de varios valores posibles, cada uno de los cuales puede ser un valor compuesto (estructura, registro).
Un ejemplo es el tipo Option[T] = Some(value: T) | None
Option[T] = Some(value: T) | None
, que se utiliza en lugar de nulos: un valor de este tipo puede ser Some(t)
si hay un valor o None
si no lo es.
Otro ejemplo ser铆a Try[T] = Success(value: T) | Failure(exception: Throwable)
Try[T] = Success(value: T) | Failure(exception: Throwable)
, que describe el resultado de un c谩lculo que podr铆a completarse con 茅xito o con un error.
Entonces nuestros concursantes:
- Buenas viejas excepciones
- Excepciones sin seguimiento de pila, ya que llenar un seguimiento de pila es una operaci贸n muy lenta
Try[T] = Success(value: T) | Failure(exception: Throwable)
Try[T] = Success(value: T) | Failure(exception: Throwable)
: las mismas excepciones, pero en un contenedor funcionalEither[String, T] = Left(error: String) | Right(value: T)
Either[String, T] = Left(error: String) | Right(value: T)
: un tipo que contiene el resultado o una descripci贸n del errorValidatedNec[String, T] = Valid(value: T) | Invalid(errors: List[String])
ValidatedNec[String, T] = Valid(value: T) | Invalid(errors: List[String])
: un tipo de la biblioteca Cats , que en caso de error puede contener varios mensajes sobre diferentes errores (no List
usa List
, pero no importa)
NOTA: en esencia, las excepciones se comparan con el seguimiento de la pila, sin y ATD, pero se seleccionan varios tipos, ya que Scala no tiene un enfoque 煤nico y es interesante comparar varios.
Adem谩s de las excepciones, las cadenas se usan para describir errores, pero con el mismo 茅xito en una situaci贸n real, se usar铆an diferentes clases ( Either[Failure, T]
).
El problema
Para probar el manejo de errores, tomamos el problema del an谩lisis y la validaci贸n de datos:
case class Person(name: String, age: Int, isMale: Boolean) type Result[T] = Either[String, T] trait PersonParser { def parse(data: Map[String, String]): Result[Person] }
es decir tener un Map[String, String]
datos sin procesar Map[String, String]
necesita obtener Person
o un error si los datos no son v谩lidos.
Tirar
Una soluci贸n para la frente usando excepciones (en adelante dar茅 solo la funci贸n de person
, puedes ver el c贸digo completo en github ):
Throwparser.scala
def person(data: Map[String, String]): Person = { val name = string(data.getOrElse("name", null)) val age = integer(data.getOrElse("age", null)) val isMale = boolean(data.getOrElse("isMale", null)) require(name.nonEmpty, "name should not be empty") require(age > 0, "age should be positive") Person(name, age, isMale) }
aqu铆 string
, integer
y boolean
validan la presencia y el formato de tipos simples y realizan la conversi贸n.
En general, es bastante simple y claro.
ThrowNST (Sin seguimiento de pila)
El c贸digo es el mismo que en el caso anterior, pero las excepciones se usan sin un seguimiento de pila siempre que sea posible: ThrowNSTParser.scala
Prueba
La soluci贸n detecta excepciones antes y permite combinar los resultados a trav茅s de for
(que no debe confundirse con bucles en otros idiomas):
TryParser.scala
def person(data: Map[String, String]): Try[Person] = for { name <- required(data.get("name")) age <- required(data.get("age")) flatMap integer isMale <- required(data.get("isMale")) flatMap boolean _ <- require(name.nonEmpty, "name should not be empty") _ <- require(age > 0, "age should be positive") } yield Person(name, age, isMale)
un poco m谩s inusual para un ojo fr谩gil, pero debido al uso de for
, es muy similar a la versi贸n con excepciones, adem谩s, la validaci贸n de la presencia de un campo y el an谩lisis del tipo deseado se producen por separado ( flatMap
se puede leer aqu铆 and then
)
Ya sea
Aqu铆 el tipo Either
est谩 oculto detr谩s del alias Result
ya que el tipo de error es fijo:
EitherParser.scala
def person(data: Map[String, String]): Result[Person] = for { name <- required(data.get("name")) age <- required(data.get("age")) flatMap integer isMale <- required(data.get("isMale")) flatMap boolean _ <- require(name.nonEmpty, "name should not be empty") _ <- require(age > 0, "age should be positive") } yield Person(name, age, isMale)
Dado que el est谩ndar Either
como Try
forma una m贸nada en Scala, el c贸digo sali贸 exactamente igual, la diferencia aqu铆 es que la cadena aparece aqu铆 como un error y las excepciones se usan m铆nimamente (solo para manejar errores al analizar un n煤mero)
Validado
Aqu铆, la biblioteca Cats se usa para no obtener lo primero que sucedi贸, sino todo lo posible (por ejemplo, si varios campos no son v谩lidos, el resultado contendr谩 errores de an谩lisis para todos estos campos)
ValidatedParser.scala
def person(data: Map[String, String]): Validated[Person] = { val name: Validated[String] = required(data.get("name")) .ensure(one("name should not be empty"))(_.nonEmpty) val age: Validated[Int] = required(data.get("age")) .andThen(integer) .ensure(one("age should be positive"))(_ > 0) val isMale: Validated[Boolean] = required(data.get("isMale")) .andThen(boolean) (name, age, isMale).mapN(Person) }
este c贸digo ya es menos similar a la versi贸n original con excepciones, pero la verificaci贸n de restricciones adicionales no est谩 divorciada de analizar los campos y todav铆a recibimos varios errores en lugar de uno, 隆vale la pena!
Prueba
Para las pruebas, se gener贸 un conjunto de datos con un porcentaje diferente de errores y se analiz贸 en cada una de las formas.
Resultado en todos los porcentajes de errores:

Con m谩s detalle, con un bajo porcentaje de errores (el tiempo es diferente aqu铆 desde que se utiliz贸 una muestra m谩s grande):

Si alguna parte de los errores sigue siendo una excepci贸n con el seguimiento de la pila (en nuestro caso, el error de analizar el n煤mero ser谩 una excepci贸n que no controlamos), entonces, por supuesto, el rendimiento de los m茅todos de manejo de errores "r谩pidos" se deteriorar谩 significativamente. Validated
ve especialmente afectado, ya que recopila todos los errores y, como resultado, recibe una excepci贸n lenta m谩s que otros:

Conclusiones
Como mostr贸 el experimento, las excepciones con los rastros de la pila son realmente muy lentas (隆el 100% de los errores son la diferencia entre Throw
y Either
m谩s de 50 veces!), Y cuando pr谩cticamente no hay excepciones, usar ADT tiene un precio. Sin embargo, el uso de excepciones sin trazas de pila es tan r谩pido (y con un bajo porcentaje de errores m谩s r谩pido) como ADT, sin embargo, si tales excepciones van m谩s all谩 de la misma validaci贸n, no ser谩 f谩cil rastrear su fuente.
En total, si la probabilidad de una excepci贸n es superior al 1%, las excepciones sin trazas de pila funcionan m谩s r谩pidamente, Validated
o regulares. Either
casi tan r谩pida. Con una gran cantidad de errores, Either
puede ser un poco m谩s r谩pido que Validated
solo debido a la sem谩ntica a prueba de fallas.
El uso de ADT para el manejo de errores proporciona otra ventaja sobre las excepciones: la posibilidad de un error est谩 conectada al tipo en s铆 y es m谩s dif铆cil pasarla por alto, como cuando se usa Option
lugar de valores nulos.