View Source Bond (Bond v0.10.0)

Design By Contract for Elixir.

Bond lets you attach preconditions and postconditions to your functions and verify them at runtime. A contract is a plain Elixir boolean expression with optional labels:

defmodule Account do
  use Bond

  @pre positive_amount: amount > 0
  @post non_negative_balance: result >= 0
  def withdraw(balance, amount), do: balance - amount
end

When a contract fails, Bond raises a Bond.PreconditionError or Bond.PostconditionError with the failing assertion's label, expression, location, and the local binding — telling you exactly what went wrong and where.

Bond is an implementation of the Design By Contract methodology (also called programming by contract), introduced by Bertrand Meyer with the Eiffel language. See the About guide for background.

Usage

use Bond in any module to enable the @pre, @post, check/1, and check/2 annotations. Contracts may use any Elixir expression that returns a boolean (or a truthy value).

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,
        "sqrt of 0 is 0": (x == 0) ~> (result === 0.0),
        "sqrt of 1 is 1": (x == 1) ~> (result === 1.0),
        "x > 1 implies result smaller than x": (x > 1) ~> (result < x)
  def sqrt(x), do: :math.sqrt(x)
end

@pre and @post accept one or more labelled assertions. Preconditions have access to the function's parameters; postconditions also have access to the result variable (bound to the function's return value) and old(...) expressions that snapshot a value before the function runs (see old expressions below).

Bond also provides a check/1,2 macro for placing assertions at arbitrary points inside a function body — useful for sanity checks during development. check honours the :bond, :checks config (see Conditional compilation) and is safe to disable in production builds.

When to use check

Don't use check for input validation, validating data from external systems, or anything else that protects the integrity of your code. If the check were removed (or compiled out via config), the system must still behave correctly. Use ordinary control flow for that.

use Bond

use Bond overrides Kernel.@/1 so that @pre, @post, and @doc annotations can be intercepted and recorded, and installs @on_definition, @before_compile, and @after_compile compiler hooks that wrap functions with contracts via defoverridable at the end of module compilation. Your defs and defps are otherwise left alone.

use Bond also imports the Bond module so the check/1 and check/2 macros are available, and imports Bond.Predicates so the predicate functions and operators defined there (such as ~> and |||) can be used in assertions. Bond.Predicates can be explicitly imported elsewhere if you want the operators outside of contract expressions.

Assertion syntax

An assertion is a boolean (or truthy) Elixir expression, optionally paired with a label. Labels are atoms or strings; they appear in error messages and generated documentation.

The recommended form is the keyword list, even for a single assertion:

@pre positive_x: x > 0
@post non_decreasing: result >= old(result)
@pre numeric_x: is_number(x), non_negative_x: x >= 0

For a bare assertion where a label adds no information, the bare form is also fine:

@pre is_number(x)
@post is_float(result)

For symmetry with ExUnit's assert(value, message) and assert message, value patterns, the check/2 macro also accepts a label before or after the expression:

check is_number(x)
check x_is_number: is_number(x)
check "x is a number", is_number(x)
check is_number(x), "x is a number"

Bond also provides the Bond.Predicates module with operators that are often useful in assertions — notably ~> (logical implication) and <~ (pattern match). Bond.Predicates is automatically imported into assertion expressions, so you can use these operators directly:

@post (x == 0) ~> (result == 0.0)
@post {:ok, _} <~ result

See Bond.Predicates for the full list.

old expressions

old expressions allow postconditions to access the value of any arbitrary expression prior to execution of the function body. Postconditions are "pre-compiled" in such a way that any old expressions that appear in assertions are resolved to the value that they had at the start of function execution.

While this facility is not particularly relevant for purely functional code, it can be useful for stateful components of an application.

For example, imagine a simple, stateful Counter module that uses an Agent to store the current count (some Agent code omitted for brevity):

defmodule Counter do
  use Bond

  def get_count(agent) do
    Agent.get(agent, & &1)
  end

  @post count_incremented_by_1: get_count(agent) == old(get_count(agent)) + 1
  def increment_count(agent) do
    Agent.update(agent, &(&1 + 1))
  end
end

Notice how the old expression captures the value of get_count/1 prior to execution of the function, and this value is used to verify that the value of get_count/1 has been updated as expected.

Note, however, that there is a potential race condition in the above code. Since Agents are inherently concurrent, it is possible that another call to increment_count/1 is interleaved between execution of the function body and the call to get_count/1 that appears in the postcondition. In this scenario the postcondition would fail because the new value of get_count/1 would be at least 2 greater than the old value captured in the postcondition, rather than exactly 1 greater as specified in the count_incremented_by_1 assertion.

As a first attempt to alleviate this race condition we can update the increment_count/1 function so that it returns the updated count as its result and use that result in the postcondition directly:

  @post returns_updated_count: result == old(get_count(agent)) + 1
  def increment_count(agent) do
    Agent.get_and_update(agent, fn count ->
      new_count = count + 1
      {new_count, new_count}
    end)
  end

