Contract Programming an Elixir approach – Part 1

Check out our findings as we explore how we adapted Contract Programming to Elixir language.

6 min read

This series explores the concepts found in Contract Programming and adapts them to the Elixir language. 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)

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

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

  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

  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"]])

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

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

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

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

How ChatGPT improved my Elixir code. Some hacks are included.

Ever wondered the impact ChatGPT can have on your Elixir code? Oleg Tarasenko has put the AI tool to the test and shares his interesting results.

Here’s why you should consider investing in RabbitMQ during a recession

In times of economic uncertainty, making wise tech investments into systems such as Rabbit MQ can be a valuable asset to your business in the long-term.


Entendiendo procesos y concurrencia

Bienvenidos al segundo capítulo de la serie “Elixir, 7 pasos para iniciar tu viaje”.  En el primer capítulo hablamos sobre la máquina virtual de Erlang, la BEAM, y las características