Tal vez mónada vs excepciones

Respuestas:

57

El uso Maybe(o su primo Eitherque funciona básicamente de la misma manera pero le permite devolver un valor arbitrario en lugar de Nothing) tiene un propósito ligeramente diferente al de las excepciones. En términos de Java, es como tener una excepción marcada en lugar de una excepción de tiempo de ejecución. Representa algo esperado que hay que tratar, en lugar de un error que no esperaba.

Entonces, una función como indexOfdevolvería un Maybevalor porque espera la posibilidad de que el elemento no esté en la lista. Esto es muy parecido a regresar nullde una función, excepto en una forma de tipo seguro que lo obliga a lidiar con el nullcaso. Eitherfunciona de la misma manera, excepto que puede devolver información asociada con el caso de error, por lo que en realidad es más similar a una excepción que Maybe.

¿Cuáles son las ventajas del enfoque Maybe/ Either? Por un lado, es un ciudadano de primera clase del idioma. Comparemos una función con Eitheruna que arroje una excepción. Para el caso de excepción, su único recurso real es una try...catchdeclaración. Para la Eitherfunción, puede usar los combinadores existentes para aclarar el control de flujo. Aquí hay un par de ejemplos:

Primero, supongamos que desea probar varias funciones que podrían generar errores en una fila hasta que obtenga una que no lo haga. Si no obtiene ninguno sin errores, desea devolver un mensaje de error especial. Este es en realidad un patrón muy útil, pero sería un dolor horrible usarlo try...catch. Afortunadamente, dado que Eitheres solo un valor normal, puede usar las funciones existentes para que el código sea mucho más claro:

firstThing <|> secondThing <|> throwError (SomeError "error message")

Otro ejemplo es tener una función opcional. Supongamos que tiene varias funciones para ejecutar, incluida una que intenta optimizar una consulta. Si esto falla, desea que todo lo demás se ejecute de todos modos. Podrías escribir código como:

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

Ambos casos son más claros y cortos que el uso try..catchy, lo que es más importante, más semántico. Usar una función como <|>o optionalhace que sus intenciones sean mucho más claras que usar try...catchpara manejar siempre las excepciones.

También tenga en cuenta que no tiene que llenar su código con líneas como if a == Nothing then Nothing else ...! El objetivo de tratar Maybey Eithercomo mónada es evitar esto. Puede codificar la semántica de propagación en la función de enlace para obtener las comprobaciones de nulo / error de forma gratuita. El único momento en el que tiene que verificar explícitamente es si desea devolver algo distinto de Nothingun dado Nothing, e incluso así es fácil: hay un montón de funciones de biblioteca estándar para hacer que ese código sea más agradable.

Finalmente, otra ventaja es que a Maybe/ Eithertype es simplemente más simple. No es necesario extender el lenguaje con palabras clave adicionales o estructuras de control; todo es solo una biblioteca. Dado que son solo valores normales, hace que el sistema de tipos sea más simple: en Java, debe diferenciar entre tipos (por ejemplo, el tipo de retorno) y efectos (por ejemplo, throwsdeclaraciones) donde no usaría Maybe. También se comportan como cualquier otro tipo definido por el usuario: no es necesario tener un código especial de manejo de errores integrado en el idioma.

Otra victoria es que Maybe/ Eitherson functors y mónadas, lo que significa que pueden aprovechar las funciones de flujo de control de mónada existentes (de las cuales hay un número justo) y, en general, jugar muy bien junto con otras mónadas.

Dicho esto, hay algunas advertencias. Por un lado, Maybeni Eitherreemplazar las excepciones no marcadas. Querrá otra forma de manejar cosas como dividir entre 0 simplemente porque sería una molestia que cada división devuelva un Maybevalor.

Otro problema es la devolución de múltiples tipos de errores (esto solo se aplica a Either). Con excepciones, puede lanzar cualquier tipo diferente de excepciones en la misma función. con Either, solo obtienes un tipo. Esto se puede superar con subtipado o un ADT que contiene todos los diferentes tipos de errores como constructores (este segundo enfoque es el que se usa generalmente en Haskell).

Aún así, sobre todo, prefiero el enfoque Maybe/ Eitherporque lo encuentro más simple y más flexible.

Tikhon Jelvis
fuente
Principalmente de acuerdo, pero no creo que el ejemplo "opcional" tenga sentido: parece que puede hacerlo igual de bien con excepciones: nulo Opcional (Acción) {try {act (); } catch (Exception) {}}
Erroresatz
11
  1. Una excepción puede llevar más información sobre la fuente de un problema. OpenFile()puede lanzar FileNotFoundo NoPermissiono TooManyDescriptorsetc. A Ninguno no lleva esta información.
  2. Las excepciones se pueden usar en contextos que carecen de valores de retorno (por ejemplo, con constructores en lenguajes que los tienen).
  3. Una excepción le permite enviar fácilmente la información a la pila, sin muchas if None return Nonedeclaraciones de estilo.
  4. El manejo de excepciones casi siempre conlleva un mayor impacto en el rendimiento que simplemente devolver un valor.
  5. Lo más importante de todo es que una excepción y una mónada tal vez tienen diferentes propósitos: una excepción se usa para indicar un problema, mientras que una tal vez no.

    "Enfermera, si hay un paciente en la habitación 5, ¿puedes pedirle que espere?"

    • Quizás mónada: "Doctor, no hay paciente en la habitación 5".
    • Excepción: "¡Doctor, no hay habitación 5!"

    (observe el "si", esto significa que el médico espera una Mónada Quizás)

