View Source Getting Started
This guide walks you through adding Bond to a project, writing your first contract, and the most common patterns you'll encounter.
For full reference material see the Bond module docs.
Installation
Add bond to your dependencies in mix.exs:
def deps do
[
{:bond, "~> 1.0.0-rc.3"}
]
endThen run mix deps.get.
Your first contract
use Bond in any module to enable @pre, @post, @invariant, and
check/1. Add a
precondition before a function definition:
defmodule Calculator do
use Bond
@pre is_number(x)
def square(x), do: x * x
endCalculator.square(2) returns 4 as expected. Calculator.square("two")
raises Bond.PreconditionError with a message that points at the failing
assertion:
** (Bond.PreconditionError) precondition failed for call to Calculator.square/1
| at: lib/calculator.ex:4
| label: nil
| assertion: is_number(x)
| binding: [x: "two"]Adding a postcondition
Postconditions are evaluated after the function body. They have access to
the function's parameters plus a result variable bound to the return
value:
defmodule Account do
use Bond
@pre amount > 0
@post result >= 0
def withdraw(balance, amount), do: balance - amount
endAccount.withdraw(100, 30) returns 70. Account.withdraw(20, 50) raises
Bond.PostconditionError — the function returned a negative balance, which
the postcondition forbids.
Labelled assertions
A single @pre or @post may contain multiple labelled assertions as a
keyword list. Labels appear in error messages so it's easy to identify
which assertion failed:
defmodule Math do
use Bond
@pre numeric_x: is_number(x), non_negative_x: x >= 0
@post float_result: is_float(result),
non_negative_result: result >= 0.0
def sqrt(x), do: :math.sqrt(x)
endLabels can be atoms (when they're valid Elixir identifiers) or strings (for phrases with spaces or punctuation):
@post "result is integer or zero": is_integer(result) or result == 0Predicates and operators
The Bond.Predicates module is automatically imported inside assertion
expressions. Two operators are especially useful in contracts:
~>— logical implication.(p ~> q)means "ifpthenq".<~— pattern match.(pattern <~ expression)ismatch?(pattern, expression).
@post "sqrt of 0 is 0": (x == 0) ~> (result === 0.0)
@post {:ok, _} <~ resultSee Bond.Predicates for the complete list.
old expressions in postconditions
For functions that mutate state, a postcondition often needs to compare
the new state to the old state. The old/1 macro snapshots a value
before the function body runs:
defmodule TurnCounter do
use Bond
# Per-process turn counter stored in the process dictionary. Owned by
# the running process, so the snapshot and the post-check observe the
# same world.
def current_turn, do: Process.get(:turn, 0)
@post incremented: current_turn() == old(current_turn()) + 1
def take_turn do
Process.put(:turn, current_turn() + 1)
:ok
end
endold is only available inside @post. Bond resolves every old(...)
expression at the start of function execution and threads the captured
value into the postcondition.
For state shared across processes — an Agent, a GenServer, an ETS
table — old(...) reads a snapshot that another process can race
against before the post-check runs. See the
Contracts in a Concurrent World guide
for the locking pattern that handles this.
Inline checks
For sanity checks inside a function body, use check/1:
def total(items) do
raw = Enum.sum(items)
check raw >= 0
check total_is_integer: is_integer(raw)
raw
end
checkis for development confidence, not validationDon't use
checkfor input validation or anything else that protects the integrity of your code — it can be compiled out entirely (see below).
Invariants for struct modules
When a module defines a struct, @invariant declarations specify
properties that hold for every value of the struct — checked
automatically on entry and exit of every public function in the
module:
defmodule BoundedStack do
use Bond
defstruct [:items, :capacity]
@invariant size_within_capacity: length(subject.items) <= subject.capacity,
non_negative_capacity: subject.capacity >= 0
def new(capacity), do: %__MODULE__{items: [], capacity: capacity}
def push(%__MODULE__{} = stack, item) do
%{stack | items: [item | stack.items]}
end
endInside an @invariant expression, subject refers to the struct
instance being checked. Bond detects the struct parameter in each
public function's head (%__MODULE__{} = name pattern,
is_struct(_, __MODULE__) guard, or %__MODULE__{...} destructure)
and rebinds subject to it — you write the invariant once and Bond
applies it everywhere.
See the @invariant
section of the moduledoc for head-shape detection, multi-struct
heads, and per-module configuration.
Disabling contracts in production
Bond's four application-config keys — :preconditions, :postconditions,
:invariants, :checks — each accept true, false, or :purge:
# config/prod.exs — strip contracts entirely from the prod build
config :bond,
preconditions: :purge,
postconditions: :purge,
invariants: :purge,
checks: :purgetrue(default) — compiled in, runtime-togglable, evaluated by default.false— compiled in, runtime-togglable, not evaluated by default.:purge— not compiled at all. Zero overhead. No contract docs.
When compiled with true or false, contracts can be flipped at runtime
via Application.put_env(:bond, :preconditions, false | true) — no
recompilation needed. :purge is the only setting with no runtime presence
(the code isn't there).
For finer control, the :overrides config lets you set per-module rules.
See the Bond moduledoc's "Conditional compilation" and "Per-module
overrides" sections for the full story.
Testing contract violations
For testing that a contract IS raised (or that a specific contract isn't),
Bond.Test provides ExUnit helpers:
defmodule MyAppTest do
use ExUnit.Case
use Bond.Test
alias MyApp.Math
test "sqrt rejects negative input" do
assert_precondition_violation(Math.sqrt(-1), label: :non_negative_x)
end
endSee Bond.Test for assert_precondition_violation/2,
assert_postcondition_violation/2, and assert_check_violation/2.
Next steps
- The
Bondmoduledoc has the full reference, including the@invariantsection for module-wide constraints on every instance of a struct, and the Property-based testing section for using contracts as oracles with StreamData. - The Contracts in a Concurrent World guide
covers
old, race conditions, how to design contracts for stateful processes, and how@invariantstrengthens the pure-state-struct pattern. - The FAQ answers common questions: "why contracts when I have ExUnit?", "how does Bond compare to Norm?", "when does Bond check invariants?", "how does Bond compose with StreamData?", and so on.