¿Cuál es el beneficio de no tener "excepciones de tiempo de ejecución", como afirma Elm?

16

Algunos idiomas afirman no tener "excepciones de tiempo de ejecución" como una clara ventaja sobre otros idiomas que los tienen.

Estoy confundido sobre el asunto.

La excepción de tiempo de ejecución es solo una herramienta, que yo sepa, y cuando se usa bien:

  • puede comunicar estados "sucios" (arrojar datos inesperados)
  • agregando pila puede apuntar a la cadena de error
  • puede distinguir entre el desorden (por ejemplo, devolver un valor vacío en una entrada no válida) y el uso inseguro que necesita atención de un desarrollador (por ejemplo, lanzar una excepción en una entrada no válida)
  • puede agregar detalles a su error con el mensaje de excepción que proporciona más detalles útiles que ayudan a los esfuerzos de depuración (teóricamente)

Por otro lado, me resulta muy difícil depurar un software que "traga" excepciones. P.ej

try { 
  myFailingCode(); 
} catch {
  // no logs, no crashes, just a dirty state
}

Entonces, la pregunta es: ¿cuál es la fuerte ventaja teórica de no tener "excepciones de tiempo de ejecución"?


Ejemplo

https://guide.elm-lang.org/

No hay errores de tiempo de ejecución en la práctica. No nulo No indefinido no es una función.

atoth
fuente
Creo que sería útil proporcionar un ejemplo o dos de los idiomas a los que se refiere y / o enlaces a dichos reclamos. Esto podría interpretarse de varias maneras.
JimmyJames
El último ejemplo que encontré fue elm en comparación con C, C ++, C #, Java, ECMAScript, etc. He actualizado mi pregunta @JimmyJames
atoth el
2
Esta es la primera vez que oigo hablar de eso. Mi primera reacción es llamar a BS. Tenga en cuenta las palabras comadreja: en la práctica
JimmyJames
@atoth Voy a editar el título de su pregunta para que quede más claro, porque hay varias preguntas no relacionadas que se parecen a ella (como "RuntimeException" vs "Exception" en Java). Si no te gusta el nuevo título, puedes editarlo nuevamente.
Andres F.
OK, quería que fuera lo suficientemente general, pero puedo estar de acuerdo en que si eso ayuda, ¡gracias por su contribución @AndresF!
atoth

Respuestas:

28

Las excepciones tienen una semántica extremadamente limitante. Deben manejarse exactamente donde se lanzan, o en la pila de llamadas directas hacia arriba, y no hay ninguna indicación para el programador en el momento de la compilación si se olvida de hacerlo.

Compare esto con Elm, donde los errores se codifican como Resultados o Maybes , que son ambos valores . Eso significa que obtendrá un error del compilador si no maneja el error. Puede almacenarlos en una variable o incluso en una colección para diferir su manejo en un momento conveniente. Puede crear una función para manejar los errores de una manera específica de la aplicación en lugar de repetir bloques try-catch muy similares por todo el lugar. Puede encadenarlos en un cálculo que tenga éxito solo si todas sus partes tienen éxito, y no tienen que estar agrupados en un bloque de prueba. No está limitado por la sintaxis incorporada.

Esto no es nada como "tragar excepciones". Hace explícitas las condiciones de error en el sistema de tipos y proporciona semánticas alternativas mucho más flexibles para manejarlas.

Considere el siguiente ejemplo. Puede pegar esto en http://elm-lang.org/try si desea verlo en acción.

