Let's make a deal: Handshake, a contract system for Ruby

Handshake is an informal design by contract system written in pure Ruby. It’s designed to offer simple, flexible semantics for improving the clarity and robustness of your code. It supports three basic kinds of checking: invariants, method pre/post conditions, and method arguments. This is a unit-tests-pass-but-you-probably-shouldn’t-use-it-for-anything-important 0.1 release.

A couple of clarifications: Pre- and post-conditions, and argument contracts, all accept the name of the method you’d like them to apply to as an optional first argument. If you omit it, they’ll be applied to the next method you define.

Also, anything is a clause that always returns true.

Updated 5/9/07: I’ve modified some of the content below to reflect changes to Handshake’s interface. Handshake is also now obtainable as a gem (gem install handshake).

Argument Contracts

Argument contracts are insanely handy. They allow you to specify conditions for the arguments and return values of methods simply and easily. Here’s an example:

class AcceptsString
  include Handshake
  contract [ String, Fixnum ] => anything
  def initialize(str, num); ...; end
end

Wait a second, I hear you say. That’s not exciting at all! I just left Java behind forever. Quasi-statically-typed languages are for suckers.

Here’s the rub: Handshake accepts as an argument clause any object that implements the === method. Why? Because it’s an incredibly convenient “is equivalent to” form of comparison. It’s what makes Ruby’s case statement so handy. Try this awkwardly contrived example on for size:

# Rate a website on a scale from 1 to 5.
class WebsiteRatingSystem
  include Handshake
  
  contract [ String, /^http:\/\// ] => anything
  def initialize(name, url)
    @name, @url, @ratings = name, url, []
  end

  contract 1..5 => anything
  def add_rating(n); @ratings << n; end

  contract nil =&gt; 1..5
  def average_rating
    ratings_sum = @ratings.inject(0.0) { |sum, n| sum + n }
    ratings_sum / @ratings.length
  end

  contract [ String, [ Integer ], Block([String, Integer] =&gt; anything) ] =&gt; Hash
  def generate_report(name, *metrics, &block)
    ..
  end
end

But Brian, you say, in the imaginary conversation I am conducting with you in my head. Brian, I like being able to give it a range, or a regexp. That seems downright neato. But man, what a hassle to have to craft my own ===-implementing object for every single clause.

As it turns out, I have made this process entirely hassle-free for you.

contract assert("equals foo") { |arg| arg == "foo" } =&gt; anything
contract any?(String, Integer) =&gt; anything
contract [ all?( hash_of?(Symbol, String), 
                 hash_with_keys(:foo, :bar, :baz) ] =&gt; anything
contract nonzero? =&gt; hash_contract(:count =&gt; Integer, :description =&gt; String)
contract responds_to?(:each, :map, :length) =&gt; anything
contract nil =&gt; String

And they said that duck typing and contracts didn’t mix! Good day, sir and/or madam.

Except for also

I’ve defined some accessors, just for you.

class HoldsSomeThings
  include Handshake
  contract_accessor :foo =&gt; String, :bar =&gt; Integer
  contract_reader ...
  contract_writer ... # You get the idea.
end

Invariants

Also handy are invariants, which are conditions that should always hold. They’re checked after your object is constructed and before and after every method invocation. They have access to local methods and instance variables. They are all up in your class, policing your object.

class NonEmptyArray &lt; Array
  include Handshake
  invariant { not empty? }
end

Pre- and post-conditions

Sometimes you need a broader, more holistic approach to method checking. These conditions, if specified, are executed before and after (respectively) the named method and have access to its arguments, return value(s), and any local methods and instance variables. These work very much like you’d expect them to:

class HoldOnJustAMinute
  include Handshake

  before("halt in the name of the law") do |arg|
    assert @justice_dispensable
    assert arg.law_abiding?
  end; def do_something_potentially_damaging(arg)
    ...
  end
end

Caveats

  • It isn’t underpinned by a formal calculus (although it’s modeled very loosely on the PLT Scheme contract library and on the Eiffel contract system). I’m more or less cool with that; Ruby’s appeal to me comes not least from its loose informalism. But the professor under whose grudging tutelage I developed this would like you to know that until it does it’s not good for anything whatsoever, and whatever benefits you think you might be deriving from such a seemingly useful library are just a figment of your diseased imagination. He informs me that he’s okay with that bit, but the half-bakedness of the implementation bothers him more. See below.
  • It doesn’t yet have a proper blame system. See abovebelow re: professor.
  • It handles inheritance only trivially: a subclass will inherit the contracts of its superclass but will not prevent you from overriding them. This means that it allows you to write contracts that fail the Liskov substitution principle. I hope to fix this soon, but in the meantime, don’t let it keep you awake at night.
  • There’s a lot of metaclass magic going on under the covers, and you shouldn’t use it in contexts with a lot of other metaclass magic (myrailsapp/lib is probably fine; myrailsapp/app is not.)
  • There’s no rdoc yet (although the code itself is fairly well-documented). Fixed. See handshake.rubyforge.org.
  • You can’t define contracts in modules.
  • And it will probably impact performance, although I haven’t conducted any performance testing.

Update: The inimitable Matthias Felleisen responds via email:

1. I never insist on a formal calculus. That’s what researchers are for. It’s a tool for exploring the ideas, communicating them in concise form, and proving basic fundamental theorems about such frameworks.

2. I do think that half-baked or quarter-baked implementations of good ideas are bad for the construction of quality software in the long term. They just propagate the silly idea that programming is shallow and anyone can do it, trained or not. One day we’ll have the equivalent of bridges come down on us in software and then the regulators will be worse than anyone can imagine now.

3. I also believe that without a properly design blame system, a contract framework provides only a 1/3 of the value that it can. That’s sad. It’s like buying a model T when we know how to build Porsches.

In my defense, this is at least two-thirds more-baked than any other Ruby contract system (5/9/07: except flgr’s ruby-contract. But I still like mine better). Since no Ruby contract system that I’ve seen is more than 15% baked, I guess that would make this… yes, about a quarter baked. It’s also a lot of fun to work on.

Finally

Find the project online at rubyforge.org/projects/handshake (MIT license), the RDoc at handshake.rubyforge.org, and the gem handshake, and please do let me know if you end up using it, and how.

Handshake vs. ruby-contract

Meet RubySearch!