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.
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.
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]
Besides the classics Num, String, Bool, we can use more interesting types like:
And many others that you will discover in the documentation.
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 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.
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.