jvoegele

jvoegele

Bond (design by contract for Elixir) - 1.0.0-rc.1 released

A little over 18 months ago I posted the original announcement for Bond — an
early cut of a Design by Contract library for Elixir. The thread surfaced excellent feedback (thanks again to @sbuttgereit, @dimitarvp, @katafrakt, @zachallaun, @jarlah, and @Asd), much of which shaped where the library went next.

Today I’m announcing Bond 1.0.0-rc.1, the first release candidate on the road to 1.0.0.

Install

def deps do
  [{:bond, "~> 1.0.0-rc.1"}]
end

What’s landed since the original announcement

The things people asked for in the original thread — and a lot more.

  • Invariants (@invariant) for struct modules. The subject binding refers to the struct instance under check; Bond detects the struct parameter in each public function’s head and threads subject through automatically. This was the most-requested missing piece in the 2024 thread.

  • A “Contracts in a Concurrent World” guide that takes the dimitarvp / katafrakt / zachallaun discussion further: how old/1 interacts with state owned by other processes, the locking pattern for Agent/GenServer/ETS-backed state, and why pure functions are still the easiest place to apply contracts.

  • Conditional compilation per environment. Each contract kind (:preconditions, :postconditions, :invariants, :checks) takes true, false, or :purge. :purge strips the contract code at compile time — zero runtime overhead, the contract isn’t there. true / false are also runtime-togglable via Application.put_env/3. Per-module overrides are supported.

  • Compatibility with other libraries that override Kernel.@/1. Bond uses the same technique as Norm. Combining them in the same module fails to compile with a clear ambiguity error rather than silently letting one win; the FAQ documents the workaround (split modules). For the broader def/defp compatibility concern @Asd raised, Bond’s compiler architecture is built on @on_definition / @before_compile / @after_compile plus defoverridable at end-of-module — it does not redefine def or defp.

  • Multi-clause contracts clarified and documented. @pre / @post attach to the next def and apply across all its clauses; the per-clause story is intentionally out of scope for 1.0 and called out as a deliberate boundary in the FAQ.

  • Predicate typespecs corrected to use as_boolean(...) instead of boolean(), so Dialyzer no longer rejects truthy non-boolean values that Bond’s predicates accept.

What 1.0 brings on top of that

The RC adds the things you want from a 1.0:

  • A documented and frozen public API surface. Every name covered by the SemVer contract is enumerated in guides/public-api.md — module attributes, macros, operators, predicates, Bond.Test and Bond.PropertyTest helpers, telemetry events, error structs, config keys, types. Internal namespaces (Bond.Compiler.*, Bond.Runtime.*) are explicitly carved out.

  • A SemVer stability promise. guides/stability.md spells out what patch / minor / major actually mean for Bond, what’s explicitly excluded (compile-error message text, generated-code shape, exception message text), and the deprecation policy (minimum one minor with a warning before removal in next major).

  • Published overhead numbers. guides/overhead.md documents both compile-time and runtime cost from a documented reference environment (M3 Max, OTP 27.2, Elixir 1.19.5), with mix run bench/... recipes for re-running on your hardware. Headlines: a :purged contract is free; an enabled @pre adds ~130 ns/call; Bond’s compile-time overhead is ~10 ms per module that uses contracts.

  • :warn_skipped_invariants — a new opt-out compile warning that catches the most common silent-skip footgun (an invariant-declaring module with a public function that neither matches the struct nor returns one). Three suppression scopes are provided: per-function attribute, per-module use option, global config.

  • Compatibility verified across Elixir 1.16–1.19 in CI, with parallel-compile races resolved and a Dialyzer baseline for Bond’s own code.

Why a release candidate, not 1.0.0 final?

The stability guarantees in guides/stability.md lock in at 1.0.0 final, not at RC. The RC window is explicitly for soliciting feedback from the Elixir community on the public API surface before that ink dries. If you find rough edges in the docs, surprising naming choices, missing predicates, awkward composition with other libraries, or anything that smells wrong for a 1.0, please open an issue or reply here — small adjustments to the public surface are still on the table between RC and final.

Bond intentionally stays in its lane: it doesn’t try to replace typespecs, set-theoretic types, or ExUnit. It complements them with runtime-checked, parameter-and-result-and-state assertions, written next to the code they constrain, with errors that tell you exactly what failed and why.

Bug reports and feedback welcome at Issues · jvoegele/bond · GitHub, or in reply here.

Links

Most Liked

jvoegele

jvoegele

Bond 1.0.0-rc.2 is out — the second release candidate on the road to 1.0.0.

def deps do
  [{:bond, "~> 1.0.0-rc.2"}]
end

