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 violationOther 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 violationWe 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:13Define 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: 1It'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 endThis 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