Chapter 2 · Part I

Contracts as a Design Tool

A precondition is not a safety check bolted onto a finished routine. It is part of the routine's specification — a claim about the world that shapes the design before a line of the body is written.

Most programmers first meet preconditions as defensive code: a guard at the top of a function that throws if an argument is wrong. That is a real use, but it is the smallest one, and mistaking it for the whole is why contracts are so often written as an afterthought or skipped entirely. This chapter teaches them the other way around — as a design tool, introduced now because they are the connective tissue of the entire book.

A contract answers a question that types alone cannot: not "what shape is this value?" but "under what conditions is this operation meaningful, and what does it promise in return?" Nex gives you four ways to answer, and they will recur in every part that follows.

2.1 The Four Forms

Nex builds contracts into the ordinary routine and class forms, not into a separate assertion library. There are four:

  • require — a precondition: what must be true for the caller to invoke this routine at all.
  • ensure — a postcondition: what the routine guarantees on exit, provided its precondition held.
  • invariant — a standing property of an object that holds before and after every one of its operations, or a property preserved across every turn of a loop.
  • variant — a quantity that strictly decreases toward a bound, used to prove that a loop or recursion terminates.

Here is an account that uses the first three. Read the contracts first and the body second — that is the order in which they were designed.

class Account
  create
    open(initial: Money)
      require
        non_negative_open: initial >= Money.zero
      do
        balance := initial
      end

  feature
    balance: Money

    withdraw(amount: Money)
      require
        positive: amount > Money.zero
        sufficient_funds: amount <= balance
      do
        balance := balance - amount
      ensure
        balance_reduced: balance = old balance - amount
        never_negative: balance >= Money.zero
      end

    deposit(amount: Money)
      require
        positive: amount > Money.zero
      do
        balance := balance + amount
      ensure
        balance_grew: balance = old balance + amount
      end

  invariant
    solvent: balance >= Money.zero
end

Every clause has a name — sufficient_funds, never_negative, solvent — and the names are not decoration. They are the vocabulary of the design. When a contract is violated at runtime, the failure reports which named claim broke, so a bug announces itself in domain language rather than as an anonymous stack trace. Writing the name forces you to say, in words, what you believe — and half the value of a contract is extracted at the moment you are forced to name it, long before it ever runs.

2.2 The Precondition Is a Specification

Look again at withdraw. Its precondition says amount <= balance. A defensive programmer would instead write the check inside the body — if amount > balance then return error — and hand the caller a failure to inspect. The contract makes the opposite choice, and the difference is the whole point.

A precondition is a statement about whose responsibility a condition is. By writing require sufficient_funds: amount <= balance, the routine declares: establishing this is the caller's job, not mine. Inside the body, the routine may then treat sufficiency as a fact. It does not re-check; it does not branch; it does not return an error for the case, because the case is not its to handle. The contract has moved the obligation across the boundary and, in doing so, has made the body simpler and the caller's duty explicit.

This is what it means to say a precondition is a specification rather than an assertion. An assertion asks, "is this true right now?" A specification asks, "who must make this true, and who may assume it?" The first is a runtime event. The second is a design decision — a division of labour written into the interface — and it holds whether or not the check ever executes.

A word on where checks run

Contracts can be verified at runtime when enabled, and a violated one fails loudly at the point of the broken promise. But their primary value is not the runtime check. It is the specification: the redistribution of responsibility that lets the interior stop defending itself against cases the boundary already ruled out. A contract that never fires has still done its job.

2.3 Postconditions, and the Word old

Where a precondition binds the caller, a postcondition binds the routine. ensure balance_reduced: balance = old balance - amount promises that, if you met the precondition, the balance afterward is exactly the balance before minus the amount — no rounding drift, no fee sneaked in, no forgotten path that leaves the balance untouched.

The keyword old refers to the value an expression had on entry, before the body ran. It is what lets a postcondition talk about change rather than just about the final state: not merely "the balance is non-negative" but "the balance moved by precisely this much." That distinction is where a whole class of quiet arithmetic bugs would otherwise live. A postcondition with old is a promise about the transition, and transitions are exactly what the order core of our running system must never get wrong.

