¿Por qué diseñar un lenguaje moderno sin un mecanismo de manejo de excepciones?

47

Muchos lenguajes modernos ofrecen características de manejo de excepciones , pero el lenguaje de programación Swift de Apple no proporciona un mecanismo de manejo de excepciones .

Cargada de excepciones como estoy, tengo problemas para entender lo que esto significa. Swift tiene afirmaciones y, por supuesto, devuelve valores; pero tengo problemas para imaginar cómo mi forma de pensar basada en excepciones se asigna a un mundo sin excepciones (y, por lo demás, por qué ese mundo es deseable ). ¿Hay cosas que no puedo hacer en un lenguaje como Swift que podría hacer con excepciones? ¿Gano algo al perder excepciones?

¿Cómo, por ejemplo, podría expresar mejor algo como

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

en un idioma (Swift, por ejemplo) que carece de manejo de excepciones?

orome
fuente
11
Es posible que desee agregar Ir a la lista , si ignoramos paniccuál no es exactamente lo mismo. Además de lo que se dice allí, una excepción no es mucho más que una forma sofisticada (pero cómoda) de realizar una GOTO, aunque nadie lo llama así, por razones obvias.
JensG
3
La clase de respuesta a su pregunta es que necesita soporte de idioma para excepciones para poder escribirlas. El soporte de idiomas generalmente incluye administración de memoria; Dado que una excepción puede ser lanzada en cualquier lugar y atrapada en cualquier lugar, debe haber una forma de deshacerse de los objetos que no dependen del flujo de control.
Robert Harvey
1
No te estoy siguiendo, @Robert. C ++ logra admitir excepciones sin recolección de basura.
Karl Bielefeldt
3
@KarlBielefeldt: A un gran costo, por lo que entiendo. Por otra parte, ¿hay algo que se realice en C ++ sin grandes gastos, al menos en esfuerzo y conocimiento de dominio requerido?
Robert Harvey
2
@RobertHarvey: Buen punto. Soy una de esas personas que no piensa lo suficiente sobre estas cosas. Me han arrullado al pensar que ARC es GC, pero, por supuesto, no lo es. Entonces, básicamente (si estoy comprendiendo, más o menos), ¿las excepciones serían algo desordenado (a pesar de C ++) en un lenguaje donde la eliminación de objetos dependía del flujo de control?
orome

Respuestas:

33

En la programación incrustada, tradicionalmente no se permitían excepciones, porque la sobrecarga del desbobinado de la pila que tenía que hacer se consideraba una variabilidad inaceptable al intentar mantener el rendimiento en tiempo real. Si bien los teléfonos inteligentes podrían considerarse técnicamente plataformas en tiempo real, ahora son lo suficientemente potentes donde las antiguas limitaciones de los sistemas integrados ya no se aplican. Solo lo menciono en aras de la minuciosidad.

Las excepciones a menudo se admiten en lenguajes de programación funcionales, pero se usan tan raramente que es posible que no lo sean. Una razón es la evaluación diferida, que se realiza ocasionalmente incluso en idiomas que no son diferidos de manera predeterminada. Tener una función que se ejecuta con una pila diferente de la que estaba en la cola para ejecutar hace que sea difícil determinar dónde colocar su controlador de excepciones.

La otra razón es que las funciones de primera clase permiten construcciones como opciones y futuros que le brindan los beneficios sintácticos de las excepciones con más flexibilidad. En otras palabras, el resto del lenguaje es lo suficientemente expresivo como para que las excepciones no le compren nada.

No estoy familiarizado con Swift, pero lo poco que he leído sobre su manejo de errores sugiere que pretendían que el manejo de errores siguiera patrones de estilo más funcional. He visto ejemplos de código con successy failurebloques que se parecen mucho a futuros.

