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.
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.
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.
El problema del semipredicado no es universal entre funciones que pueden fallar.
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:
str.find
devuelve −1 si no se encuentra la subcadena, [2] pero −1 es un índice válido (los índices negativos generalmente comienzan desde el final [3] ).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:
x, y = f()
llama a la función f
que devuelve un par de valores y asigna los elementos del par a dos variables.GETHASH
función devuelve el valor de la clave dada en un mapa asociativo o, en caso contrario, un valor predeterminado. Sin embargo, también devuelve un valor booleano secundario que indica si se encontró el valor, lo que permite distinguir entre los casos "no se encontró ningún valor" y "el valor encontrado fue igual al valor predeterminado". Esto es diferente de devolver una tupla, en que los valores de retorno secundarios son opcionales : si a la persona que llama no le importan, puede ignorarlos por completo, mientras que los retornos con valores de tupla son simplemente azúcar sintáctico para devolver y descomprimir una lista, y cada persona que llama Siempre debe conocer y consumir todos los artículos devueltos.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 errno
variable 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.
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 .
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 int
y 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.
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()
En lenguajes de escritura dinámica , como PHP y Lisp , el enfoque habitual es devolver false
, none
o null
cuando 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.
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 Real
y una getchar
funció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.
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' ; }
i
o j
es negativo, el índice es relativo al final de la secuencia s
: len(s) + i
o len(s) + j
se sustituye". nota de operaciones de secuencia común (3).