stringtranslate.com

Problema de semipredicado

En programación informática , un problema de semipredicado ocurre cuando una subrutina destinada a devolver un valor útil puede fallar, pero la señalización de falla utiliza un valor de retorno que de otro modo sería válido . [1] El problema es que quien llama a la subrutina no puede saber qué significa el resultado en este caso.

Ejemplo

La operación de división da como resultado un número real , pero falla cuando el divisor es cero . Si tuviéramos que escribir una función que realice una división, podríamos optar por devolver 0 en esta entrada no válida. Sin embargo, si el dividendo es 0, el resultado también es 0. Esto significa que no hay ningún número al que podamos devolver para señalar de forma única el intento de división por cero, ya que todos los números reales están en el rango de la división.

Implicaciones prácticas

Los primeros programadores manejaban casos potencialmente excepcionales como la división usando una convención que requería que la rutina que realizaba la llamada verificara las entradas antes de llamar a la función de división. Esto tenía dos problemas: primero, sobrecargaba enormemente todo el código que realizaba la división (una operación muy común); segundo, violaba los principios de no repetirse y de encapsulación , el primero de los cuales sugería eliminar el código duplicado y el segundo sugería que el código asociado a los datos se contuviera en un solo lugar (en este ejemplo de división, la verificación de la entrada se hizo por separado). Para un cálculo más complicado que la división, podría ser difícil para el que realiza la llamada reconocer una entrada no válida; en algunos casos, determinar la validez de la entrada puede ser tan costoso como realizar el cálculo completo. La función de destino también podría modificarse y entonces esperaría condiciones previas diferentes a las del que realiza la llamada; tal modificación requeriría cambios en cada lugar donde se llamara la función.

Soluciones

El problema del semipredicado no es universal entre las funciones que pueden fallar.

Uso de una convención personalizada para interpretar los valores de retorno

Si el rango de una función no cubre todo el espacio correspondiente al tipo de datos del valor de retorno de la función, se puede utilizar un valor que se sabe que es imposible en el cálculo normal. Por ejemplo, considere la función index, que toma una cadena y una subcadena, y devuelve el índice entero de la subcadena en la cadena principal. Si la búsqueda falla, la función puede programarse para que devuelva −1 (o cualquier otro valor negativo), ya que esto nunca puede significar un resultado exitoso.

Sin embargo, esta solución tiene sus problemas, ya que sobrecarga el significado natural de una función con una convención arbitraria:

Retorno multivalor

Muchos lenguajes permiten, mediante un mecanismo u otro, que una función devuelva múltiples valores. Si esto está disponible, la función puede rediseñarse para que devuelva un valor booleano que indique éxito o fracaso, junto con su valor de retorno principal. Si son posibles múltiples modos de error, la función puede devolver en su lugar un código de retorno enumerado (código de error) junto con su valor de retorno principal.

Varias técnicas para devolver múltiples valores incluyen:

Variable global para el estado de retorno

De manera similar a un argumento "out", una variable global puede almacenar qué error ocurrió (o simplemente si ocurrió un error).

Por ejemplo, si se produce un error y se indica (generalmente como se ha indicado anteriormente, con un valor ilegal como −1), la errnovariable Unix se configura para indicar qué valor se produjo. El uso de una variable global tiene sus inconvenientes habituales: la seguridad de los subprocesos se convierte en una preocupación (los sistemas operativos modernos utilizan una versión de errno segura para subprocesos) y, si solo se utiliza una variable global de error, su tipo debe ser lo suficientemente amplio como para contener toda la información interesante sobre todos los posibles errores en el sistema.

Excepciones

Las excepciones son un esquema ampliamente utilizado para resolver este problema. Una condición de error no se considera en absoluto un valor de retorno de la función; se interrumpe el flujo de control normal y el manejo explícito del error se lleva a cabo de manera automática. Son un ejemplo de señalización fuera de banda .

Ampliación del tipo de valor de retorno

Tipos híbridos creados manualmente

En C , un enfoque común, cuando es posible, es utilizar un tipo de datos deliberadamente más amplio que el estrictamente necesario para la función. Por ejemplo, la función estándar getchar()se define con el tipo de retorno inty devuelve un valor en el rango [0, 255] (el rango de unsigned char) en caso de éxito o el valor EOF( definido por la implementación , pero fuera del rango de unsigned char) al final de la entrada o en un error de lectura.

Tipos de referencia que aceptan valores NULL