Aquí hay un ejemplo usando una Futurede este tutorial Scala :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Puede ver que tiene aproximadamente la misma estructura que su ejemplo usando excepciones. El futurebloque es como a try. El onFailurebloque es como un controlador de excepciones. En Scala, como en la mayoría de los lenguajes funcionales, Futurese implementa completamente usando el lenguaje mismo. No requiere ninguna sintaxis especial como las excepciones. Eso significa que puedes definir tus propias construcciones similares. Tal vez agregue un timeoutbloque, por ejemplo, o la funcionalidad de registro.

Además, puede pasar el futuro, devolverlo desde la función, almacenarlo en una estructura de datos o lo que sea. Es un valor de primera clase. No está limitado como las excepciones que deben propagarse directamente hacia la pila.

Las opciones resuelven el problema de manejo de errores de una manera ligeramente diferente, que funciona mejor para algunos casos de uso. No estás atrapado con el único método.

Esas son las cosas que "ganas al perder excepciones".

Karl Bielefeldt
fuente
1
Entonces, Futurees esencialmente una forma de examinar el valor devuelto por una función, sin detenerse a esperar. Al igual que Swift, se basa en el valor de retorno, pero a diferencia de Swift, la respuesta al valor de retorno puede ocurrir en un momento posterior (un poco como excepciones). ¿Derecho?
orome
1
Entiendes un Futurecorrecto, pero creo que podrías estar describiendo mal a Swift. Vea la primera parte de esta respuesta stackoverflow , por ejemplo.
Karl Bielefeldt
Hmm, soy nuevo en Swift, por lo que esa respuesta es un poco difícil de analizar. Pero si no me equivoco: eso esencialmente pasa un controlador que se puede invocar en un momento posterior; ¿derecho?
orome
Si. Básicamente, está creando una devolución de llamada cuando se produce un error.
Karl Bielefeldt
Eithersería un mejor ejemplo en mi humilde opinión
Paweł Prażak
19

Las excepciones pueden hacer que el código sea más difícil de razonar. Si bien no son tan poderosos como los gotos, pueden causar muchos de los mismos problemas debido a su naturaleza no local. Por ejemplo, supongamos que tiene un código imperativo como este:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

No puede decir de un vistazo si alguno de estos procedimientos puede generar una excepción. Tiene que leer la documentación de cada uno de estos procedimientos para resolverlo. (Algunos idiomas facilitan esto un poco al aumentar la firma de tipo con esta información.) El código anterior se compilará bien independientemente de si alguno de los procedimientos arroja, por lo que es muy fácil olvidar manejar una excepción.

Además, incluso si la intención es propagar la excepción a la persona que llama, a menudo es necesario agregar un código adicional para evitar que las cosas se queden en un estado inconsistente (por ejemplo, si su cafetera se rompe, aún necesita limpiar el desorden y regresar ¡La taza!). Por lo tanto, en muchos casos, el código que usa excepciones se vería tan complejo como el código que no lo hizo debido a la limpieza adicional requerida.

Las excepciones se pueden emular con un sistema de tipos suficientemente potente. Muchos de los lenguajes que evitan excepciones usan valores de retorno para obtener el mismo comportamiento. Es similar a cómo se hace en C, pero los sistemas de tipo moderno generalmente lo hacen más elegante y también más difícil de olvidar para manejar la condición de error. También pueden proporcionar azúcar sintáctica para hacer las cosas menos engorrosas, a veces casi tan limpias como lo serían con excepciones.

En particular, al incorporar el manejo de errores en el sistema de tipos en lugar de implementarlo como una característica separada, se pueden usar "excepciones" para otras cosas que ni siquiera están relacionadas con errores. (Es bien sabido que el manejo de excepciones es en realidad una propiedad de las mónadas).

