1.3 The Function Design Recipe

Often when beginners are tasked with writing a program to solve a problem, they jump immediately to writing code. It doesn’t matter whether the code is correct or not, or even if they fully understand the problem: somehow the allure of filling up the screen with text is too tempting.

In CSC108, we teach the Function Design Recipe as a principled way of approaching problem-solving in Python. It delays writing any code at all until a complete docstring has been written, ensuring that we have thought through exactly what the function needs to do in all circumstances before we set about getting it to do that. Get into the habit of following the design recipe, and your teammates and even your future self will thank you later!

While the Function Design Recipe was taught in CSC108 (and we assume that you will review it on your own time if needed), we want to expand on some important aspects that will be incorporated more heavily in this course: preconditions in the function docstring, type contracts, and testing methodologies.

Preconditions

One of the most important purposes of a function docstring is to let others know how to use the function. After all, we don’t just write code for ourselves, but for other members of our development team or company, or even the world at large if we’re writing a library we think is useful to anyone.

The docstring of a function describes not only what the function does—through text and examples—but also the requirements necessary to use the function. One such requirement is the type contract: this requires that when someone calls the function, they do so with arguments of a specified type.

For example, given this function docstring:

def decreases_at(numbers: List[int]) -> int:
    """Return the index of the first number that is less than its predecessor.

    >>> decreases_at([3, 6, 9, 12, 2, 1, 8, 5])
    4
    """

We know that decreases_at expects to be called on a list of integers; if we violate the type contract, say by calling it on a single integer or a dictionary, we cannot expect it to work properly.

In practice, we often want to extend this idea beyond specifying the required type of arguments. For example, we might want to say that “this function must be given numbers between 1 and 10” or “the first argument must be greater than the second argument.” A precondition of a function is any property that the function’s arguments must satisfy to ensure that the function works as described. They are included in a function’s docstring, and form a crucial part of the function’s interface.

As a user of a function, preconditions are extremely important, since they tell you what you have to do to use the function properly. They limit how a function can be used. On the flip side, preconditions are freeing to the implementor of a function: by specifying a certain property in a precondition, the person writing the body of the function can go ahead and assume that this property is satisfied, which often leads to a simpler or more efficient implementation. Consider a method for searching a list. Binary search is efficient, but depends on having a sorted list. If the search method had to confirm this, the added work would make it slower than linear search! In this case, it makes sense to simply require the caller to provide a sorted list.

The bottom line is that specifying preconditions is part of the design of a function. It is a matter of specifying precisely what service we want to provide to the users of our functions—and what restrictions we want to impose upon them.