En lenguajes con punteros o referencias, una solución es devolver un puntero a un valor, en lugar del valor en sí. Este puntero de retorno puede entonces establecerse en null para indicar un error. Normalmente es adecuado para funciones que devuelven un puntero de todos modos. Esto tiene una ventaja de rendimiento sobre el estilo de manejo de excepciones OOP, [4] con el inconveniente de que los programadores negligentes pueden no comprobar el valor de retorno, lo que resulta en un bloqueo cuando se utiliza el puntero no válido. Si un puntero es nulo o no es otro ejemplo del problema del predicado; null puede ser una bandera que indica un fallo o el valor de un puntero devuelto correctamente. Un patrón común en el entorno UNIX es establecer una variable separada para indicar la causa de un error. Un ejemplo de esto es la función de la biblioteca estándar de C. fopen()

Tipos implícitamente híbridos

En lenguajes de tipado dinámico , como PHP y Lisp , el enfoque habitual es devolver false, noneo nullcuando falla la llamada a la función. Esto funciona devolviendo un tipo diferente del tipo de retorno normal (expandiendo así el tipo). Es un equivalente de tipado dinámico a devolver un puntero nulo.

Por ejemplo, una función numérica normalmente devuelve un número (int o float), y aunque cero puede ser una respuesta válida, false no lo es. De manera similar, una función que normalmente devuelve una cadena puede a veces devolver la cadena vacía como una respuesta válida, pero devolver false en caso de error. Este proceso de manipulación de tipos requiere cuidado al probar el valor de retorno: por ejemplo, en PHP, use ===(ie, equal and of same type) en lugar de solo ==(ie, equal, after automatic type conversion). Funciona solo cuando la función original no está destinada a devolver un valor booleano, y aún requiere que la información sobre el error se transmita por otros medios.

Tipos explícitamente híbridos

En Haskell y otros lenguajes de programación funcional , es común usar un tipo de datos que sea tan grande como sea necesario para expresar cualquier resultado posible. Por ejemplo, uno puede escribir una función de división que devuelva el tipo Maybe Real, y una getcharfunción que devuelva Either String Char. El primero es un tipo de opción , que tiene solo un valor de error, Nothing. El segundo caso es una unión etiquetada : un resultado es una cadena con un mensaje de error descriptivo o un carácter leído correctamente. El sistema de inferencia de tipos de Haskell ayuda a garantizar que los invocadores se ocupen de los posibles errores. Dado que las condiciones de error se vuelven explícitas en el tipo de función, mirar su firma le dice inmediatamente al programador cómo tratar los errores. Además, las uniones etiquetadas y los tipos de opción forman mónadas cuando se les dota de funciones apropiadas: esto se puede usar para mantener el código ordenado al propagar automáticamente las condiciones de error no manejadas.

Ejemplo

Rust tiene tipos de datos algebraicosResult<T, E> y viene con los tipos y integrados Option<T>.

fn  find ( clave : String )  -> Opción < String > { if clave == "hola" { Some ( clave ) } else { None } }            

El lenguaje de programación C++ introducido std::optional<T>en la actualización C++17 .

std :: optional < int > find_int_in_str ( std :: string_view str ) { constexpr auto digits = "0123456789" ; auto n = str.find_first_of ( digits ); if ( n == std :: string :: npos ) { // La cadena simplemente no contiene números , no necesariamente un error return std :: nullopt ; }                      int resultado ; // Más lógica de búsqueda que establece 'resultado' devuelve resultado ; }    

y std::expected<T, E>en la actualización de C++23

enumeración clase parse_error { kEmptyString , kOutOfRange , kNotANumber };      std :: esperado < int , parse_error > parse_number ( std :: string_viewstr ) { if ( str.empty ()) { // Marcar una situación inesperada entre varias return std :: esperado ( parse_error :: kEmptyString ) ; }            int resultado ; // Más lógica de conversión que establece 'resultado' return resultado ; }    

Véase también

Referencias

  1. ^ Norvig, Peter (1992). "El solucionador de problemas general". Paradigmas de programación de inteligencia artificial: estudios de casos en Common LISP . Morgan Kaufmann . pág. 127. ISBN. 1-55860-191-0.
  2. ^ "Tipos integrados". Documentación de Python 3.10.4 .
  3. ^ "Si io jes negativo, el índice es relativo al final de la secuencia s: len(s) + io len(s) + jse sustituye". Nota de operaciones de secuencia comunes (3).
  4. ^ Por qué las excepciones deberían ser excepcionales: un ejemplo de comparación de desempeño.