F5.04 · part 2 of 3
Assertions in Elixir
Elixir has no require or ensure keyword, so a
contract is written in the language’s ordinary idioms — and each idiom fits a different term.
Guards and pattern matching in a function head express preconditions on the shape of arguments.
A with chain composes several checks and short-circuits on the first
failure. Tagged tuples carry expected failures back to the caller. And raise — or a match that fails — crashes on a broken invariant, which is a
bug rather than a bad request. Choosing the right one is choosing how a violation should be handled.
Four idioms, four kinds of check. Select one to see which part of a contract it expresses.
Four idioms
The split that matters is between expected failures and impossible states. A not-found or an
already-enrolled is expected: it flows back as {:error, reason} and the caller
decides what to do. A negative progress value is impossible if the invariant holds, so reaching it is a defect
— it should crash, not be handled. Select an idiom.
The contract toolkit · select an idiom
idiom: with
expresses: composed preconditions
There is no contract keyword because there does not need to be. Guards, with,
tuples, and raise already say everything — you only have to pick the one that fits
the failure.
In code
The whole contract, in one function. The head’s binary patterns are the shape precondition; the
with composes the runtime checks and returns tagged tuples for the expected failures;
and the match in build/2 asserts the invariant, crashing if it is ever false.
# guard via pattern: a malformed id never matches — the shape precondition def enroll(<<"USR", _::binary>> = user_id, <<"CRS", _::binary>> = course_id) do with :ok <- ensure_not_enrolled(user_id, course_id), # expected failure -> {:error, _} enrollment = build(user_id, course_id), :ok <- Portal.Store.put(enrollment) do {:ok, enrollment} end end def enroll(_, _), do: {:error, :bad_reference} # precondition unmet, the caller's fault defp build(u, c) do e = %Enrollment{id: Portal.ID.new("ENR"), user_id: u, course_id: c} true = e.progress in 0..100 # invariant assertion — raises MatchError if ever false e end
match the idiom to the failure
Shape → guard; expected → tuple; composition → with; impossible → raise.
one honest command
Precondition, expected errors, and invariant, all visible in enroll.
Next: failing fast — why every one of these checks belongs at the boundary, before any state changes.
References
Primary sources for this lesson, and where it connects in the course.
Sources
- Bertrand Meyer — Applying Design by Contract — preconditions, postconditions, invariants.
- Eiffel — Design by Contract and assertions — the contract metaphor, in depth.
- Elixir — Patterns and guards — guards as executable preconditions.