skip to content

Abstraction

Exposing what a concept promises while suppressing how it delivers, so clients depend on responsibilities and contracts, not on a realization. Interviewers probe what a good abstraction buys.

on this pageshow

questions

11

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

open as a page

Languages disagree about who decides that a concrete type satisfies an abstraction: in Java the type's author declares it, in Go the compiler infers it from method shape, and in Rust a third party may be forbidden from declaring it at all. Compare these models and what each one costs.

level: middleimportance: must knowfreq 48%

basics

~20 s

Nominal (Java, C#): the type's author writes implements, so retrofitting a library type needs a wrapper. Structural (Go, TypeScript): matching method shape is enough, so a consumer can define the abstraction afterwards. Rust traits are nominal but either side may write the impl, and the orphan rule forces a newtype when neither is local.

open as a page

You need to add an operation to an abstraction that code outside your control already implements, without breaking those implementations. Compare the mechanisms offered by Java's default methods, C#'s default interface members, Swift's protocol extensions and Go's optional-interface upgrade.

level: seniorimportance: must knowfreq 42%

basics

~20 s

Either add it with a default body or do not add it to the abstraction at all. Java default methods dispatch virtually and an implementer's own method always wins; C# default interface members are visible only through an interface-typed reference; Swift extension members that are not protocol requirements dispatch statically; Go publishes a second small interface and type-asserts for it.

open as a page

A repository operation that hides SQL lets a raw driver failure — a SQLSTATE code, a socket timeout — escape to a business-level caller. Explain why that is an abstraction-level defect, and how different languages' error models push you to fix it.

level: seniorimportance: must knowfreq 55%

basics

~20 s

The caller must now understand the layer it was insulated from, and its own callers inherit that coupling. Java translates and wraps, Go wraps with %w and reopens deliberately via errors.As, Rust converts through From at the ? boundary, Python chains with raise-from, Erlang crashes to a supervisor instead.

open as a page

A collection interface promises "give me the element at position i" for every implementation, but one implementation walks a chain of nodes to answer it. Which languages let a caller see that cost difference through the API itself, and which hide it?

level: middleimportance: should knowfreq 45%

basics

~20 s

Interfaces promise behaviour, not cost, so a linked implementation answers index-at-i in linear time behind the same call. C++ and Rust omit indexing from list types entirely; Scala splits IndexedSeq from LinearSeq; Java only adds a RandomAccess marker.

open as a page

Keeping a routine at a single level of abstraction means extracting its lower-level steps into named sub-routines. Where those sub-routines are allowed to live differs sharply by language. How does that change the decision, and what goes wrong when the only home is the type's own member list?

level: middleimportance: should knowfreq 45%

basics

~20 s

If a step's only home is a class member or a package-level function, every extraction widens a namespace and adds public-ish surface. Haskell where-bindings, Scheme internal defines, Pascal nested procedures and Kotlin local funs let a routine gain levels without gaining API.

open as a page

In Eiffel a subtype literally cannot tighten an inherited precondition, while in Python or Kotlin an override can silently demand more than its parent did. Explain the mechanism behind that difference and what each approach costs.

level: seniorimportance: should knowfreq 36%

basics

~20 s

Eiffel's require else is OR-ed with the inherited clause and ensure then is AND-ed with it, so only weakening and strengthening are expressible. Ada 2012's Pre'Class and D's in/out combine the same way. Kotlin and Python assertions merely replace the parent's, so tightening passes unnoticed.

open as a page

An object graph loaded from a database exposes related objects as ordinary fields, but reading one of those fields can fire a query, or fail outright. Compare how object-relational mappers in different languages handle that leak.

level: seniorimportance: should knowfreq 55%

basics

~20 s

Three doctrines: silent (Hibernate proxies and Rails associations query on field access, or throw once detached), loud (SQLAlchemy's raiseload, Rails strict_loading), or impossible (Elixir's Ecto returns a NotLoaded struct until you preload). Transparency is what makes the leak silent.

open as a page

Some frameworks make a network call look exactly like an ordinary method call on a local object. Explain what that abstraction cannot hide, and contrast it with languages and runtimes that keep remoteness visible in the syntax or the type.

level: principalimportance: should knowfreq 40%

basics

~20 s

Latency, partial failure, concurrency and the absence of shared memory cannot be hidden. CORBA and Java RMI put them behind an ordinary call; Erlang keeps send, receive and monitors syntactically distinct, Go forces an error return plus a context deadline, Cap'n Proto uses an eventual-send that returns a promise.

open as a page

Contract checks cost cycles, and several languages let you compile them out — Eiffel per class and per assertion level, Rust's debug_assert!, Java's assert being inert unless -ea is passed. How would you decide which checks may be stripped in a production build, and where is stripping never acceptable?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Strip checks that catch your own team's bugs; never strip a check standing between untrusted input and a decision — that is validation, not a contract. Eiffel strips per class and level, Rust's debug_assert! vanishes under --release while assert! stays, Java's assert is inert by default, and SPARK proves contracts so nothing runs at all.

open as a page

One school derives a program's abstraction levels by refining a top-level statement downward, as in Wirth's stepwise refinement; another builds vocabulary upward until the top level reads as the problem itself, as with Forth word factoring, Lisp macros and Smalltalk protocols. Compare the two, and say when each produces the wrong levels.

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Top-down refinement derives every intermediate level from one decomposition, so the levels freeze the first guess. Bottom-up vocabulary building (Forth words, Lisp macros, Smalltalk protocols) yields reusable levels but risks a private dialect and weak tooling. Mature designs alternate.

open as a page