Roble
fuente
77
No estoy de acuerdo con los puntos 2 y 3. En particular, 2 en realidad no plantea un problema en los idiomas que usan mónadas porque esos idiomas tienen convenciones que no generan el enigma de tener que manejar el fracaso durante la construcción. En cuanto a 3, lenguas con las mónadas tienen una sintaxis adecuada para el manejo de las mónadas de una manera transparente que no requiere ninguna comprobación explícita (por ejemplo, Nonelos valores pueden simplemente ser propagado). Su punto 5 es único tipo de derecho ... la cuestión es: ¿qué situaciones son inequívocamente excepcional? Pues resulta que ... no muchos .
Konrad Rudolph
3
Con respecto a (2), se puede escribir bindde una manera tal que las pruebas para Noneno incurrir en una sobrecarga sintáctica sin embargo. Un ejemplo muy simple, C # simplemente sobrecarga los Nullableoperadores adecuadamente. Sin comprobación de Nonenecesaria, incluso cuando se utiliza el tipo. Por supuesto, el cheque se sigue haciendo (que es un tipo seguro), pero detrás de las escenas y no el desorden de su código. Lo mismo se aplica en cierto sentido a su objeción a mi objeción a (5), pero estoy de acuerdo en que no siempre se pueden aplicar.
Konrad Rudolph
44
@Oak: En cuanto a (3), todo el punto de tratamiento Maybecomo una mónada es hacer que la propagación Noneimplícita. Esto significa que si desea volver Nonedeterminado None, que no tienen que escribir ningún código especial. La única vez que necesite partido es que si quieres hacer algo especial en None. Nunca se necesita if None then Nonetipo de declaraciones.
Tikhon Jelvis
55
@Oak: eso es cierto, pero ese no era realmente mi punto. Lo que digo es que obtienes una nullverificación exactamente así (por ejemplo if Nothing then Nothing) gratis porque Maybees una mónada. Está codificado en la definición de bind ( >>=) para Maybe.
Tikhon Jelvis
2
Además, con respecto a (1): podría escribir fácilmente una mónada que puede transportar información de error (por ejemplo Either) que se comporta de la misma manera Maybe. Cambiar entre los dos es realmente bastante simple porque en Mayberealidad es solo un caso especial de Either. (En Haskell, se podría pensar Maybecomo Either ().)
Tikhon Jelvis
6

"Quizás" no reemplaza las excepciones. Las excepciones están destinadas a ser utilizadas en casos excepcionales (por ejemplo: abrir una conexión db y el servidor db no está allí aunque debería estarlo). "Quizás" es para modelar una situación en la que puede o no tener un valor válido; digamos que está obteniendo un valor de un diccionario para una clave: puede estar allí o no; no hay nada "excepcional" en ninguno de estos resultados.

Nemanja Trifunovic
fuente
2
En realidad, tengo bastante código que involucra diccionarios donde una clave faltante significa que las cosas han salido terriblemente mal en algún momento, muy probablemente un error en mi código o en el código que convirtió la entrada en lo que se usa en ese punto.
3
@delnan: No estoy diciendo que un valor faltante nunca pueda ser un signo de un estado excepcional, simplemente no tiene por qué serlo. Tal vez realmente modela una situación en la que una variable puede o no tener un valor válido; piense en los tipos anulables en C #.
Nemanja Trifunovic
6

Respaldo la respuesta de Tikhon, pero creo que hay un punto práctico muy importante que todos faltan:

  1. Los mecanismos de manejo de excepciones, al menos en los idiomas principales, están íntimamente unidos a hilos individuales.
  2. El Eithermecanismo no está acoplado a hilos en absoluto.

Entonces, algo que estamos viendo hoy en día en la vida real es que muchas soluciones de programación asíncrona están adoptando una variante del Eitherestilo de manejo de errores. Considere las promesas de Javascript , como se detalla en cualquiera de estos enlaces:

El concepto de promesas le permite escribir código asincrónico como este (tomado del último enlace):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

Básicamente, una promesa es un objeto que:

  1. Representa el resultado de un cálculo asincrónico, que puede o no haberse terminado todavía;
  2. Le permite encadenar más operaciones para realizar su resultado, que se activará cuando ese resultado esté disponible y cuyos resultados a su vez estén disponibles como promesas;
  3. Le permite conectar un controlador de fallas que se invocará si falla el cálculo de la promesa. Si no hay un controlador, el error se propaga a los controladores posteriores de la cadena.

Básicamente, dado que el soporte de excepciones nativas del lenguaje no funciona cuando su cálculo está ocurriendo en varios subprocesos, una implementación prometedora debe proporcionar un mecanismo de manejo de errores, y estos resultan ser mónadas similares a los Maybe/ Eithertipos de Haskell .