Notice how the two halves cooperate. The precondition lets the body assume; the postcondition obliges the body to deliver. Together they form a complete specification of the routine that a reader can trust without reading the body at all — which is precisely what you want at the seams of a large system, where you compose routines you did not write and will not re-read.

2.4 Invariants: The Object's Standing Promise

A precondition and postcondition describe a single routine. An invariant describes the whole object, across its entire lifetime. The Account's invariant, solvent: balance >= Money.zero, is a promise that holds after construction and after every method — that there is no sequence of legal operations, however long, that can leave an account with a negative balance.

This is a stronger and more useful claim than any single postcondition, because it is closed under composition. Each method must both assume the invariant on entry and re-establish it on exit; in exchange, no method ever has to worry about receiving an object in a broken state. The invariant is the object's contract with itself, and it is the constraint tradition of Chapter 1 in its most local form: a standing narrowing of what states the object may occupy, enforced at every step rather than hoped for.

When we model the order core in Part II, its invariants will carry most of the correctness argument — a placed order's total equals the sum of its lines, a shipped order has a tracking id, and so on. The invariant is where "make illegal states unrepresentable" lands for a mutable object: the states are still reachable in memory, but the invariant forbids the object from ever resting in one.

2.5 Variants: Promising an End

The fourth form addresses a hardness the other three do not touch: not correctness but termination. A loop can preserve every invariant and satisfy every postcondition and still never stop. A variant is an integer quantity, bounded below, that must strictly decrease on every iteration — and a quantity that only ever goes down and cannot go below its bound cannot decrease forever, so the loop must end.

-- Sum a list of line totals. The variant proves the loop terminates.
function sum_totals(items: Array[Money]): Money
  do
    from
      let i: Integer := 0
      result := Money.zero
    invariant
      in_range: 0 <= i and i <= items.length
    variant
      items.length - i
    until
      i >= items.length
    do
      result := result + items.get(i)
      i := i + 1
    end
  end

The variant here is items.length - i. It starts at the list's length, drops by one each turn as i rises, and cannot fall below zero because the loop stops when i reaches the length. Stating it forces you to name why the loop ends — and, as with preconditions, most of the value arrives at the moment of naming. A loop whose variant you cannot write is a loop whose termination you do not actually understand.

2.6 Contracts as the Membrane

Everything so far has shown contracts guarding a single object. Their deeper role in this book is guarding a boundary — the seam where an open edge hands a value to a constrained core.

Recall the arrangement from Chapter 1: the pricing engine is deliberately open, so that new rules can be added without the core knowing they exist. But that openness has a cost. When some rule the core has never heard of produces a price, the compiler cannot vouch for it — the rule was designed to live outside the compiler's knowledge. The type system, which guards the doors we can name in advance, has nothing to say about a value arriving from a door left open on purpose. This is exactly the moment types fall silent.

A contract speaks where the type is silent. The core exposes a boundary routine whose precondition states precisely what any price must satisfy to be admitted — regardless of which rule produced it:

-- The core accepts a quote from the open pricing edge — but only on its terms.
apply_quote(order: Placed, quote: Money)
  require
    priced_in_order_currency: quote.currency = order.total.currency
    non_negative: quote >= Money.zero
    not_below_floor: quote >= order.floor_price
  do
    order.set_total(quote)
  ensure
    total_is_quote: order.total = quote
    still_valid: order.is_consistent
  end

Any pricing rule at all may compute quote — a promotion, a loyalty tier, something invented next year. The core does not know and does not care. What it insists on is the contract at the doorway: the quote is in the right currency, not negative, not below the order's floor. The rule's author, on the other side of the membrane, reads the same precondition and knows exactly what they must deliver for their price to be accepted. Neither side has to see inside the other. The precondition is the interface between a discipline of openness and a discipline of constraint.

This is the pattern the whole book turns on. In Part IV, when we step outside the type system into generic dispatch, and in Part V, when computation runs on partial information, the type checker will fall silent again and again — and each time, a contract will take over the guarding. That recurrence is what makes constraint and flexibility one story rather than two. With the tool in hand, we can now go and constrain the core. The next part begins where correctness begins: making illegal states impossible to write down.