Viento rizado
fuente
¿Es correcto ese tipo de sistema de tipos que tiene Swift, incluidos los opcionales, es el tipo de "sistema de tipos poderoso" que logra esto?
orome
2
Sí, los tipos opcionales y, en general, los de suma (denominados "enum" en Swift / Rust) pueden lograr esto. Sin embargo, se necesita un poco de trabajo adicional para que sean agradables de usar: en Swift, esto se logra con la sintaxis de encadenamiento opcional; en Haskell, esto se logra con la notación monádica.
Rufflewind
¿Puede un "sistema de tipos suficientemente poderosa" dar un seguimiento de pila, si no que es bastante inútil
Paweł Pražák
1
+1 por señalar que las excepciones oscurecen el flujo de control. Como una nota auxiliar: es lógico que las excepciones no sean realmente más malvadas que goto: gotoestá restringido a una sola función, que es un rango bastante pequeño, siempre que la función sea realmente pequeña, la excepción actúa más como un cruce de gotoy come from(ver en.wikipedia.org/wiki/INTERCAL ;-)). Puede conectar casi cualquier dos piezas de código, posiblemente saltando sobre el código algunas terceras funciones. Lo único que no puede hacer, que gotopuede, es regresar.
cmaster
2
@ PawełPrażak Cuando se trabaja con muchas funciones de orden superior, los seguimientos de pila no son tan valiosos. Las fuertes garantías sobre las entradas y salidas y la evitación de los efectos secundarios son las que evitan que esta indirección provoque errores confusos.
Jack
11

Aquí hay algunas respuestas excelentes, pero creo que una razón importante no se ha enfatizado lo suficiente: cuando se producen excepciones, los objetos se pueden dejar en estados no válidos. Si puede "atrapar" una excepción, entonces su código de controlador de excepciones podrá acceder y trabajar con esos objetos no válidos. Eso va a ir terriblemente mal a menos que el código para esos objetos se haya escrito perfectamente, lo cual es muy, muy difícil de hacer.

Por ejemplo, imagine implementar Vector. Si alguien crea una instancia de su Vector con un conjunto de objetos, pero ocurre una excepción durante la inicialización (quizás, digamos, mientras copia sus objetos en la memoria recién asignada), es muy difícil codificar correctamente la implementación de Vector de tal manera que no Se pierde la memoria. Este breve artículo de Stroustroup cubre el ejemplo de Vector .

Y eso es simplemente la punta del iceberg. ¿Qué pasaría si, por ejemplo, hubiera copiado algunos de los elementos, pero no todos? Para implementar un contenedor como Vector correctamente, casi tiene que hacer que cada acción que realice sea reversible, por lo que toda la operación es atómica (como una transacción de base de datos). Esto es complicado y la mayoría de las aplicaciones se equivocan. E incluso cuando se hace correctamente, complica enormemente el proceso de implementación del contenedor.

Entonces, algunos idiomas modernos han decidido que no vale la pena. El óxido, por ejemplo, tiene excepciones, pero no se pueden "atrapar", por lo que no hay forma de que el código interactúe con objetos en un estado no válido.

Flores de charlie
fuente
1
El propósito de la captura es hacer que un objeto sea coherente (o terminar algo si no es posible) después de que ocurra un error.
JoulinRouge
@JoulinRouge lo sé. Pero algunos idiomas han decidido no darle esa oportunidad, sino que bloquean todo el proceso. Esos diseñadores de idiomas conocen el tipo de limpieza que le gustaría hacer, pero han concluido que es demasiado complicado darle eso, y las compensaciones involucradas en hacerlo no valdrían la pena. Me doy cuenta de que es posible que no esté de acuerdo con su elección ... pero es valioso saber que tomaron la decisión conscientemente por estas razones particulares.
Charlie Flowers
6

Una cosa que me sorprendió inicialmente del lenguaje Rust es que no admite excepciones de captura. Puede lanzar excepciones, pero solo el tiempo de ejecución puede detectarlas cuando una tarea (piense en un hilo, pero no siempre un hilo separado del sistema operativo) muere; Si comienza una tarea usted mismo, puede preguntar si salió normalmente o si se fail!()editó.

Como tal, no es idiomático failmuy a menudo. Los pocos casos en los que sucede son, por ejemplo, en el arnés de prueba (que no sabe cómo es el código de usuario), como el nivel superior de un compilador (la mayoría de los compiladores se bifurcan) o cuando se invoca una devolución de llamada en la entrada del usuario.

En cambio, el idioma común es usar la Resultplantilla para pasar explícitamente los errores que se deben manejar. Esto es mucho más fácil gracias a la try!macro , que se puede envolver alrededor de cualquier expresión que produzca un Resultado y produzca el brazo exitoso si hay uno, o de lo contrario regresa temprano de la función.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}
o11c
fuente
1
Entonces, ¿es justo decir que esto también (como el enfoque de Go) es similar a Swift, que tiene assert, pero no catch?
orome
1
En Swift, ¡inténtalo! significa: Sí, sé que esto podría fallar, pero estoy seguro de que no lo hará, así que no lo estoy manejando, y si falla, entonces mi código es incorrecto, por lo tanto, bloquee en ese caso.
gnasher729
6

En mi opinión, las excepciones son una herramienta esencial para detectar errores de código en tiempo de ejecución. Tanto en pruebas como en producción. Haga que sus mensajes sean lo suficientemente detallados para que, en combinación con un seguimiento de la pila, pueda descubrir qué sucedió en un registro.

Las excepciones son principalmente una herramienta de desarrollo y una forma de obtener informes de error razonables de la producción en casos inesperados.

Aparte de la separación de las preocupaciones (el camino feliz con solo los errores esperados frente a la falla hasta llegar a un controlador genérico para errores inesperados) es algo bueno, hacer que su código sea más legible y mantenible, de hecho es imposible preparar su código para todo lo posible casos inesperados, incluso al hincharlo con código de manejo de errores para completar la ilegibilidad.

Ese es en realidad el significado de "inesperado".

Por cierto, lo que se espera y lo que no es una decisión que solo se puede tomar en el sitio de la llamada. Es por eso que las excepciones comprobadas en Java no funcionaron: la decisión se toma al momento de desarrollar una API, cuando no está del todo claro qué se espera o qué inesperado.

Ejemplo simple: la API de un mapa hash puede tener dos métodos:

Value get(Key)

y

Option<Value> getOption(key)

el primero arroja una excepción si no se encuentra, el último le da un valor opcional. En algunos casos, este último tiene más sentido, pero en otros, su código debe esperar que haya un valor para una clave determinada, por lo que si no hay uno, es un error que este código no puede solucionar porque La suposición ha fallado. En este caso, en realidad es el comportamiento deseado salir de la ruta del código y pasar a un controlador genérico en caso de que falle la llamada.

El código nunca debe tratar con supuestos básicos fallidos.

Excepto al marcarlos y lanzar excepciones bien legibles, por supuesto.

Lanzar excepciones no es malo, pero atraparlas puede serlo. No intentes arreglar errores inesperados. Capture excepciones en algunos lugares donde desea continuar algún ciclo u operación, regístrelos, tal vez informe un error desconocido, y eso es todo.

Los bloques de captura en todo el lugar son una muy mala idea.

Diseñe sus API de manera que sea fácil expresar su intención, es decir, declarar si espera un caso determinado, como clave no encontrada o no. Los usuarios de su API pueden elegir la llamada de lanzamiento solo para casos realmente inesperados.

Supongo que la razón por la cual las personas resienten las excepciones y van demasiado lejos al omitir esta herramienta crucial para la automatización del manejo de errores y una mejor separación de las preocupaciones de los nuevos idiomas son malas experiencias.

Eso y algunos malentendidos sobre para qué son realmente buenos.

Simularlos haciendo TODO a través del enlace monádico hace que su código sea menos legible y fácil de mantener, y termina sin un seguimiento de la pila, lo que empeora este enfoque.

El manejo de errores de estilo funcional es ideal para casos de error esperados.

Deje que el manejo de excepciones se encargue automáticamente de todo lo demás, para eso está :)

hacendado
fuente
3

Swift usa los mismos principios aquí que Objective-C, solo que más consecuentemente. En Objective-C, las excepciones indican errores de programación. No se manejan excepto por las herramientas de informe de fallas. El "manejo de excepciones" se realiza reparando el código. (Hay algunas excepciones de ejem. Por ejemplo, en las comunicaciones entre procesos. Pero eso es bastante raro y muchas personas nunca se topan con él. Y Objective-C en realidad tiene try / catch / finally / throw, pero rara vez los usas). Swift acaba de eliminar la posibilidad de detectar excepciones.

