View Source Reusable Contracts
Sometimes the same agreement governs several functions. A handful of operations
all require a positive amount that fits within an account's balance; a family
of functions all promise a non-negative result. Restating the same @pre/@post
on each one is repetitive and drifts out of sync.
A named contract captures a bundle of @pre/@post once, under a name, and
applies it to as many functions as you like — in the same module or across
modules:
defmodule Money do
use Bond
defcontract withdrawal(account, amount) do
@pre positive: amount > 0
@pre sufficient: amount <= account.balance
@post non_negative: result.balance >= 0
end
end
defmodule Account do
use Bond
@apply_contract {Money, :withdrawal}
def withdraw(acct, amt), do: %{acct | balance: acct.balance - amt}
endAccount.withdraw/2 now enforces all three assertions, and a violation names the
contract it came from:
** (Bond.PreconditionError) precondition (from contract Money.withdrawal) failed
for call to Account.withdraw/2A named contract is, in effect, an inherited contract whose source is a definition rather than a behaviour callback — it shares the canonical argument names and positional rebinding model described in the Contract Inheritance guide.
Defining a contract
defcontract name(arg1, arg2, …) do … end declares a contract. The head is a
canonical signature: its parameter list supplies the names the contract's
expressions reference and the order they bind in. The body may contain only
@pre/@post (bare or labelled, exactly as under use Bond), and each
expression may reference only the declared arguments — plus result (and
old/1) in a @post:
defcontract transfer(from, to, amount) do
@pre enough: amount <= from.balance
@pre distinct: from.id != to.id
@post conserved: result.from.balance + result.to.balance == old(from.balance) + old(to.balance)
endA reference to a name the contract does not declare is a compile error that points at the offending assertion.
Overloading by arity
Contracts are identified by {name, arity}, so the same name at different
arities are distinct contracts:
defcontract positive(x) do
@pre x > 0
end
defcontract positive(x, floor) do
@pre x > floor
endThere is nothing more to do at the application site — the arity of the function you apply to selects the overload.
Applying a contract
@apply_contract immediately precedes the function it constrains, like @pre:
@apply_contract :name— a contract defined in the same module.@apply_contract {Module, :name}— a contract defined in another module, read through that module's generated reflection at compile time.
defmodule Ledger do
use Bond
@apply_contract {Money, :withdrawal} # arity 2 → Money.withdrawal/2
def withdraw(account, amount), do: debit(account, amount)
@apply_contract :audited # a contract defined in this module
def post(entry), do: append(entry)
defcontract audited(entry) do
@pre has_actor: entry.actor != nil
end
endThe applying function's parameters are rebound to the contract's canonical names
positionally, so the function is free to name them differently — withdraw(acct, amt) works against withdrawal(account, amount) just as a behaviour
implementation's parameters rebind to its callback's names. The contract's
declared arity must match the function's arity; a mismatch is a compile error
that lists the contract's available arities.
How failures are attributed
A failing assertion from an applied contract names its source. A cross-module
contract reads (from contract Money.withdrawal); a contract defined in the
failing call's own module abbreviates to (from contract :withdrawal). The
originating {module, name} is also available programmatically as the
:source_contract field on the error struct, and in the
[:bond, :assertion, :failure] telemetry metadata.
Scope and non-goals (v1)
Named contracts in this version deliberately mirror inheriting a single behaviour contract verbatim. The following are reported as clear compile errors, and are candidates for a future version rather than silent partial behaviour:
- One contract per function. A function applies a single named contract.
Two
@apply_contractannotations (or a list) on one function is an error. - No own
@pre/@postalongside an applied contract. An applied contract is enforced as written; for a function-specific assertion, useBond.check/1in the body, or add the assertion to the contract. (This is the same stance inheritance takes, for the same reason: the lifted contract evaluates in the contract's canonical-name vocabulary.) - No combining with behaviour/protocol inheritance on the same function.
- No refinement of an applied contract with
@pre_weaken/@post_strengthen.
@apply_contract relies on Bond's @ syntax, so it is unavailable under
use Bond, at_annotations: false. defcontract itself works in either mode.
Named contracts vs. a hand-rolled macro
You can already share contract logic by writing a macro that emits @pre/@post
(see the FAQ entry on macro-emitted contracts). defcontract is the first-class
form of that pattern: it is discoverable, validates references at definition time,
binds positionally so the contract is decoupled from any one function's parameter
names, and attributes failures to the contract by name. Reach for a macro only
when you need to compute assertions dynamically; reach for defcontract to share
a fixed agreement.