This article relies largely or entirely on a single source. Relevant discussion may be found on the talk page. Please help improve this article by introducing citations to additional sources. (February 2012) |
In computer programming, a semipredicate problem occurs when a subroutine intended to return a useful value can fail, but the signalling of failure uses an otherwise valid return value.[1] The problem is that the caller of the subroutine cannot tell what the result means in this case.
The division operation yields a real number, but fails when the divisor is zero. If we were to write a function that performs division, we might choose to return 0 on this invalid input. However, if the dividend is 0, the result is 0 too. This means there is no number we can return to uniquely signal attempted division by zero, since all real numbers are in the range of division.
Early programmers dealt with potentially exceptional cases, as in the case of division, using a convention that required the calling routine to check the validity of the inputs before calling the division function. This had two problems. First, it greatly encumbers all code that performs division (a very common operation). Second, it violates the Don't repeat yourself and encapsulation principles, where the former is about eliminating duplicated code, and the latter suggests that data associated code should be contained in one place (in this case the verification of input was done separately). If we imagine a more complicated computation than division, it could be hard for the caller to know what is considered invalid input; in some cases figuring out whether the input is valid may be as costly as performing the entire computation, there's also the possibility of the target function being modified and then expecting different preconditions than the ones the caller has checked for, such a change would require changes in all the places where the function was called from.
This section does not cite any sources. Please help improve this section by adding citations to reliable sources. Unsourced material may be challenged and removed. (June 2013) (Learn how and when to remove this template message) |
The semipredicate problem is not universal among functions that can fail.
If the function's range does not cover the entire data type defined for the function, a value known to be impossible under normal computation can be used. For example, consider the function index
, which takes a string and a substring, and returns the integer index of the substring in the main string. If the search fails, the function may be programmed to return -32,768 (or any other negative value), since this can never signify a successful result.
This solution has its problems, though; it overloads the natural meaning of a function with an arbitrary convention. First, the programmer must remember specific failure values for many functions, which of course cannot be identical if the functions have different domains. Second, a different implementation of the same function may choose to use a different failure value, resulting in possible bugs when programmers move from environment to environment. Third, if the failing function wishes to communicate useful information about why it had failed, one failure value is insufficient. Fourth, a signed integer halves the possible index range to be able to store the sign bit.
Many languages allow, through one mechanism or another, a function to return multiple values. If this is available, the function can be redesigned to return a boolean value signalling success or failure, in addition to its primary return value. If multiple error modes are possible, the function may instead return an enumerated return code (error code) in addition to its primary return value.
Various techniques for returning multiple values include:
GETHASH
function returns the value of the given key in an associative map, or a default value otherwise. However, it also returns a secondary boolean indicating whether the value was found, making it possible to distinguish between the "no value was found" and "the value found was equal to default value" cases. This is different than returning a tuple, in that secondary return values are optional – if a caller does not care about them, it may ignore them completely, whereas tuple-valued returns are merely syntactic sugar for returning and unpacking a list, and every caller must always know about and consume all items returned.Similar to an "out" argument, a global variable can store what error occurred (or simply whether an error occurred).
For instance, if an error occurs, and is signalled (generally as above, by an illegal value like −1) the Unix errno variable is set to indicate which value occurred. Using a global has its usual drawbacks: thread safety becomes a concern (modern operating systems use a thread-safe version of errno), and if only one error global is used, its type must be wide enough to contain all interesting information about all possible errors in the system.
Exceptions are one widely used scheme for solving this problem. An error condition is not considered a return value of the function at all; normal control flow is disrupted and explicit handling of the error takes place automatically. They are an example of out-of-band signalling.
In C, a common approach, when possible, is to use a data type deliberately wider than strictly needed by the function. For example, the standard function getchar()
is defined with return type int
and returns an unsigned char on success or the value EOF
(implementation-defined, but outside the range [0, 255]) on the end of the input or a read error.
In languages with pointers or references, one solution is to return a pointer to a value, rather than the value itself. This return pointer can then be set to null to indicate an error. it is typically suited to functions that return a pointer anyway, this has a performance advantage over OOP style of exception handling [2], with the drawback that negligent programmers may not check the return value, resulting in a crash when the invalid pointer is used, a common pattern in the UNIX environment is setting a separate variable to indicate the cause of an error, an example of this is the C standard library fopen()[1] function.
In scripting languages, such as PHP and Lisp, the usual approach is to return "false", "none" or "null" when the function call fails. This works by returning a different type to the normal return type (thus expanding the type). It is a dynamically-typed equivalent to returning a null pointer.
For example, a numeric function normally returns a number (int or float), and while zero might be a valid response; false is not. Similarly, a function that normally returns a string might sometimes return the empty string as a valid response, but return false on failure. This process of type-juggling necessitates care in testing the return value: e.g. in PHP, use === [i.e. equal and of same type] rather than just == [i.e. equal, after automatic type-conversion]. It works only when the original function is not meant to return a boolean value, and still requires that information about the error be conveyed via other means.
In Haskell and other functional programming languages, it is common to use a data type that is just as big as it needs to be to express any possible result. For example, we could write a division function that returned the type Maybe Real
, and a getchar
function returning Either String Char
. The first is an option type, which has only one failure value, Nothing
. The second case is a tagged union: a result is either some string with a descriptive error message, or a successfully read character. Haskell's type inference system helps ensure that callers deal with possible errors. Since the error conditions become explicit in the function type, looking at its signature immediately tells the programmer how to treat errors. In addition, tagged unions and option types form monads when endowed with appropriate functions: this may be used to keep the code tidy by automatically propagating unhandled error conditions.