How I upgraded my Ruby with Contracts ===================================== Toying with many languages made me discover new approaches and techniques. For example, Haskell taught me about [0] and Erlang/Elixir enlightened me on Pattern-matching[1]. 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[2] by Aditya Bhargava[3] and in this article I will try to present Design by Contracts[4] 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: - Any, when we have no type constraint None, when you need no argument - Or, if our argument can be of different types, for example - Or[Fixnum, Float] Not, if our argument can't be of a certain type, - like Not[nil] Maybe, if our argument is optionnal, example - Maybe[String] 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 the 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. [0] http://learnyouahaskell.com/making-our-own-types-and-typeclasses [1] http://learnyousomeerlang.com/syntax-in-functions [2] https://github.com/egonSchiele/contracts.ruby [3] http://adit.io [4] https : //en.wikipedia.org/wiki/Design_by_contract ------- Last update: 13 July, 2015