Contract Programming an Elixir approach – Part 1

This series explores the concepts found in Contract Programming and adapts them to the Erlang and BEAM languages, in general, are surrounded by philosophies like “fail fast”, “defensive programming”, and “offensive programming”, and contract programming can be a nice addition. The series is also available on Github.

You will find a lot of unconventional uses of Elixir. There are probably things you would not try in production, however, through the series, we will share some well-established Elixir libraries that already use contracts very well.

Programming by contract?

It is an approach to program verification that relies on the successful execution of statements; not that different from what we do with ExUnit when testing:

defmodule Program do
  def sum_all(numbers), do: Enum.sum(numbers)
end

ExUnit.start(autorun: false)

defmodule ProgramTest do
  use ExUnit.Case

  test "Result is the sum of all numbers" do
    assert Program.sum_all([-10, -5, 0, 5, 10]) == 0
  end

  test "Should be able to process ranges" do
    assert Program.sum_all(0..10) == 55
  end

  test "Passed in parameter should only be a list or range" do
    assert_raise Protocol.UndefinedError,
                 ~s(protocol Enumerable not implemented for "1 2 3" of type BitString),
                 fn -> Program.sum_all("1 2 3") end
  end

  test "All parameters must be of numeric value" do
    assert_raise ArithmeticError, ~s(bad argument in arithmetic expression), fn ->
      Program.sum_all([["1", "2", "3"]])
    end
  end
end

ExUnit.run()
....

Finished in 0.00 seconds (0.00s async, 0.00s sync)
4 tests, 0 failures

In the example above, we’re taking Program.sum_all/1 and verifying its behavior by giving it inputs and matching them with the outputs. In a sense, our function becomes a component that we can only inspect from the outside. Contract programming differs in that our assertions get embedded inside the components of our system. Let’s try to use the assert keyword within the program:

defmodule VerifiedProgram do
  use ExUnit.Case

  def sum_all(numbers) do
    assert is_list(numbers) || is_struct(numbers, Range),
           "Passed in parameter must be a list or range"

    result =
      Enum.reduce(numbers, 0, fn number, accumulator ->
        assert is_number(number), "Element #{inspect(number)} is not a number"
        accumulator + number
      end)

    assert is_number(result), "Result didn't return a number got #{inspect(result)}"
    result
  end
end

Our solution became a bit more verbose, but hopefully, we’re now able to extract the error points through evaluation:

VerifiedProgram.sum_all("1 2 3")
** (ExUnit.AssertionError) 

Passed in parameter must be a list or range
 
VerifiedProgram.sum_all(["1", "2", "3"])
** (ExUnit.AssertionError) 

Element "1" is not a number

This style of verification shifts the focus. Instead of just checking input/output, we’re now explicitly limiting the function reach. When something unexpected happens, we stop the program entirely to try to give a reasonable error.

This is how the concept of “contracts” works in a very basic sense.

How to run tests in contract programming

Having contracts in our codebase doesn’t mean that we can stop testing. We should still write them and maybe even reduce the scope of our checks:

defmodule VerifiedProgramTest do
  use ExUnit.Case

  test "Result is the sum of all numbers" do
    assert VerifiedProgram.sum_all(0..10) == 55
    assert VerifiedProgram.sum_all([-10, -5, 0, 5, 10]) == 0
    assert VerifiedProgram.sum_all([1.11, 2.22, 3.33]) == 6.66
  end
end

ExUnit.run()
.

Finished in 0.00 seconds (0.00s async, 0.00s sync)
1 test, 0 failures
 
By  using our functions in runtime or test-time we can re-align the expectations of our system components if requirements change:
# Now we expect this to work
VerifiedProgram.sum_all("1 2 3 4")
** (ExUnit.AssertionError) 

Passed in parameter must be a list or range

We also need to make the changes required for it to happen. In this case, we need to expand our domain to also include stringified numbers, separated by a space.

Should we add use ExUnit everywhere then?

As seen in the examples above, there’s nothing stopping us from trying the assert keyword. It is a creative way to verify our system components. However, I feel that the failures are designed in such a way as to be used in a test environment, not necessarily at runtime.

From the docs: “In general, a developer will want to use the general assert macro in tests. This macro introspects your code and provides good reporting whenever there is a failure.“Thankfully for us, in Elixir, we have a more primitive mechanism in which we can assert data in an effective way: pattern matching. I would like to explore this more in-depth in the second installment of this contract series.

Main takeaways

  • Contract programming is a technique for program verification that can be applied in Elixir.
  • Similar to testing, we’re not limited to only verifying at test time.
  • We embedded assertions within our code to check for failures.
  • Although not endorsed, we may take advantage of ExUnit to do contracts in Elixir.
  • Other mechanisms native to Erlang and Elixir may be used to achieve similar results.

More info on contracts

About the author

While on a desk Raúl spends time as an elixir programmer, still distilling some thoughts on the topic of “contract programming”; otherwise he’s a recent dad, enjoys simulation games and trying out traditional food. He operates from Tijuana, México from where he was born and lives.

Keep reading

Meet the team: Erik Schön
Meet the Team: Erik Schön thumbnail

Meet the team: Erik Schön

Meet Erik Schön, Managing Director and and Nordics Business Unit Lead at Erlang Solutions. He shares his 2025 highlights and festive traditions.

Optimising for Concurrency: Comparing and contrasting the BEAM and JVM virtual machines

Optimising for Concurrency: Comparing and contrasting the BEAM and JVM virtual machines

Attila Sragli explores the BEAM VM's inner workings, comparing them to the JVM to highlight their importance.

MongooseIM 6.3: Prometheus, CockroachDB and more

MongooseIM 6.3: Prometheus, CockroachDB and more

Pawel Chrząszcz introduces MongooseIM 6.3.0 with Prometheus monitoring and CockroachDB support for greater scalability and flexibility.