import Html exposing (Html, Attribute, beginnerProgram, text, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String

main =
  beginnerProgram { model = "", view = view, update = update }

-- UPDATE

type Msg = NewContent String

update (NewContent content) oldContent =
  content

getDefault = Result.withDefault "Please enter an integer" 

double = Result.map (\x -> x*2)

calculate = String.toInt >> double >> Result.map toString >> getDefault

-- VIEW

view content =
  div []
    [ input [ placeholder "Number to double", onInput NewContent, myStyle ] []
    , div [ myStyle ] [ text (calculate content) ]
    ]

myStyle =
  style
    [ ("width", "100%")
    , ("height", "40px")
    , ("padding", "10px 0")
    , ("font-size", "2em")
    , ("text-align", "center")
    ]

Tenga String.toInten cuenta que en la calculatefunción tiene la posibilidad de fallar. En Java, esto tiene el potencial de generar una excepción de tiempo de ejecución. A medida que lee la entrada del usuario, tiene muchas posibilidades de hacerlo. En cambio, Elm me obliga a lidiar con eso devolviendo un Result, pero tenga en cuenta que no tengo que lidiar con eso de inmediato. Puedo duplicar la entrada y convertirla en una cadena, luego verificar si hay una entrada incorrecta en la getDefaultfunción. Este lugar es mucho más adecuado para la verificación que el punto donde ocurrió el error o hacia arriba en la pila de llamadas.

La forma en que el compilador fuerza nuestra mano también es mucho más fina que las excepciones comprobadas de Java. Debe utilizar una función muy específica, como Result.withDefaultextraer el valor que desea. Si bien técnicamente podrías abusar de ese tipo de mecanismo, no tiene mucho sentido. Como puede diferir la decisión hasta que conozca un buen mensaje predeterminado / de error, no hay razón para no usarlo.

Karl Bielefeldt
fuente
8
That means you get a compiler error if you don't handle the error.- Bueno, ese fue el razonamiento detrás de Checked Exceptions en Java, pero todos sabemos lo bien que funcionó.
Robert Harvey
44
@RobertHarvey En cierto modo, las excepciones comprobadas de Java fueron la versión pobre de esto. Lamentablemente, podrían "tragarse" (como en el ejemplo del OP). Tampoco eran tipos reales, lo que los convierte en una ruta adicional extraña al flujo de código. Idiomas con mejores sistemas del tipo que permiten a los errores codifican como valores de primera clase, que es lo que haces en (digamos) con Haskell Maybe, Either, etc. miradas Elm como si fuera tomando una página de lenguajes como ML, OCaml o Haskell.
Andres F.
3
@JimmyJames No, no te obligan. Solo tiene que "manejar" el error cuando desea usar el valor. Si lo hago x = some_func(), no tengo que hacer nada a menos que quiera examinar el valor de x, en cuyo caso puedo verificar si tengo un error o un valor "válido"; Además, es un error de tipo estático intentar usar uno en lugar del otro, por lo que no puedo hacerlo. Si los tipos de Elm funcionan de manera similar a otros lenguajes funcionales, ¡en realidad puedo hacer cosas como componer valores de diferentes funciones antes de saber si son errores o no! Esto es típico de los lenguajes FP.
Andres F.
66
@atoth Pero usted no tiene beneficios significativos y no es una muy buena razón (tal como se ha explicado en varias respuestas a su pregunta). Realmente te animo a que aprendas un idioma con una sintaxis similar a ML y verás lo liberador que es deshacerse de la sintaxis tipo C cruft (ML, por cierto, se desarrolló a principios de los años 70, lo que lo hace más o menos Un contemporáneo de C). Las personas que diseñaron este tipo de sistemas de tipos consideran que este tipo de sintaxis es normal, a diferencia de C :) Mientras lo hace, no estaría de más aprender un Lisp también :)
Andres F.
66
@atoth Si quieres sacar una cosa de todo esto, toma esta: siempre asegúrate de no ser presa de la paradoja de Blub . No te irrites con la nueva sintaxis. Tal vez esté allí debido a las potentes funciones con las que no está familiarizado :)
Andres F.
10

Para entender esta afirmación, primero tenemos que entender qué nos compra un sistema de tipo estático. En esencia, lo que nos da un sistema de tipo estático es una garantía: si el tipo de programa verifica, no puede ocurrir una determinada clase de comportamientos de tiempo de ejecución.

Eso suena siniestro. Bueno, un verificador de tipo es similar a un verificador de teoremas. (En realidad, según el isomorfismo de Curry-Howard, son lo mismo.) Una cosa que es muy peculiar acerca de los teoremas es que cuando se prueba un teorema, se demuestra exactamente lo que dice el teorema, nada más. (Por ejemplo, cuando alguien dice "He demostrado que este programa es correcto", siempre debe preguntar "defina 'correcto'"). Lo mismo es cierto para los sistemas de tipos. Cuando decimos "es un programa de tipo seguro", lo que queremos decir es que no que no se produzca ninguna posibilidad de error. Solo podemos decir que los errores que el sistema de tipos nos promete evitar no pueden ocurrir.