sacundim
fuente
No tiene nada que hacer con hilos. JavaScript en el navegador siempre se ejecuta en un hilo, no en varios hilos. Pero aún no puede usar la excepción porque no sabe cuándo se llama a su función en el futuro. Asíncrono no significa automáticamente una participación de hilos. Esa es también la razón por la cual no puede trabajar con excepciones. Solo puede recuperar una excepción si llama a una función e inmediatamente se ejecuta. Pero todo el propósito de Asynchronous es que se ejecuta en el futuro, a menudo cuando algo más termina, y no de inmediato. Es por eso que no puedes usar excepciones allí.
David Raab
1

El sistema de tipo Haskell requerirá que el usuario reconozca la posibilidad de un Nothing, mientras que los lenguajes de programación a menudo no requieren que se detecte una excepción. Eso significa que sabremos, en tiempo de compilación, que el usuario ha verificado un error.

chrisaycock
fuente
1
Hay excepciones marcadas en Java. Y las personas a menudo los trataban mal incluso.
Vladimir
1
@ DairT'arg ButJava requiere comprobaciones de errores de E / S (como ejemplo), pero no de NPE. En Haskell es al revés: el equivalente nulo (Quizás) está marcado pero los errores de E / S simplemente lanzan excepciones (sin marcar). No estoy seguro de la gran diferencia que eso hace, pero no escucho a Haskellers quejarse de Quizás y creo que a mucha gente le gustaría que se revisen los NPE.
1
@delnan ¿La gente quiere que se verifique el NPE? Deben estar locos. Y lo digo en serio. Hacer que NPE esté marcado solo tiene sentido si tiene una forma de evitarlo en primer lugar (al tener referencias no anulables, que Java no tiene).
Konrad Rudolph,
55
@KonradRudolph Sí, la gente probablemente no quiere que se verifique en el sentido de que tendrán que agregar un throws NPEa cada firma y catch(...) {throw ...} a cada cuerpo de método. Pero sí creo que hay un mercado para lo verificado en el mismo sentido que con Maybe: la nulabilidad es opcional y se rastrea en el sistema de tipos.
1
@delnan Ah, entonces estoy de acuerdo.
Konrad Rudolph
0

Tal vez la mónada es básicamente la misma que el uso de la comprobación de "nulo significa error" en la mayoría de los idiomas principales (excepto que requiere que se verifique el nulo), y tiene en gran medida las mismas ventajas y desventajas.

Telastyn
fuente
8
Bueno, no tiene las mismas desventajas, ya que puede verificarse estáticamente cuando se usa correctamente. No existe el equivalente de una excepción de puntero nulo cuando se usan quizás mónadas (nuevamente, suponiendo que se usen correctamente).
Konrad Rudolph,
En efecto. ¿Cómo crees que debería aclararse?
Telastyn
@Konrad Eso se debe a que para usar el valor (posiblemente) en el Quizás, debe verificar Ninguno (aunque, por supuesto, existe la posibilidad de automatizar muchas de esas comprobaciones). Otros idiomas guardan ese tipo de cosas manuales (principalmente por razones filosóficas, creo).
Donal Fellows
2
@Donal Sí, pero tenga en cuenta que la mayoría de los idiomas que implementan mónadas proporcionan azúcar sintáctica adecuada para que esta comprobación a menudo se pueda ocultar por completo. Por ejemplo, puede agregar dos Maybenúmeros escribiendo a + b sin la necesidad de verificar None, y el resultado es una vez más un valor opcional.
Konrad Rudolph
2
La comparación con los tipos anulables es válida para el Maybe tipo , pero el uso Maybecomo mónada agrega azúcar de sintaxis que permite expresar la lógica nula de manera mucho más elegante.
tdammers
0

El manejo de excepciones puede ser un verdadero dolor para la factorización y las pruebas. Sé que Python proporciona una buena sintaxis "con" que le permite atrapar excepciones sin el bloque rígido "try ... catch". Pero en Java, por ejemplo, intente que los bloques catch sean grandes, repetitivos, ya sea detallados o extremadamente detallados, y difíciles de romper. Además de eso, Java agrega todo el ruido alrededor de las excepciones marcadas y no marcadas.

Si, en cambio, su mónada captura excepciones y las trata como una propiedad del espacio monádico (en lugar de alguna anomalía de procesamiento), entonces es libre de mezclar y combinar funciones que se unen a ese espacio independientemente de lo que arrojen o atrapen.

Si, mejor aún, su mónada previene condiciones en las que podrían ocurrir excepciones (como pasar un cheque nulo a Quizás), entonces aún mejor. si ... entonces es mucho más fácil factorizar y probar que intentar ... atrapar.

Por lo que he visto, Go está adoptando un enfoque similar al especificar que cada función devuelve (respuesta, error). Es lo mismo que "elevar" la función a un espacio de mónada donde el tipo de respuesta central está decorado con una indicación de error y, de manera efectiva, eludir y atrapar excepciones.

Robar
fuente