jvoegele
Bond - Design by Contract for Elixir
Announcing Bond, Design by Contract for Elixir
Bond provides support for contract programming (also known as “Design by Contract”) for Elixir.
The primary goal for Bond is to provide the most feature-complete and thoroughly documented Design by Contract library for Elixir, with a concise and flexible syntax for specifying contracts.
Current and planned features include:
- Function preconditions with
@pre - Function postconditions with
@post - “old” expressions in postconditions
- “check” expressions for arbitrary assertions within a function body
- Predicates (such as
implies?andxor) for use in assertions - Detailed assertion failure reporting
- Incorporation of preconditions and postconditions into @doc for function
- Conditional compilation of contracts per environment
- More detailed assertion failure reporting, including color coding à la
ExUnit - Invariants for structs and/or stateful processes (if possible)
For comparison to other Design by Contract libraries for Elixir, please see the history section of the Bond documentation.
Most Liked
sbuttgereit
Hi–
Interesting project. I saw this in the docs:
Contracts in the form of preconditions and postconditions are part of the public interface for a module in the same way that function signatures and typespecs are.
Is the intention that pre/post conditions replace traditional typespecs when used or should they be considered supplemental? If they are a replacement, do they interact with Dialyzer at all? Also, the set-theoretic types, assuming they land, also offer a manner of contract with functionality ramifications, are there thoughts about how this library would relate to that future?
I’ve not looked much beyond a quick search through the docs. But these are the questions I’d need to understand for adoption.
jvoegele
Hi @sbuttgereit,
Contracts are not intended to replace typespecs or set-theoretic types; rather, they are complementary mechanisms.
Typespecs are evaluated at compile-time or with a static analysis tool such as Dialyzer, whereas contracts as implemented by Bond are evaluated at run-time (although they are attached to functions at compile-time so must be valid Elixir code).
The closest analogue to contracts in native Elixir is guard expressions for functions, which allow for making rudimentary “assertions” about function arguments. Assertions in Bond are much more powerful and are not limited to the pre-defined set of functions that are allowed in guard expressions.
jarlah
fun
<3
@post is_hallo: result == :hallo
def hello do
:world
end
in this case dialyzer is screaming at me already .. but its MUCH easier to actually get the error in your face .. than dialyzer warning about something that SHOULD fail .. but never does. ref this thread where dialyzer warns about something that never fails Dialyzer warning from hell: no_return when calling erl_tar.create - #2 by jarlah
iex(1)> TestBond.hello()
** (Bond.PostconditionError) postcondition failed in TestBond.hello/0
| label: :is_hallo
| assertion: result == :hallo
| binding: [result: :world]
(test_bond 0.1.0) lib/test_bond.ex:6: TestBond.hello/0
iex:1: (file)
iex(1)>
its probably more valuable in the ecto example i described above
Last Post!
jvoegele
Thank you for the feedback, @Asd. I’ve attempted to answer all of your questions below.
Why do you override the
def,defpand@functions? It makes this library inapplicable with other libraries (likedecorators, for example). More traditional approach is to use@on_definitioncallback to collect data for each function and then@before_compileto replace implementations of the functions. This way it is compatible with other solutions.
As mentioned in the history section of the Bond docs the implementation of Bond is based on an older contracts library, ex_contract, from Dariusz Gawdzik. I carried over some of the code from ex_contract more or less directly.
However, you make a good point about compatibility with other libraries, so I will revisit the implementation and try to use @on_definition and @before_compile instead of overriding various Kernel macros.
Why do you use
gen_statem? Module is always compiled in a single process.
I chose to use a finite state machine for keeping track of compile state. I chose gen_statem simply because it is already available with Erlang and I didn’t have to pull in an extra dependency for an FSM.
Bond.FunctionWithContract.function_idwill fail when context atom in variable is notnil(for example code was generated by macro)
I will look into this and commit a fix.
It tries to insert assertions to every clause, which results in code failing to compile with cryptic error when assertion references a variable which is present only in one clause. I think that this is a problem with the design of the solution, not with the implementation. Is it expected?
This is an intentional design decision: to have preconditions and postconditions defined before the first clause of a function, and apply those preconditions and postconditions to every clause of that function.
Not everyone agrees with that design decision, though, as can be witnessed in the discussion on my unmerged pull request for ex_contract.
My opinion, informed by Bertrand Meyer’s writing on Design by Contract, and especially Object-Oriented Software Construction, is that contracts are part of the public interface for a module and its functions, and not simply an implementation aid to the developer of a module. To me this means that contracts should be declarative and specify the observable aspects of a function.
In the case of multi-clause functions, it is an implementation detail to use different names for parameters in the different clauses of the function, and that implementation detail is not even visible to callers of the function unless they look directly at the source code. I.e., it’s not in the docs and not part of the public interface for the function.
That said, I do think I could make improvements to Bond to make it easier to define contracts for multi-clause functions. I’m thinking of adding some sort of bind clause to assertions in Bond, which would allow for pattern matching on arguments and binding arguments to arbitrary names. (Incidentally, this might also help with defining invariants at the module level by providing a mechanism for invariants to reference function parameters and/or pattern match on function results, as discussed with @zachallaun above.)
In the meantime, the best approach is just to use consistent names for parameters in all clauses of a multi-clause function. If the generic name for a parameter is not sufficient in the context of the individual function clause, then an alias could be used. Something like the following, perhaps (untested):
@spec get(URI.t() | String.t()) :: term()
@pre uri_string_or_struct: is_binary(uri) or is_struct(uri, URI),
uri_string_is_parseable: is_binary(uri) ~> URI.parse(uri)
def get(%URI{} = uri) do
http_request(uri)
end
def get(uri = url_string) when is_binary(url_string) do
get(URI.parse(url_string)
end
Note the alias of the uri parameter as url_string in the second clause of the get/1 function, which allows the generic name uri to be used in the contract but still allows for a more meaningful name for that specific function clause.
Predicates work not only with booleans, but their spec states otherwise. Why?
This was simply an oversight on my part. I’ve updated the specs to look like this instead:
@spec xor(as_boolean(term()), as_boolean(term())) :: boolean()
@spec implies?(as_boolean(term()), as_boolean(term())) :: boolean()
This will be in the next release of Bond.
Popular in Announcing
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