Por lo tanto, los programas pueden tener infinitos comportamientos de tiempo de ejecución diferentes. De ellos, infinitamente muchos son útiles, pero también infinitamente muchos son "incorrectos" (para varias definiciones de "corrección"). Un sistema de tipo estático nos permite demostrar que un cierto conjunto finito y fijo de esos infinitos comportamientos incorrectos de tiempo de ejecución no puede ocurrir.

La diferencia entre los diferentes sistemas de tipos radica básicamente en cuál, cuántos y cuán complejos pueden ser los comportamientos de tiempo de ejecución que no se producen. Los sistemas de tipo débil como el de Java solo pueden probar cosas muy básicas. Por ejemplo, Java puede demostrar que un método que se escribe como devolver a Stringno puede devolver a List. Pero, por ejemplo, puede no demostrar que el método no no volverá. Tampoco puede probar que el método no arroje una excepción. Y no puede probar que no devolverá el error String  ; cualquiera Stringsatisfará el verificador de tipo. (Y, por supuesto, incluso nullsatisfaga él también.) Incluso hay cosas muy simples que Java no se puede demostrar, por lo que tenemos excepciones como ArrayStoreException, ClassCastExceptiono favorito de todos, el NullPointerException.

Los sistemas de tipos más potentes como el de Agda también pueden probar cosas como "devolverá la suma de los dos argumentos" o "devuelve la versión ordenada de la lista pasada como argumento".

Ahora, lo que los diseñadores de Elm quieren decir con la afirmación de que no tienen excepciones de tiempo de ejecución es que el sistema de tipos de Elm puede demostrar la ausencia de (una porción significativa de) comportamientos de tiempo de ejecución que en otros idiomas no se puede demostrar que no ocurran y, por lo tanto, podrían conducir a a un comportamiento erróneo en tiempo de ejecución (que en el mejor de los casos significa una excepción, en el peor de los casos significa un bloqueo, y en el peor de los casos significa sin bloqueo, sin excepción y simplemente un resultado silenciosamente incorrecto).

Por lo tanto, están no diciendo "no implementamos excepciones". Están diciendo "cosas que serían excepciones de tiempo de ejecución en lenguajes típicos con los que los programadores típicos que llegarían a Elm tendrían experiencia, son atrapados por el sistema de tipos". Por supuesto, alguien que viene de Idris, Agda, Guru, Epigram, Isabelle / HOL, Coq o idiomas similares verá a Elm como bastante débil en comparación. La declaración está más dirigida a los programadores típicos de Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl, ...

Jörg W Mittag
fuente
55
Nota para editores potenciales: Lamento mucho el uso de negativos dobles e incluso triples. Sin embargo, los dejé a propósito: los sistemas de tipos garantizan la ausencia de ciertos tipos de comportamientos de tiempo de ejecución, es decir, garantizan que ciertas cosas no ocurran. Y quería mantener intacta esa formulación "demostrar que no ocurre", lo que desafortunadamente conduce a construcciones como "no puede probar que un método no regrese". Si puede encontrar una manera de mejorarlos, continúe, pero tenga en cuenta lo anterior. ¡Gracias!
Jörg W Mittag
2
Buena respuesta en general, pero un pequeño detalle: "un verificador de tipo es similar a un probador de teoremas". En realidad, un verificador de tipo es más parecido a un verificador de teorema: ambos hacen la verificación , no la deducción .
cabeza de jardín
4

Elm no puede garantizar ninguna excepción de tiempo de ejecución por la misma razón C no puede garantizar ninguna excepción de tiempo de ejecución: el lenguaje no admite el concepto de excepciones.

Elm tiene una forma de señalar las condiciones de error en tiempo de ejecución, pero este sistema no es una excepción, es "Resultados". Una función que puede fallar devuelve un "Resultado" que contiene un valor regular o un error. Elms está fuertemente tipado, por lo que esto es explícito en el sistema de tipos. Si una función siempre devuelve un entero, tiene el tipo Int. Pero si devuelve un entero o falla, el tipo de retorno es Result Error Int. (La cadena es el mensaje de error). Esto lo obliga a manejar explícitamente ambos casos en el sitio de la llamada.

