Concepts / Subroutine Contracts in Program Design

Subroutine Contracts in Program Design

A precondition is a condition that must be true when a subroutine is called if it is to work correctly, and it is an obligation of the caller.

  • Programming
Interactive lab

Try it: The math Module: Logs, Roots and Angles

What math.log(x), math.log10(x), math.log(x, base), math.sqrt(x) and math.sin / cos / tan really compute: which logarithm base each call uses, why 0 and negative inputs must be checked before the call (ValueError: math domain error), why trigonometric functions need radians, and that math.pi and every result are binary64 approximations (math.log(1000, 10) prints 2.9999999999999996).

How it works

  1. math.log(x) is the natural logarithm (base e); math.log10(x) is base 10; math.log(x, base) computes log(x) / log(base) with two rounded natural logs.
  2. Logarithms need x > 0 and math.sqrt needs x >= 0; otherwise the call raises ValueError: math domain error, so a guard checks the input first.
  3. math.sin, math.cos and math.tan read their argument as radians: convert degrees with degrees / 360.0 * 2 * math.pi.
  4. math.pi is a float value (no parentheses) that approximates pi to about 16 digits, so sin(180 degrees) is 1.2246467991473532e-16, not 0.
  5. Every result is the nearest binary64 value; print shows the shortest repr that reads back as the same float, while the stored value has more digits.

Default run (6 steps): Ready: the program calls math.log10(x). … print(result) shows 3.0.

Simplified: One-call programs over int / float literals you type (|x| up to 1e15). The results come from the browser's IEEE-754 math library: math.sqrt and the radians arithmetic are identical to CPython; log / sin / cos / tan match CPython 3.12 (Windows) exactly for about 91% of the tested inputs and are within 2 units in the last place otherwise, because C math libraries differ in the last bit. Error messages are CPython 3.12's (3.14 says 'expected a positive input').

Educational simulation

Loading the simulation…

Why Trusting a Subroutine Blindly Falls Short

When a program is built out of subroutines, each subroutine becomes a piece you rely on without re-reading its internals every time you use it. That reliance raises a practical question: what exactly does a subroutine need from the code that calls it, and what does it promise in return? A tempting first answer is simple. If I call a subroutine, it should just do its job, so I can hand it whatever values I have and trust the result.

This answer is incomplete. A subroutine is written with certain circumstances in mind. When a caller steps outside those circumstances, the subroutine has no obligation to behave well, and the reason is not that the subroutine is poorly written. The caller has not held up its side of the arrangement. To see the gap clearly, we need to describe the agreement between a subroutine and its caller using two kinds of conditions: one that the caller must make true before the call, and one that the subroutine guarantees afterward.

Tracing a Valid Call to Math.sqrt

Before defining any terms, follow one call through from start to finish. The caller has a value it wants to take the square root of, and it passes that value as the parameter x. The state of the program is what matters at each step, so we will track it explicitly.

Worked example

A valid call: square root of 9

The caller wants the square root of 9 and calls Math.sqrt(x) with x set to 9. Track the state before the call, during the call, and after the call.

  1. 1
    Before the call

    The caller has supplied the parameter x = 9. This value is greater than or equal to zero, so the precondition that x is greater than or equal to zero is satisfied at the moment of the call.

  2. 2
    During the call

    Control passes from the caller to Math.sqrt. The subroutine relies on the precondition the caller has established and computes the square root of 9.

  3. 3
    After the call

    The return value is 3. The postcondition says the square of the returned value equals the parameter that was supplied, so we check that 3 multiplied by 3 equals 9. It does, so the postcondition holds.

Result

The call returns 3, and the caller can rely on the postcondition because the precondition was satisfied before the call.

Preconditions: The Caller's Obligation

Definition

A precondition is a condition that must be true when a subroutine is called if it is to work correctly, and it is an obligation of the caller.

The word that matters most in that definition is when. A precondition is judged at the moment of the call, not at some later point. It describes the state the caller must establish before handing control to the subroutine. Because the caller is the one who arranges that state, a precondition is an obligation of the caller rather than of the subroutine.

The source gives a concrete illustration. For the built-in function Math.sqrt(x), the precondition is that the parameter x is greater than or equal to zero. This requirement is not an arbitrary rule imposed by the library. It follows from what the subroutine is trying to compute, because it is not possible to take the square root of a negative number.

What happens when a precondition is violated? The source states plainly that if you call a subroutine without meeting its precondition, there is no reason to expect it to work properly. The program might crash or give incorrect results. The point is about accountability. In that situation the fault lies with the caller, because the caller did not live up to its side of the arrangement.

Callermust establish preconditionMath.sqrt subroutineowes postcondition1. call with x = 9 (precondition met)2. return 3 (postcondition holds)
In what order do the caller and the subroutine exchange responsibilities, and at which step does the caller's precondition obligation end and the subroutine's postcondition guarantee begin?