Swift tiene una característica que se parece al manejo de excepciones, pero solo es un manejo forzado de errores. Históricamente, Objective-C tenía un patrón de manejo de errores bastante generalizado: un método devolvería BOOL (YES para el éxito) o una referencia de objeto (nil para falla, no nil para éxito), y tenía un parámetro "puntero a NSError *" que se usaría para almacenar una referencia NSError. Swift convierte automáticamente las llamadas a dicho método en algo que parece un manejo de excepciones.

En general, las funciones Swift pueden devolver fácilmente alternativas, como un resultado si una función funcionó bien y un error si falla; eso hace que el manejo de errores sea mucho más fácil. Pero la respuesta a la pregunta original: los diseñadores de Swift obviamente sintieron que crear un lenguaje seguro y escribir código exitoso en dicho idioma es más fácil si el idioma no tiene excepciones.

gnasher729
fuente
Para Swift, esta es la respuesta correcta, OMI. Swift tiene que mantenerse compatible con los marcos del sistema Objective-C existentes, por lo que no tienen excepciones tradicionales. Hace un tiempo escribí una publicación de blog sobre cómo funciona ObjC para el manejo de errores: orangejuiceliberationfront.com/…
uliwitness
2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

En C terminarías con algo como lo anterior.

¿Hay cosas que no puedo hacer en Swift que pueda hacer con excepciones?

No, no hay nada Simplemente terminas manejando códigos de resultados en lugar de excepciones.
Las excepciones le permiten reorganizar su código para que el manejo de errores esté separado de su código de ruta feliz, pero eso es todo.

metal de piedra
fuente
Y, del mismo modo, ¿esas llamadas para ...throw_ioerror()devolver errores en lugar de arrojar excepciones?
orome
1
@raxacoricofallapatorius Si alegamos que no existen excepciones, supongo que el programa sigue el patrón habitual de devolver códigos de error en caso de error y 0 en caso de éxito.
stonemetal
1
@stonemetal Algunos idiomas, como Rust y Haskell, usan el sistema de tipos para devolver algo más significativo que un código de error, sin agregar puntos de salida ocultos como lo hacen las excepciones. Una función Rust puede, por ejemplo, devolver una Result<T, E>enumeración, que puede ser an Ok<T>o an Err<E>, Tsiendo el tipo que deseaba si existiera y Eun tipo que representa un error. La coincidencia de patrones y algunos métodos particulares simplifican el manejo de éxitos y fracasos. En resumen, no asuma que la falta de excepciones significa automáticamente códigos de error.
8bittree
1

Además de la respuesta de Charlie:

Estos ejemplos de manejo de excepciones declarados que se ven en muchos manuales y libros, se ven muy inteligentes solo en ejemplos muy pequeños.

Incluso si deja de lado el argumento sobre el estado de objeto no válido, siempre provocan un gran dolor cuando se trata de una aplicación grande.

Por ejemplo, cuando tiene que lidiar con IO, usando alguna criptografía, puede tener 20 tipos de excepciones que pueden ser eliminadas de 50 métodos. Imagine la cantidad de código de manejo de excepciones que necesitará. El manejo de excepciones tomará varias veces más código que el código mismo.

En realidad, sabe cuándo no puede aparecer una excepción y simplemente no necesita escribir tanto manejo de excepciones, por lo que solo usa algunas soluciones para ignorar las excepciones declaradas. En mi práctica, solo alrededor del 5% de las excepciones declaradas deben manejarse en código para tener una aplicación confiable.

konmik
fuente
Bueno, en realidad, estas excepciones a menudo se pueden manejar en un solo lugar. Por ejemplo, en una función de "actualización de datos de descarga" si el SSL falla o el DNS no se puede resolver o el servidor web devuelve un 404, no importa, tómalo en la parte superior e informa el error al usuario.
Zan Lynx