Aquí hay un ejemplo de la introducción (un poco simplificado):

view : String -> String 
view userInputAge =
  case String.toInt userInputAge of
    Err msg ->
        text "Not a valid number!"

    Ok age ->
        text "OK!"

La función toIntpuede fallar si la entrada no es analizable, por lo que su tipo de retorno sí lo es Result String int. Para obtener el valor entero real, debe "desempaquetar" mediante la coincidencia de patrones, lo que a su vez lo obliga a manejar ambos casos.

Los resultados y excepciones fundamentalmente hacen lo mismo, la diferencia importante son los "valores predeterminados". Las excepciones aparecerán y terminarán el programa de manera predeterminada, y debe atraparlas explícitamente si desea manejarlas. El resultado es al revés: se ve obligado a manejarlos de manera predeterminada, por lo que debe pasarlos de manera explícita al principio si desea que finalicen el programa. Es fácil ver cómo este comportamiento puede conducir a un código más robusto.

JacquesB
fuente
2
@atoth Aquí hay un ejemplo. Imagina que el lenguaje A permite excepciones. Entonces te dan la función doSomeStuff(x: Int): Int. Normalmente espera que devuelva un Int, pero ¿puede arrojar una excepción también? Sin mirar su código fuente, no puedes saberlo. Por el contrario, un lenguaje B que codifica errores a través de tipos puede tener la misma función declarada así: doSomeStuff(x: Int): ErrorOrResultOfType<Int>(en Elm, este tipo se denomina realmente Result). A diferencia del primer caso, ahora es inmediatamente obvio si la función puede fallar, y debe manejarla explícitamente.
Andres F.
1
Como @RobertHarvey implica en un comentario sobre otra respuesta, esto parece ser básicamente como excepciones marcadas en Java. Lo que aprendí al trabajar con Java en los primeros días, cuando se verificaron la mayoría de las excepciones, es que realmente no desea verse obligado a escribir código para siempre errores en el momento en que ocurren.
JimmyJames
2
@JimmyJames No es como las excepciones marcadas porque las excepciones no se componen, se pueden ignorar ("tragar") y no son valores de primera clase :) Realmente recomiendo aprender un lenguaje funcional estáticamente tipado para comprender realmente esto. Esto no es una novedad inventada por Elm: así es como se programa en lenguajes como ML o Haskell, y es diferente de Java.
Andres F.
2
@AndresF. this is how you program in languages such as ML or HaskellEn Haskell, sí; ML, no. Robert Harper, uno de los principales contribuyentes de Standard ML e investigador del lenguaje de programación, considera que las excepciones son útiles . Los tipos de error pueden interferir con la composición de la función en los casos en que puede garantizar que no se produzca un error. Las excepciones también tienen un rendimiento diferente. No paga por las excepciones que no se lanzan, pero paga por verificar los valores de error cada vez, y las excepciones son una forma natural de expresar retroceso en algunos algoritmos
Doval
2
@JimmyJames Espero que ahora vea que las excepciones marcadas y los tipos de error reales son solo superficialmente similares. Las excepciones marcadas no se combinan con gracia, son engorrosas de usar y no están orientadas a la expresión (y, por lo tanto, simplemente puede "tragarlas", como sucede con Java). Las excepciones no comprobadas son menos engorrosas, por lo que son la norma en todas partes, excepto en Java, pero es más probable que te hagan tropezar, y no puedes saber mirando una declaración de función si se lanza o no, lo que hace que tu programa sea más difícil comprender.
Andres F.
2

Primero, tenga en cuenta que su ejemplo de excepciones de "deglución" es, en general, una práctica terrible y no tiene relación alguna con no tener excepciones de tiempo de ejecución; cuando lo piensa, tuvo un error de tiempo de ejecución, pero eligió ocultarlo y no hacer nada al respecto. Esto a menudo dará lugar a errores que son difíciles de entender.

Esta pregunta podría interpretarse de muchas maneras, pero como mencionó a Elm en los comentarios, el contexto es más claro.

Elm es, entre otras cosas, un lenguaje de programación estáticamente tipado . Uno de los beneficios de este tipo de sistemas de tipos es que el compilador detecta muchas clases de errores (aunque no todos), antes de que el programa se use realmente. Algunos tipos de errores pueden codificarse en tipos (como Elm's Resulty Task), en lugar de arrojarse como excepciones. Esto es lo que quieren decir los diseñadores de Elm: se detectarán muchos errores en el momento de la compilación en lugar del "tiempo de ejecución", y el compilador lo obligará a tratarlos en lugar de ignorarlos y esperar lo mejor. Está claro por qué esto es una ventaja: mejor que el programador se dé cuenta de un problema antes que el usuario.

Tenga en cuenta que cuando no utiliza excepciones, los errores se codifican de otras maneras menos sorprendentes. De la documentación de Elm :

Una de las garantías de Elm es que no verá errores de tiempo de ejecución en la práctica. NoRedInk ha estado usando Elm en producción durante aproximadamente un año, ¡y todavía no han tenido uno! Como todas las garantías en Elm, esto se reduce a elecciones de diseño de lenguaje fundamentales. En este caso, nos ayuda el hecho de que Elm trata los errores como datos. (¿Has notado que hacemos muchas cosas aquí?)

Los diseñadores de Elm son un poco atrevidos al afirmar que "no hay excepciones de tiempo de ejecución" , aunque lo califican con "en la práctica". Lo que probablemente significan es "menos errores inesperados que si estuviera codificando en JavaScript".

Andres F.
fuente
¿Lo estoy interpretando mal o simplemente están jugando un juego semántico? Prohíben el nombre de "excepción de tiempo de ejecución", pero luego lo reemplazan con un mecanismo diferente que transmite información de error a la pila. Esto suena como simplemente cambiar la implementación de una excepción a un concepto u objeto similar que implementa un mensaje de error de manera diferente. Eso difícilmente es devastador. Es como cualquier lenguaje escrito estáticamente. Compare el cambio de un COM HRESULT a una excepción .NET. Mecanismo diferente, pero sigue siendo una excepción en tiempo de ejecución, no importa cómo lo llames.
Mike apoya a Mónica el
@ Mike Para ser sincero, no he mirado a Elm en detalle. A juzgar por los documentos, tienen tipos Resulty Taskque se parecen mucho a los más familiares Eithery Futurede otros idiomas. A diferencia de las excepciones, los valores de estos tipos se pueden combinar y en algún momento debe manejarlos explícitamente: ¿representan un valor válido o un error? No leo las mentes, pero esta falta de sorpresa para el programador es probablemente lo que los diseñadores de Elm querían decir con "sin excepciones de tiempo de ejecución" :)
Andres F.
@ Mike, estoy de acuerdo, no es devastador. La diferencia con las excepciones de tiempo de ejecución es que no son explícitas en los tipos (es decir, no puede saber, sin mirar su código fuente, si puede arrojar un fragmento de código); La codificación de errores en los tipos es muy explícita y evita que el programador los pase por alto, lo que genera un código más seguro. Esto lo hacen muchos lenguajes de FP y, de hecho, no es nada nuevo.
Andres F.
1
Según sus comentarios, creo que hay más que "comprobaciones de tipo estático" . Puede agregarlo a JS usando el Script mecanografiado, que es mucho menos restrictivo que un nuevo ecosistema de "hacer o deshacer".
atoth
1
@AndresF .: Técnicamente hablando, la característica más nueva del sistema de tipos de Java es el polimorfismo paramétrico, que proviene de finales de los años 60. Entonces, en cierto modo, decir "moderno" cuando quiere decir "no Java" es algo correcto.
Jörg W Mittag
0

Elm afirma:

No hay errores de tiempo de ejecución en la práctica. No nulo No indefinido no es una función.

Pero preguntas sobre excepciones de tiempo de ejecución . Hay una diferencia

En Elm, nada devuelve un resultado inesperado. NO puede escribir un programa válido en Elm que produzca errores de tiempo de ejecución. Por lo tanto, no necesita excepciones.

Entonces, la pregunta debería ser:

¿Cuál es el beneficio de tener "sin errores de tiempo de ejecución"?

Si puede escribir código que nunca tenga errores de tiempo de ejecución, sus programas nunca fallarán.

Héctor
fuente