Next / Previous / Contents / Shipman's homepage

5.2. Preconditions: The other side of the contract

So far we have talked about how intended functions describe the semantics of a prime — what the code does. However, your code may not be able to guarantee all the conditions necessary for successful operation. This means that you may specify preconditions in order shift the burden of certain conditions onto the caller.

Let's take for example a function sqrt(x) that computes the square root. Here's our first attempt:

    # [ y  :=  square root of x ]
    y = math.sqrt(x)

However, what if the caller passes a negative argument? Worse yet, what if the caller passes a string argument? We're expecting x to be a number!

The way to handle this is to specify one or more preconditions. A precondition is a statement that must be true for the code to work properly. Here's our revised intended function for sqrt(x).

    # [ x is a nonnegative number ->
    #       y  :=  square root of x ]

Another name for this convention is partial specification. We define what our function does if x is a nonnegative number, but we don't define what it does for any other cases. The precondition precedes the rest of the intended function, followed by “->”.

Another way to read this intended function is as a contract:

If the caller insures that all preconditions are true, the prime guarantees to make the specified changes in state items.

Recall Stan Kelly-Bootle's definition of an interface in Section 2, “The contract-based approach to program construction”? With our improved intended function, we can apportion the blame for malfunctions quite clearly. If the caller supplied a nonnegative number and didn't get the right result, it's the fault of the sqrt function. If the caller didn't supply a nonnegative number, it's the caller's fault if the result is wrong.

Here's an example of multiple preconditions: an intended function for the atan2(y,x) function in the standard Python math module.

    # [ (y is a number) and (x is a number) ->
    #       return the angle that a vector from the origin to
    #       (x,y) makes relative to the x-axis, in radians, in
    #       the range [-pi, pi] ]

What about functions and methods with optional arguments? Here's an example intended function to describe the s.rjust(n[, fill]) method of the Python str type.

    # [ (s is a string) and (n is a positive int) and
    #   (fill is a string) ->
    #     return s, left-padded to length n with fill (defaulting
    #     to spaces) ]

There are other ways to write this intended function. For example, you could write the function signature as “s.rjust(n, fill=' ')”, in which case the intended function wouldn't even have to mention the default value process because it is built into the Python semantics for function calls: if you leave out the second argument to .rjust(), it's the same as if you passed a space for that argument.