This RC is small and focused. The headline is improved compatibility: Bond can now share a single module with other libraries that override Kernel.@/1 or wrap your functions — Norm being the obvious one. In rc.1 the answer was “split them into separate modules”; now they can live together.

What changed since rc.1

  • Same-module coexistence, part 1 — use Bond, at_annotations: false. This opts a module out of Bond’s Kernel.@/1 override, so another library (e.g. Norm’s @contract) can own @ in that module. You then write contracts as the fully-qualified Bond.pre/1, Bond.post/1, and Bond.invariant/1 calls (check/1 stays as-is). The bare pre/post/invariant macros are never imported in either mode, so they can’t collide with your function names. The FAQ has a worked “Bond + Norm in the same module” example.

  • Same-module coexistence, part 2 — tolerance of externally-generated override clauses. Libraries that wrap a function by making it defoverridable and redefining it (Norm’s @contract, anything built on the decorator library, …) inject a clause that Bond’s @on_definition hook observes. Bond used to reject that as the function being defined twice. It now recognizes those generated wrappers, ignores them for its bookkeeping, and still wraps the function as a whole — composing with the other library’s wrapper via super.

  • Fix: the ElixirLS / IEx already_started crash. If you were dogfooding rc.1 in an editor, you may have hit a (MatchError) … {:error, {:already_started, #PID<…>}} on essentially every edit of a use Bond module. An aborted compile (a transient syntax error while you’re typing) could leave Bond’s per-module compile-state process registered in the long-lived BEAM, and the next compile crashed trying to start it again. Bond now discards the stale process and starts fresh. One-shot mix compile was never affected — this only bit editor/IEx sessions that keep the VM alive across recompiles. (Found by dogfooding Bond in a real app; exactly the kind of rough edge the RC window is for.)

One breaking change to be aware of

If you tried out rc.1 and used the positional @pre / @post label forms, they’ve been removed:

# Was (removed in rc.2):
@pre :positive, x > 0
@post result >= 0, "non-negative result"

# Now — labels are a keyword key (quote it for spaces/punctuation):
@pre positive: x > 0
@post "non-negative result": result >= 0

@pre expr (bare) and @pre label: expr (keyword) are the two remaining forms; the qualified Bond.pre/Bond.post calls are likewise keyword-only. The removed shapes raise a CompileError with the migration message, so you won’t get a silent change in behavior. This finishes unifying contract labelling on the single keyword-list form that check/2 already moved to in 0.16.0 — RC is the right time to settle it before the public surface freezes at 1.0.0 final.

Still an RC

Everything from the rc.1 announcement still stands — the frozen public API surface, the SemVer stability promise, the published overhead numbers, and 1.16–1.19 CI coverage. The stability guarantees lock in at 1.0.0 final, not at RC, so feedback on naming, composition, docs, or anything that smells wrong for a 1.0 is still very welcome here or at Issues · jvoegele/bond · GitHub.

arcanemachine

arcanemachine

This sounds like a very interesting library, and it totally flew under the radar for me.

I shall now put it on my teetering pile of “things I really want to use”. Seems like it could be useful enough to stay near the top…

Where Next?

Popular in News & Updates Top

mobileoverlord
New versions of the Kiosk systems are out. These systems update official Nerves systems with a local web browser for rendering user inter...
New
jjcarstens
NervesPack 0.4.0 is a minor bump with a fairly big change. This update drops :nerves_firmware_ssh in favor of the newer and more focused...
New
jjcarstens
Do you like Hacktoberfest? Also enjoy working with Nerves and want to contribute? Fantastic! :tada: :beers: Here are some potential sta...
New
bartblast
Hey there! There’s a new kid on the block! :smiley: What is it? Full stack isomorphic Elixir web framework that can be used on top of P...
New
hugobarauna
Welcome to the second day of Livebook Launch Week! :tada: Today we will discuss all the new Machine Learning capabilities in Elixir and ...
New
hugobarauna
This post introduces the new data features in Livebook 0.9: fast data exploration through integration with Explorer, interactive data tab...
New
zachdaniel
Hey folks! In order to give the new AshSqlite a trial run and make sure its got some real usage, AshHq’s multi-package search has been re...
New
zachdaniel
Hey folks, made some recent performance improvements to spark, the tool underlying all of our DSLs. GitHub - ash-project/spark: Tooling f...
New
bartblast
Hey Elixir community! :waving_hand: First, I owe you all an apology. There’s a running joke among my friends that Hologram is like nucle...
New
bartblast
I’m excited to announce Hologram v0.5.0, a major evolution of the full-stack Elixir web framework! This release brings massive performanc...
New

Other popular topics Top

JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43757 214
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement