skip to content

A library operation documents that its argument must be a non-empty collection, and a caller passes an empty one. Whose defect is that, and how do Eiffel, Racket, Kotlin and Rust each express and enforce that obligation differently?

level: middleimportance: must knowfreq 40%

answer

  1. Precondition = caller owes; postcondition = supplier owes
  2. Eiffel require blames the client, ensure blames the supplier
  3. Racket: two parties per boundary, callbacks flip blame
  4. Kotlin require vs check = argument vs state, no party
  5. Rust newtype = obligation discharged, nothing to check

basics

~20 s

The caller's. Preconditions are caller obligations. Eiffel's require blames the caller by name, Racket blames the module that crossed the boundary, Kotlin's require throws with no party named, and Rust encodes non-emptiness in the argument type so there is nothing left to check.

solid answer

~50 s

A precondition is the **caller's** obligation; postconditions and invariants are the **implementer's**. Languages disagree on whether a violation names a party at all. - **Eiffel** — a failed `require` raises a precondition violation attributed to the caller, and the supplier is explicitly entitled to assume it and not re-check. Documentation, check and blame are one construct. - **Racket** — `contract-out` records two parties per module boundary and the error names the guilty one. When a function is passed as an argument the blame flips: the callback's caller becomes the negative party. Higher-order blame is what almost no other system has. - **Kotlin** — `require` versus `check` splits the diagnosis (bad argument versus bad receiver state) but names no party, and both always run. - **Rust** — `NonZeroUsize` or a `NonEmpty` newtype discharges the obligation in the type system: no runtime failure, nobody to blame, and the caller pays once at construction. A contract violation is a bug; validating untrusted external input is not a contract.

code

eiffel · 11 lines
eiffel
first_item (c: COLLECTION [G]): G
    require
        not_empty: not c.is_empty
    do
        Result := c.item (1)
    ensure
        from_collection: c.has (Result)
    end

-- caller passes an empty collection ->
-- precondition violation "not_empty", reported against the calling routine

go deeper

for a junior

Be able to say which side owes a precondition and which side owes a postcondition, and give one concrete language keyword (Kotlin's require, Eiffel's require).

for a middle

