julienblanchard.com $home

How I upgraded my Ruby with Contracts

2015-07-13 17:18:00

Toying with many languages made me discover new approaches and techniques. For example, Haskell taught me about Types and Erlang/Elixir enlightened me on Pattern-matching.

Professionally I mainly code with Ruby and I dreamed of having an advanced type system and some pattern-matching. I discovered this brilliant gem Contracts.ruby by Aditya Bhargava and in this article I will try to present Design by Contracts through the use of this gem.

What is a contract?

A contract ensures what kind of input a method expects (pre-condition), what it outputs (post-condition). It will define how our method behaves but also check its behavior.

The gem Contracts.ruby allows us to decorate our methods with code that will check that the inputs and outputs correspond to what the contract specifies. Of course, one is not obliged to annotate each method but I think that specifying a contract for your public API can only be beneficial.

A first example

Contract Num, Num => Num
def add(a, b)
  a + b
end

The contract of my method is Contract Num Num => Num, meaning that the add method takes two numbers as input and returns a number. Simple, right?

You will object that ok, it’s documentation, I could just add a comment. But since this is a contract, the Contracts.ruby gem will help ensure that it is respected.

require 'contracts'

class Foo
    include Contracts

    Contract Num, Num => Num
    def self.add(a, b)
        a + b
    end
end

Foo.add(1, 2) obviously returns 3 but Foo.add(1, ‘2’) will return:

ParamContractError: Contract violation for argument 2 of 2:
        Expected: Num,
        Actual: "2"
        Value guarded in: Foo::add
        With Contract: Num, Num => Num

The error highlights that the contract of the method add wasn’t respected because the second parameter we sent, ‘2’, isn’t of the type Num.

Note that you must always specify the type of the value returned even if the method does not return anything:

Contract String => nil
def hello(name)
    puts "hello, #{name}!"
end

If our method returns many values, its signature will be

Contract Num => [Num, Num]

Types at our disposal

Besides the classics Num, String, Bool, we can use more interesting types like:

And many others that you will discover in the documentation.

Advanced Types Contracts

We can use contracts with advanced types like lists:

Contract ArrayOf[Num] => Num
def multiply(vals)
    vals.reduce(:*)
end

The contract of multiply method indicates that it wants a list of values of the type Num. Therefore multiply([2, 4, 16]) is valid but multiply([2, 4, ‘foo’]) is not.

Hashes:

Contract ({ nom: String, age: Num, ville: String }) => nil

Methods:

Contract ArrayOf[Any], Proc => ArrayOf[Any]

If you use Ruby 2.x keyword arguments, the contract will look like:

Contract KeywordArgs[foo: String, bar: Num] => String

We can also define our own contracts with synonyms:

Token = String
Client = Or[Hash, nil]

Contract Token => Client
def authenticate(token)

Our authenticate method is thus more clear as to what it expects and what it does. A Token of type String is desired as input and it returns a Client which can be a Hash or nothing (nil).

Pattern-matching

Pattern-matching will, for a given value, test if it matches a pattern or not. If this is the case an action is triggered. It’s a bit like Java method overloading. One could imagine it as a giant switch case but much more elegant.

A simple example with calculation (not effective at all) of the Fibonacci sequence:

Contract 0 => 0
def fib(n)
    0
end

Contract 1 => 1
def fib(n)
    1
end

Contract Num => Num
def fib(n)
    fib(n-1) + fib(n-2)
end

For a given argument, each method will be tried in order. The first method that does not generate an error will be used.

A little more real-world™ example, the management of an HTTP response based on its status code:

Contract 200, JsonString => JsonString
def handle_response(status, response)
    transform_response(response)
end

Contract Num, JsonString => JsonString
def handle_response(status, response)
    response
end

If the HTTP response code is 200 it will transform the answer, otherwise we will simply return the response.

Conclusion

There are many benefits. Contracts allow us to have greater consistency in our inputs and outputs. The flow of data in our system is clearer. And most the type errors of our system can be detected and fixed quickly. Additionally it’s easier to understand what a method does, needs and returns. It also provides some kind of documentation that would always be up to date :p.

I think we can thus save a lot of unit tests on the type of the argument(s) received by a method and focus on what it produces with this contract system. Refactoring also becomes a lot easier with this kind of safety.

I hope this article has convinced you of the value of contracts and pattern-matching in your daily Ruby and also gave you the urge to explore other languages ​​with other paradigms.