The sequence above reads from top to bottom. The caller's obligation ends at the moment of the call: by then the precondition must already be true. Only after the subroutine runs does the subroutine's obligation, the postcondition, come into play.

Postconditions: The Subroutine's Promise

Definition

A postcondition is a condition that will be true after the subroutine has run, assuming its preconditions were met and there are no bugs, and it is an obligation of the subroutine.

Where the precondition is about what the caller must provide, the postcondition is about what the subroutine delivers. Once the subroutine has run under the agreed circumstances, the postcondition describes a result the caller can rely on.

Notice the two qualifications packed into the definition. The postcondition is only guaranteed if the precondition was met, and it assumes the subroutine itself contains no bugs. These qualifications are not decoration. They make explicit that a postcondition is a promise made under conditions, not an unconditional fact about the function.

Reading the Built-in Call Math.sqrt(x)

The contract of a built-in subroutine can be located in its call. Each part of the call corresponds to one side of the agreement. The name identifies the subroutine, the parameter carries the input that the precondition constrains, and the returned value carries the output that the postcondition describes.

Math.sqrtthe subroutine being calledxprecondition: x is greaterthan or equal to zeroreturned valuepostcondition: its squareequals xreceivesproduces
Which parts of a built-in function's call correspond to its precondition (the input requirement) and its postcondition (the output guarantee)?

Putting the Contract Together

Taken together, the precondition and the postcondition describe the contract of a subroutine. The interaction between a subroutine and the rest of the program is specified by that contract, and a convenient way to express the contract is in terms of preconditions and postconditions. The two conditions attach to the two parties. The caller is responsible for the precondition, which must hold at the moment of the call. The subroutine is responsible for the postcondition, which describes its result afterward.

The arrow of dependency runs from the precondition to the postcondition. The postcondition is only a dependable promise when the precondition has been satisfied. That dependency is what makes the contract a contract rather than two independent statements.

Comparing a Satisfied and a Violated Precondition

Apply the same reasoning to a second call, Math.sqrt(-4). The precondition fails, because -4 is not greater than or equal to zero. The postcondition is no longer something the caller can rely on, because the source makes its validity conditional on the precondition being met. The subroutine has not failed the contract. The caller has.

x = 9precondition metx = -4precondition violatedreturn 3postcondition holds: 3times 3 equals 9no guaranteepostcondition cannot berelied onguarantee availableguarantee lost
How does the outcome differ when the precondition is satisfied versus when it is not, and what guarantee is lost in the second case?
QuestionPreconditionPostcondition
When is it judged?At the moment of the callAfter the subroutine has run
Who is responsible?The callerThe subroutine
What does it describe?The state the caller must establishThe result the caller can rely on
What does it depend on?Nothing else; it is the starting conditionThat the precondition was met and there are no bugs

The two kinds of conditions, separated by the questions a programmer should ask about each

Mistakes in Reading a Contract

  • Common mistake

    Assuming a subroutine works correctly no matter what it is given

    What it looks like

    Passing Math.sqrt(-4) and expecting a meaningful square root

    Why it fails

    A subroutine is written with certain circumstances in mind. Outside them it has no obligation to behave well.

    Do this instead

    Check the precondition at the moment of the call and make sure the caller has established it.

  • Common mistake

    Blaming the subroutine when a precondition fails

    What it looks like

    Concluding that Math.sqrt is broken after it gives an unreliable result for a negative input

    Why it fails

    The source places the fault with the caller, because the caller did not meet the precondition.

    Do this instead

    Trace the input back to the caller and ask whether the required condition was true before the call.

  • Common mistake

    Treating the postcondition as unconditional

    What it looks like

    Relying on the squared result equaling the parameter for every input

    Why it fails

    The postcondition is only guaranteed when the precondition was met and the subroutine has no bugs.

    Do this instead

    Always pair a postcondition with the precondition that makes it valid.

  • Common mistake

    Confusing the two kinds of conditions because both describe the same call

    Why it fails

    Both describe one call, but from different positions: one before the subroutine runs, one after.

Practice: Reading a Contract

EASY

A caller wants the square root of a value stored in a variable and calls Math.sqrt(x). State the precondition that the caller must establish before the call, identify the postcondition the subroutine promises, and say which of the two is the caller's obligation and which is the subroutine's. Then decide what you should check first if the returned result looks wrong.

Hints
  • Ask which condition is judged at the moment of the call.
  • The source ties the square root's validity to whether x is greater than or equal to zero.
  • If the precondition fails, consider whether the fault lies with the subroutine or with the caller.

Key Takeaways

  • A precondition is a condition that must be true when a subroutine is called, and it is an obligation of the caller.
  • A postcondition is a condition that will be true after the subroutine has run, assuming its precondition was met and there are no bugs, and it is an obligation of the subroutine.
  • Preconditions and postconditions together express the contract that specifies how a subroutine interacts with the rest of the program.
  • For Math.sqrt(x), the precondition is that x is greater than or equal to zero, and the postcondition is that the square of the returned value equals x, which holds only when the precondition is met.