Contrast at least two languages: one that mechanises the obligation (Eiffel), one that only mechanises the diagnosis (Kotlin's require versus check), and explain why double-checking a precondition is a smell.

for a senior

Bring in blame as a concept — Racket naming the guilty module and flipping polarity for callbacks — and draw the contract-versus-input-validation boundary explicitly.

for a principal

Frame it as where an obligation should be discharged: in the type system (Rust newtypes, Ada subtype predicates) at the edge, versus at every call boundary, and what that means for API totality and for what can be compiled out.

## The idea Design by Contract, introduced with Eiffel, models the relationship between a routine and its callers as a commercial agreement. Each side has obligations and each side gets benefits, and the obligations are written into the code rather than into prose. - **Precondition** — what the caller must guarantee before the call. Obligation of the caller, benefit to the implementer, who may assume it without checking. - **Postcondition** — what the routine guarantees on return, provided the precondition held. Obligation of the implementer, benefit to the caller. - **Class invariant** — a property of the object that holds between externally visible operations. Obligation of every operation of the class, benefit to every client. The non-obvious consequence is the *no double-checking* rule: if a condition is a precondition, the implementer should not also validate it and raise a friendly error. Two checks means neither side owns the condition, and the error you get depends on which check ran first. ## Blame is a design decision, not an error message What separates real contract systems from an assertion sprinkled in a function body is whether the violation identifies a *party*. **Eiffel** compiles the clause into the routine header. A failed `require` produces a precondition violation whose semantics are: the client called me wrongly. A failed `ensure` says: I failed to deliver. The exception type itself carries the accusation, and Eiffel's teaching material is explicit that a supplier which defensively re-checks its own preconditions has misunderstood the model. **Racket** goes further and makes blame first-class. `contract-out` attaches a contract to a module boundary, and every boundary has two parties: the module that provides the value (positive) and the module that uses it (negative). A violation message literally names the guilty module. The deep part is *higher-order* contracts. If you take a function as an argument and the contract on that argument's own parameter is violated, the fault is not with the module that supplied the callback — it is with you, the module that invoked it. Racket flips the polarity of nested arrow contracts to get this right, which is why a contract system for first-class functions cannot be a simple boolean assertion at the entry point: the check is deferred until the callback is applied, and the wrapper must remember whom to accuse. **Kotlin** ships the diagnostic half without the party half. `require(x.isNotEmpty())` raises `IllegalArgumentException` — a bad argument — and `check(state != null)` raises `IllegalStateException` — a bad receiver state. That split encodes exactly the caller-versus-implementer distinction in the *exception type*, but the two functions are ordinary library calls, always evaluated, with no relation to any inherited specification. **Rust** answers by refusing to have a runtime question. `NonZeroUsize`, or a hand-rolled `NonEmpty<T>` whose only constructor is a fallible `from_vec`, moves the obligation to the type. The caller proves non-emptiness once, at the point where the failure is actually recoverable and where there is real context to report, and the library function's signature is then total: every value of the parameter type is acceptable. Nothing can be stripped, nothing can drift out of date with the documentation, and there is no blame because there is no violation. ## Contracts versus input validation The most common professional error is using contract machinery on untrusted input. A precondition says *this must be true or the program has a bug*. Data arriving from a network request, a file or a user form can perfectly legitimately be empty; rejecting it is normal control flow that returns a 400 or an error value, not an assertion failure. The test is simple: if a well-behaved system can produce this input, it is validation, and it belongs in ordinary code that stays in the binary. If only a defect can produce it, it is a contract. This matters because several languages allow contract checks to be compiled out, and because contract failures should be unrecoverable by design: catching a precondition violation and continuing means continuing with a program you have just proved wrong. ## What a strong answer sounds like Name the party first (the caller), then show that you know the difference between a language that mechanises blame (Eiffel, Racket), a language that only mechanises the diagnosis (Kotlin), and a language that dissolves the problem in the type system (Rust, and Ada's subtype predicates in the same spirit). Finish with the boundary rule: contracts govern cooperating code, validation governs the outside world.

  • Racket flips blame when a function is passed as an argument. Why is that the correct assignment?
    A contract on a higher-order argument constrains how the *receiving* module may call the callback, not what the supplying module provided. If the receiver applies the callback with a value the contract forbids, the supplier did nothing wrong. Racket therefore swaps the positive and negative parties for each nested arrow, so the accusation lands on the code that made the bad call. A flat boolean assertion at the entry point cannot express this, because the violation only becomes observable later, when the callback is applied.
  • If a condition is a documented precondition, why is it wrong for the implementer to also validate it and raise a friendly error?
    Because then neither side owns it. The caller stops treating the requirement as its obligation and starts relying on the error, so the specification quietly becomes 'accepts anything and reports'. You also get two failure modes for one condition, and which one fires depends on build flags in languages that strip assertions. Eiffel states this explicitly; Rust reaches the same end by making the invalid value unconstructible, so there is only ever one place the failure can occur.

A contract is a delivery agreement: the customer owes a valid address (precondition), the courier owes a parcel at that address (postcondition). If the address is nonsense, nobody blames the courier — and Racket is the courier that writes the customer's name on the failure slip.

saying these in an interview costs you the question

  • Saying the library should defensively re-check a condition it has already published as a precondition
  • Treating a precondition violation as a recoverable exception that clients should catch and handle
  • Using contract assertions to validate untrusted request or file input
  • Claiming every language's assert is a contract — an assert has no inherited specification and no party
  • Saying Rust's newtype approach is 'just validation moved around', missing that it makes the invalid state unrepresentable

context