contracts.ruby – 1. What is it and what it offers – 2. Basic usage



contracts.ruby – 1. What is it and what it offers – 2. Basic usage

0 1


webinar-contracts-31-07-2015

My presentation for the internal webinar on contracts.ruby

On Github Szeliga / webinar-contracts-31-07-2015

contracts.ruby

assert on steroids

http://egonschiele.github.io/contracts.ruby

Presentation plan:

What is it and what it offers? Basic usage Custom contracts Performance Summary

1. What is it and what it offers

  • Provides assertions on arguments and return values
  • Forces the proper usage of an API
  • Introduces method overloading (like in C++ and Java)
  • Adds invariants to objects
* Allows you to explicitly define what type an argument and return value should be * Adds the ability to force a proper API usage, so someone won't try to call methods with wonky arguments and also documents what to expect from a method * Eliminates conditionial branching in methods by overloading them * Enables you to define conditions on an object that must hold in any given point of time

What does it look like?

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

greeting('Szymon') # => Hello, Szymon!
It's defined above a method declaration starting with the Contract keyword Using the hashrocket syntax you define ArgumentType => ReturnType

When argument or return value doesn't match...

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

greeting(1)

... it raises an exception

Contract violation for argument 1 of 1: (ParamContractError)
        Expected: String,
        Actual: 1
        Value guarded in: Object::greeting
        With Contract: String => NilClass
        At: snippet2.rb:5

2. Basic usage

What sort of contracts do we have out of the box?

  • Primitives
  • Boolean operation
  • Arrays and Hashes
  • Special cases
  • Invariants
  • Method overloading

Primitives

Contract Num, Num => Num
def add(x, y)
  x + y
end
add(1, 2) # => 3

Contract Pos, Neg => Num
def add_positive_to_negative(x, y)
  x + y
end
add_positive_to_negative(1, -1) # => 0
add_positive_to_negative(1, 1) # => Contract violation

We can also use literals like 1, "a", {} or nil

There is also a Bool that expects true or false

Boolean operations

Contract Or[Fixnum, Float] => Or[Fixnum, Float]
def double(x)
  2 * x
end
double(1) # => 2
double(1.5) # => 3
double("1") # => Contract violation
Other boolean operations include And, Xor, Not

Arrays and Hashes

Contract HashOf[Symbol, String] => String
def serialize(params)
  return JSON.dump(params)
end
serialize(foo: 'bar') # => {"foo":"bar"}
serialize(foo: 10) # => Contract violation

Contract ArrayOf[Num] => Num
def sum(numbers)
  numbers.reduce(:+)
end
sum(1.upto(10).to_a) # => 55
sum([1, 2, 3, '4']) # => Contract violation
We can also define an expected hash or array that should be passed as an argument. We can define a hash key: Contract, we can also define [Num, String, Neg]

Special cases

  • Any - passes for any argument (no constraint)
  • None - when a method takes no arguments
  • Maybe(Num) - argument can be nil or given contract
  • RespondTo[:foo, :bar] - must respond to the given methods
  • Exactly(Numeric) - won't pass for subclasses
  • Send[:valid?] - the method must return true

Invariants

class Order
  include Contracts, Contracts::Invariants
  attr_accessor :client, :products
  invariant(:client) { client.nil? == false }
  invariant(:products) { Array(products).any? }

  Contract Any, Any => Order
  def initialize(client, product)
    self.client = client
    self.products = products
    self
  end
end
Order.new(nil, nil)
failure_callback': Invariant violation: (InvariantError)
            Expected: client condition to be true
            Actual: false
            Value guarded in: Order::initialize
            At: snippet6.rb:13
Define contraints on an object that must hold at all times Affects only methods with contracts Very good for defining nonanemic domain models

Method overloading

Contract ->(n) { n < 12 } => Hash
def get_ticket(age)
  { ticket_type: :child }
end

Contract ->(n) { n >= 12 } => Hash
def get_ticket(age)
  { ticket_type: :adult }