In this version we utilize Agent.get_and_update/3 to update the counter and return the updated counter value in one operation. The new counter value is the result of the function which can be used in postconditions. The returns_updated_count assertion compares this result to the old value of get_count/1 to ensure that it was incremented by exactly 1.

However, as you may have noticed, it is still possible for another call to increment_count/1 to be interleaved between the call to get_count/1 in the old expression of the postcondition and the call to Agent.get_and_update/3 in the function body. Alas, there is no way to "lock" an Agent over multiple operations to ensure that there are no concurrent updates to the Agent state. Therefore, our only choice is to soften the guarantee made by our postcondition:

  @post count_increased: get_count(agent) > old(get_count(agent))
  def increment_count(agent) do
    Agent.update(agent, &(&1 + 1))
  end

The count_increased assertion in the postcondition now guarantees only that the new value of get_count/1 is strictly greater than the old value. This assertion always holds true regardless of the number of concurrent state updates to the counter.

Although this assertion is not as strong as the count_incremented_by_1 assertion in the original version, it is the strongest we can provide given the possibility of concurrent state updates.

Future versions of Bond may provide stronger support for stateful contracts in the form of invariants for structs and/or stateful processes, although this is still a subject of research.

Documenting contracts

Contracts are part of a module's public interface, in the same way that function signatures and typespecs are. Bond treats them that way: every function with a contract gets a #### Preconditions and/or #### Postconditions section appended to its @doc, formatted as the original assertion source. The sections appear in ex_doc output and in editors that show function docs on hover (VS Code, Vim's K, etc.).

Auto-generated contract sections appear whether or not you wrote a @doc yourself — Bond synthesises one when needed.

Conditional compilation and docs

When a function has all of its contracts compile-disabled (see Conditional compilation), the function runs with zero contract overhead and its auto-generated contract sections are also suppressed. If you want the contract documentation visible in production builds, leave at least one of :preconditions or :postconditions enabled, or write an explicit @doc for the function.

Conditional compilation

Contracts are evaluated on every call by default. For hot paths or production builds you can compile contracts out entirely via three application-config keys, read at compile time:

# config/prod.exs
config :bond,
  preconditions: false,
  postconditions: false,
  checks: false

Each key defaults to true. When set to false:

  • :preconditions@pre evaluation is omitted from generated override clauses, and the #### Preconditions section is omitted from the auto-generated docs.
  • :postconditions — same for @post.
  • :checks — every check/1,2 macro call expands to :ok and the wrapped expression is not evaluated. Don't put side effects inside check.

When both :preconditions and :postconditions are disabled for a function, Bond emits no override clause at all. The function runs exactly as you wrote it, with no per-call overhead.

Because Application.compile_env/3 is used to read the config, changing these values requires recompilation (mix deps.compile bond --force, or in practice MIX_ENV=prod mix compile --force). The Elixir compiler tracks the dependency for you in normal incremental builds.

A typical pattern: enable contracts in dev and test, disable in prod.

# config/config.exs
import Config

# Default: everything enabled.
# Specific environments may override below.

# config/prod.exs
import Config

config :bond,
  preconditions: false,
  postconditions: false,
  checks: false

Summary

Types

Type to represent a compile-time quoted assertion expression, which must be a valid Elixir expression that, when unquoted, evaluates to a boolean/0 or as_boolean/1 value.

Type to represent a label for an assertion, which must be a compile-time atom or string.

Functions

Override Kernel.@/1 to support @pre and @post annotations.

Check an assertion or a keyword list of assertions for validity.

Check a single labelled assertion for validity.

Types

Link to this type

assertion_expression()

View Source
@type assertion_expression() :: {atom(), Macro.metadata(), list()}

Type to represent a compile-time quoted assertion expression, which must be a valid Elixir expression that, when unquoted, evaluates to a boolean/0 or as_boolean/1 value.

@type assertion_label() :: String.t() | atom()

Type to represent a label for an assertion, which must be a compile-time atom or string.

Functions

Override Kernel.@/1 to support @pre and @post annotations.

See the Bond module docs for the syntax of @pre and @post annotations.

Link to this macro

check(assertion_or_list_of_assertions)

View Source (macro)
@spec check(assertion_expression()) :: as_boolean(any())
@spec check(Keyword.t(assertion_expression())) :: [as_boolean(any())]

Check an assertion or a keyword list of assertions for validity.

Returns the result(s) of the assertion(s) if satisfied, or raises a Bond.CheckError exception if any assertions are not satisfied.

Examples

iex> check 1 == 1.0
true
iex> check 1 == 1.0, "integer 1 is equal to float 1.0"
true
iex> check "integer 1 is equal to float 1.0", 1 == 1.0
true
iex> check tautology: 1 == 1
[true]
iex> check "1 is 1": 1 == 1, "2 is 2": 2 == 2
[true, true]

Conditional compilation

check honours the :bond, :checks application config. When set to false, every check call in modules that use Bond expands to :ok and the wrapped expression is not evaluated at all. Don't rely on side effects inside check expressions, and don't rely on the return value of check if your build may have checks disabled.

Link to this macro

check(label_or_expression, expression_or_label)

View Source (macro)

Check a single labelled assertion for validity.

See check/1 for details and examples.