View Source Bond (Bond v0.1.0)

Design By Contract for Elixir.

As described on Wikipedia:

Design by contract (DbC), also known as contract programming, programming by contract and design-by-contract programming, is an approach for designing software.

It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.

The term was coined by Bertrand Meyer in connection with his design of the Eiffel programming language and first described in various articles starting in 1986 and the two successive editions (1988, 1997) of his book Object-Oriented Software Construction.

Design by contract has its roots in work on formal verification, formal specification and Hoare logic.

Bond applies the central ideas of contract programming to Elixir and provides support for attaching preconditions and postconditions to function definitions and conditionally evaluating them based on compile-time configuration.

Quick start

use Bond in your module and then define preconditions and postconditions for your functions with the @pre and @post annotations, respectively. For example:

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

Usage

use Bond

When you use Bond, the Bond module will override several Kernel macros in order to support attaching preconditions and postconditions to functions. Specifically:

use Bond will also import the Bond module so that the check/1 and check/2 macros are available for use.

Additionally, the Bond.Predicates module is automatically imported for all preconditions, postconditions, and checks, so that the predicate functions and operators that are defined therein can be used for assertions. Bond.Predicates can be explicitly imported into modules for use outside of assertions.

Assertion syntax

Assertions in Bond are conditional Elixir expressions, optionally associated with a textual label (either an atom or a string). These assertions may appear in @pre or @post expressions, or in calls to check/1 or check/2.

Bond offers considerable flexibility in its assertion syntax; assertions may take any of the following forms:

  • expression - a "bare" expression without any associated label
  • label, expression - an expression preceded by a string or atom label
  • expression, label - an expression followed by a string or atom label
  • label_1: expression_1, label_2: expression_2 - a keyword list with labels as the keys and expressions as the associated values

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.

Subset of Macro.Env struct that excludes fields that, according to the documentation, "are private to Elixir's macro expansion mechanism".

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.

Override Kernel.def/2 to support wrapping with preconditions and postconditions.

Override Kernel.defp/2 to support wrapping with preconditions and postconditions.

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.

@type env() :: %{
  optional(:__struct__) => module(),
  context: Macro.Env.context(),
  context_modules: Macro.Env.context_modules(),
  file: Macro.Env.file(),
  function: Macro.Env.name_arity() | nil,
  line: Macro.Env.line(),
  module: module()
}

Subset of Macro.Env struct that excludes fields that, according to the documentation, "are private to Elixir's macro expansion mechanism".

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]
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.

Link to this macro

def(definition, body)

View Source (macro)

Override Kernel.def/2 to support wrapping with preconditions and postconditions.

Link to this macro

defp(definition, body)

View Source (macro)

Override Kernel.defp/2 to support wrapping with preconditions and postconditions.