stringtranslate.com

problema de semipredicado

En programación de computadoras , 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 produce un número real , pero falla cuando el divisor es cero . Si tuviéramos que escribir una función que realice 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 regresar para señalar de manera única el intento de división por cero, ya que todos los números reales están en el rango de división.

Implicaciones prácticas

Los primeros programadores manejaron casos potencialmente excepcionales, como la división, utilizando una convención que requería que la rutina de 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); en segundo lugar, violó los principios de No repetirse y de encapsulación , el primero de los cuales sugiere eliminar el código duplicado y el segundo sugiere que el código asociado a datos esté contenido en un solo lugar (en este ejemplo de división, la verificación de la entrada se realizó por separado). ). Para un cálculo más complicado que la división, podría resultar difícil para quien llama reconocer una entrada no válida; En algunos casos, determinar la validez de la entrada puede ser tan costoso como realizar todo el cálculo. La función de destino también podría modificarse y luego esperaría condiciones previas diferentes a las que esperaría la persona que llama; tal modificación requeriría cambios en cada lugar donde se llamó la función.

Soluciones

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

Usar 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 devolver −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:

Rentabilidad multivalor

Muchos lenguajes permiten, mediante un mecanismo u otro, que una función devuelva múltiples valores. Si está disponible, la función se puede rediseñar para devolver un valor booleano que indique éxito o fracaso, junto con su valor de retorno principal. Si son posibles varios modos de error, la función puede devolver 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 la devolución

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

Por ejemplo, si ocurre un error y se señala (generalmente como se indicó anteriormente, mediante un valor ilegal como −1), la errnovariable Unix se configura para indicar qué valor ocurrió. El uso de un 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 los subprocesos), y si solo se utiliza un error global, su tipo debe ser lo suficientemente amplio como para contener toda la información interesante sobre todos los errores 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; El flujo de control normal se interrumpe y el manejo explícito del error se realiza automáticamente. Son un ejemplo de señalización fuera de banda .

Ampliando el 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 de lo estrictamente necesario para la función. Por ejemplo, la función estándar getchar()se define con un 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 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 se puede establecer en nulo 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 OOP de manejo de excepciones, [4] con el inconveniente de que los programadores negligentes pueden no verificar 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 de predicados; nulo puede ser un indicador que indica un error 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 biblioteca estándar de C. fopen()

Tipos implícitamente híbridos

En lenguajes de escritura dinámica , 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 devolución normal (expandiendo así el tipo). Es un equivalente de tipo dinámico a devolver un puntero nulo.

Por ejemplo, una función numérica normalmente devuelve un número (int o flotante) y, si bien cero puede ser una respuesta válida, falso no lo es. De manera similar, una función que normalmente devuelve una cadena a veces puede devolver la cadena vacía como una respuesta válida, pero devolver falso en caso de error. Este proceso de malabarismo de tipos requiere cuidado al probar el valor de retorno: por ejemplo, en PHP, use ===(es decir, igual y del mismo tipo) en lugar de solo ==(es decir, igual, después de la conversión automática de tipos). Funciona sólo 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 funcionales , es común utilizar un tipo de datos que sea tan grande como sea necesario para expresar cualquier resultado posible. Por ejemplo, se puede escribir una función de división que devuelva el tipo Maybe Realy una getcharfunción que devuelva Either String Char. El primero es un tipo de opción , que tiene un solo valor de error Nothing. El segundo caso es una unión etiquetada : el 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 las personas que llaman se enfrenten a 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 opciones forman mónadas cuando se les dotan de funciones apropiadas: esto puede usarse para mantener el código ordenado propagando automáticamente condiciones de error no controladas.

Ejemplo

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

fn  buscar ( clave : Cadena )  -> Opción <Cadena> { si clave == " hola " { Algunos ( clave ) } más { Ninguno } }            

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

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

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

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

Ver también

Referencias

  1. ^ Norvig, Peter (1992). "El solucionador de problemas generales". Paradigmas de programación de inteligencia artificial: estudios de casos en LISP común . Morgan Kaufman . pag. 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 común (3).
  4. ^ Por qué las excepciones deberían ser excepcionales: un ejemplo de comparación de rendimiento.