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
, theBond
module will override severalKernel
macros in order to support attaching preconditions and postconditions to functions. Specifically:
Kernel.@/1
is overridden byBond.@/1
Kernel.def/2
is overridden byBond.def/2
Kernel.defp/2
is overridden byBond.defp/2
use Bond
will also import theBond
module so that thecheck/1
andcheck/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 labellabel, expression
- an expression preceded by a string or atom labelexpression, label
- an expression followed by a string or atom labellabel_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.
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
@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 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.
@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]
@spec check(assertion_label(), assertion_expression()) :: as_boolean(any())
@spec check(assertion_expression(), assertion_label()) :: as_boolean(any())
Check a single labelled assertion for validity.
See check/1
for details and examples.
Override Kernel.def/2
to support wrapping with preconditions and postconditions.
Override Kernel.defp/2
to support wrapping with preconditions and postconditions.