Semipredicate problem

Semipredicate problem

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. The problem is that the caller of the subroutine cannot tell what the result means in this case.

Example

The division operation yields a real number, but fails when the denominator 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 numerator is 0, the result is 0 too. Also, even if a nonzero numerator would be required, dividing a small number by a very large one can yield 0 as well, due to rounding errors. 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.

Practical implications

In the case of division, a convention could be put into place requiring the caller to verify the validity of the input before calling the division function. This is undesirable for two reasons. First, it greatly encumbers all code that performs division. Second, it violates the important principle of encapsulation in programming, whereby treatment of concerns should be contained to one place. If we imagine a more complicated computation than division, the caller may not even know that invalid input is being handed to the target function; indeed, figuring out that the input is invalid may be as costly as performing the entire computation.

Solutions

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 -1 (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.

Multivalued return

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 of a boolean return an enumeration of error codes.

Various techniques for returning multiple values include:
* Returning a tuple of values.
* Languages with pointers and those which allow pass by reference, or an equivalent mechanism, can allow for multivalued return by designating some parameters as "out" arguments. In this case, the function could just return the error value, with a mutable value being passed to the function intended to store the actual result upon success.
* Some logic programming languages don't even have specific "return" values, returning all data through "out" arguments.

Error codes

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 signaled (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, 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

Exceptions are one widely used scheme for solving this problem (as well as others). 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. Exceptions also clear the calling code of clutter associated with checking return values after each call. They are an example of out of band signalling.

Expanding the type

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 NULL, to indicate an error. This approach may cause some overhead, and is typically suited to functions which would be returning a pointer anyway.

In dynamically-typed languages, such as PHP, Python 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 would normally return 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: eg. 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 returning Either String Char. The first case has only one failure value, Nothing. In the second, a result is either some string with a descriptive error message, or a successfully read character. Haskell's type inference system helps ensure that the caller 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. Monads keep the code tidy.


Wikimedia Foundation. 2010.

Игры ⚽ Нужно решить контрольную?

Look at other dictionaries:

  • Exception handling — is a programming language construct or computer hardware mechanism designed to handle the occurrence of exceptions, special conditions that change the normal flow of program execution. Programming languages differ considerably in their support… …   Wikipedia

  • Nullable type — In programming, nullable types are a feature of some statically typed programming languages which allows a data type to be set to the special value NULL instead of their common range of possible values. For value types or built in data types like …   Wikipedia

Share the article and excerpts

Direct link
Do a right-click on the link above
and select “Copy Link”