end

p get_ticket(11) # => {:ticket_type=>:child}
p get_ticket(12) # => {:ticket_type=>:adult}

What is this lambda doing there... ?

Awesome for breaking down conditional branching into several methods and explicitly stating the cases for each condition. Possible usage GuestUser as a special case pattern The second contract could be Num => Hash, but we can explicitly spell out what to expect

3. Custom contracts aka where the fun begins

There are 3 ways to define a custom contract

  • A proc
  • A class with a valid? class method
  • A class with a valid? instance method
As we've seen on the previous slide, we can define our own contracts, there are three ways to do this Instance method is weird, I couldn't get it to work

Procs

is_even = ->(num) { num % 2 == 0 }

Contract is_even => String
def check(num)
  "Yay!"
end

Produces the following error when fails

Contract violation for argument 1 of 1: (ParamContractError)
        Expected: #<proc:0x007fe455849ec8@snippet8.rb:4 (lambda)="">,
        Actual: 1
            </proc:0x007fe455849ec8@snippet8.rb:4>
Good if we need a custom contract for a singe use, gives an unreadable error message

Class with valid? class method

class EvenNumber
  def self.valid?(num)
    num % 2 == 0
  end
end

Contract EvenNumber => String
def check(num)
  "Yay!"
end

Produces the following error when fails

Contract violation for argument 1 of 1: (ParamContractError)
        Expected: EvenNumber,
        Actual: 1
It's more expresive and returns a comprehensive error message

Class with valid? instance method

class Or < CallableClass
  def initialize(*vals)
    @vals = vals
  end

  def valid?(val)
    @vals.any? do |contract|
      res, _ = Contract.valid?(val, contract)
      res
    end
  end
end
This is the definition of the Or contract, I don't quite get this one, and I wasn't able to get it to work as I expected

Performance

http://adit.io/posts/2013-03-04-How-I-Made-My-Ruby-Project-10x-Faster.html

The author has written a blog post about how he was optimizing the library I've also ran the benchmarks provided with the library, and here is what I've got

IO - opening websites and reading their body 100 times

contracts.ruby|master ⇒ RUBYLIB=./lib ruby benchmarks/io.rb
                                     user     system      total        real
testing download                 3.970000   0.360000   4.330000 ( 48.206278)
testing contracts download       3.810000   0.290000   4.100000 ( 48.630163)
The author pointed out to me that the IO benchmark is a real-world example

Invariants - ran 1 mil times

contracts.ruby|master ⇒ RUBYLIB=./lib ruby benchmarks/invariants.rb
                                           user     system      total        real
testing contracts add                  4.850000   0.040000   4.890000 (  4.954304)
testing contracts add with invariants  6.180000   0.030000   6.210000 (  6.232967)
This benchamrk demonstrates how adding invariants to an object, degrades the performance, compared to an object that doesn't check those contraints on it's own, so it's not entirely accurate.

For a real world usage (making requests)

the performance drop is barely visible

In web development most of the performance degradation will come from the internet connection speed, so the performance drop of the library won't be noticed

Summary

Pros

  • Very good in the development process
  • Explicit dependency and results declaration
  • Debugging of elusive bugs in production
  • You can disable contracts through NO_CONTRACTS env var
  • Method overloading is sweet
  • Invariants are also sweet
PROS: AD 1 - allows to easily log and debug methods in development and staging AD 2 - I personally don't like when code is hidden away too much and too much magic happens AD 3 - add a contract on a method and log what sort of violations occur to better understand the situation

Cons

  • No way to change failure handling
  • It does degrade the performance a bit
  • Without a proper failure handling it can't be used in code that is facing the end user
AD 1 - The only possibility is a global block configuration that gets passed the hash with the description AD 2 - altough not that much visible

Thank you for your attention. Any questions?

1
contracts.ruby assert on steroids http://egonschiele.github.io/contracts.ruby