La forma más limpia de informar errores en Haskell

22

Estoy trabajando en aprender Haskell, y he encontrado tres formas diferentes de tratar los errores en las funciones que escribo:

  1. Simplemente puedo escribir error "Some error message.", lo que arroja una excepción.
  2. Puedo recuperar mi función Maybe SomeType, donde puedo o no devolver lo que me gustaría devolver.
  3. Puedo devolver mi función Either String SomeType, donde puedo devolver un mensaje de error o lo que me pidieron que devolviera en primer lugar.

Mi pregunta es: ¿Qué método de manejo de errores debo usar y por qué? Tal vez debería diferentes métodos, dependiendo del contexto?

Mi comprensión actual es:

  • Es "difícil" lidiar con excepciones en código puramente funcional, y en Haskell uno quiere mantener las cosas tan puramente funcionales como sea posible.
  • Volver Maybe SomeTypees lo correcto si la función fallará o tendrá éxito (es decir, no hay diferentes maneras en que puede fallar ).
  • Volver Either String SomeTypees lo correcto si una función puede fallar en cualquiera de varias formas.
CmdrMoozy
fuente

Respuestas:

32

Muy bien, primera regla de manejo de errores en Haskell: nunca usarerror .

Es terrible en todos los sentidos. Existe únicamente como un acto de la historia y el hecho de que Prelude lo use es terrible. No lo uses

El único momento concebible que podría usar es cuando algo es tan terriblemente interno que algo debe estar mal con la estructura misma de la realidad, lo que hace que el resultado de su programa sea discutible.

Ahora la pregunta es Maybevs Either. Maybese adapta muy bien a algo como head, que puede o no devolver un valor, pero solo hay una posible razón para fallar. Nothingestá diciendo algo como "se rompió, y ya sabes por qué". Algunos dirían que indica una función parcial.

La forma más robusta de manejo de errores es Either+ un error ADT.

Por ejemplo, en uno de mis compiladores de pasatiempos, tengo algo como

data CompilerError = ParserError ParserError
                   | TCError TCError
                   ...
                   | ImpossibleError String

data ParserError = ParserError (Int, Int) String
data TCError = CouldntUnify Ty Ty
             | MissingDefinition Name
             | InfiniteType Ty
             ...

type ErrorM m = ExceptT CompilerError m -- from MTL

Ahora defino un montón de tipos de error, anidándolos para tener un glorioso error de nivel superior. Esto puede ser un error de cualquier etapa de la compilación o ImpossibleError, lo que significa un error del compilador.

Cada uno de estos tipos de error intenta mantener la mayor cantidad de información posible durante una impresión bonita u otro análisis. Más importante aún, al no tener una cadena, ¡puedo probar que ejecutar un programa mal escrito a través del verificador de tipo realmente genera un error de unificación! Una vez que algo es un String, desaparece para siempre y cualquier información que contenga es opaca para el compilador / pruebas, por lo Either Stringque tampoco es genial.

Finalmente empaque este tipo en ExceptTun nuevo transformador de mónada de MTL. Esto es esencialmente EitherTy viene con un buen lote de funciones para lanzar y atrapar errores de una manera pura y agradable.

Finalmente, vale la pena mencionar que Haskell tiene los mecanismos para admitir el manejo de excepciones como lo hacen otros idiomas, excepto que la captura de una excepción vive IO. Sé que a algunas personas les gusta usarlas para IOaplicaciones pesadas en las que todo podría fallar, pero con tan poca frecuencia que no les gusta pensar en ello. Si usa estas excepciones impuras o simplemente ExceptT Error IOes realmente una cuestión de gustos. Personalmente, opto ExceptTporque me gusta que me recuerden la posibilidad de fracaso.


Como un resumen,

  • Maybe - Puedo fallar de una manera obvia
  • Either CustomType - Puedo fallar y te diré lo que pasó
  • IO+ excepciones: a veces fallo. Revisa mis documentos para ver qué arrojo cuando lo hago
  • error - Yo también te odio usuario
Daniel Gratzer
fuente
Esta respuesta me aclara mucho. Me estaba cuestionando sobre la base del hecho de que las funciones integradas les gusta heado lastparecen usar error, por lo que me preguntaba si esa era realmente una buena manera de hacer las cosas y simplemente me faltaba algo. Esto responde a esa pregunta. :)
CmdrMoozy
55
@CmdrMoozy Me alegra ayudar :) Es realmente lamentable que el preludio tenga tan malas prácticas. Tal es el camino del legado: /
Daniel Gratzer
3
+1 Su resumen es increíble
recursion.ninja
2

No separaría 2 y 3 en función de cuántas formas puede fallar algo. Tan pronto como creas que solo hay una forma posible, otra mostrará su rostro. En cambio, la métrica debería ser "¿les importa a mis llamantes por qué algo falló? ¿Pueden realmente hacer algo al respecto?".

Más allá de eso, no me queda claro de inmediato que Either String SomeTypepueda producir una condición de error. Haría un tipo de datos algebraico simple con las condiciones con un nombre más descriptivo.

Lo que use depende de la naturaleza del problema que enfrenta y de los modismos del paquete de software con el que está trabajando. Aunque tendería a evitar el n. ° 1.

Telastyn
fuente
En realidad, el uso Eitherde errores es un patrón muy conocido en Haskell.
Rufflewind
1
@rufflewind: Eitherno es la parte poco clara, el uso de Stringun error en lugar de una cadena real.
Telastyn
Ah, leí mal tu intención.
Rufflewind