On Github Szeliga / webinar-contracts-31-07-2015
assert on steroids
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
What sort of contracts do we have out of the box?
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 falseContract 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
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]
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
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 expectThere are 3 ways to define a custom contract
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 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 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
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 gotIO - 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 noticedPros
Cons