Trust, but verify

Self-confident code does not ask questions about the data given to it. It enforces validity, asserts that its expectations are met, or ignores data which isn’t up to its standards. Previously we’ve looked at some methods for enforcement, using fetch() and Array(). Today I want to talk about the second tactic, assertions.

Assertions and contracts get comparatively little attention in the Ruby world. I’m not sure why that is. Some might say that pervasive unit-testing has rendered contract-checking redundant or less important than it is in less test-infected programming communities. Or even that TDD and assertions represent opposing philosophies of how to address correctness in software. I disagree; I think that the two techniques are complementary.

The pragmatic essence of code assertions is the idea of failing fast. Most violations of a contract will eventually result in an error; the question is, how far away from the original contract violation will the exception be raised? And will the expectation which wasn’t satisfied be clearly marked?

The advantages of assertions are more than theoretical. Studies of software projects have shown that projects which employ assertions tend to have fewer defects.

Assertions need not use an elaborate Design by Contract framework (although such libraries do exist for Ruby). They don’t even have to use the word “assert”. Here’s an idiom I like to use in methods which receive an “options” hash:

def initialize(options={})
  @color = options.delete(:color) { "chartreuse" }
  @flavor = options.delete(:flavor) { "bacony" }
  @texture = options.delete(:texture) { "squamous"}
  raise "Unknown options #{options.keys.join(', ')}" if !options.empty?
end

The last line of the method guards against the not-uncommon scenario of a misspelled option key. It’s an assertion even though there’s no assert() in sight.

Ruby does not come with its own assert() method, but implementing one of your own is a trivial exercise:

def assert(condition, message = "Assertion failed")
  raise Exception, message unless condition
end

The only thing notable about this code is that we raise Exception explicitly, rather than some derivative of it such as RuntimeError. Assertion violations are by definition indicative of an error in the code, which means we should give the program little opportunity to rescue the exception and continue. By raising Exception, we ensure the error will bypass default rescue clauses.

begin
  assert("black" == "white", "Zebra attack!")
rescue nil
end # raises "Zebra attack!" despite the rescue

It’s possible to go overboard with assertions. Specifying every nitpicky detail about your inputs can lead to brittle, hard-to-test code and violates the spirit of dynamic typing. But used judiciously they can help to document expectations, keep yourself and your API consumers honest, and reduce time spent debugging errors.

Some guidelines for using assertions effectively:

Assert at module boundaries. Don’t pepper every internal method with assertions. Instead, use them as gatekeepers between modules. Especially use them where your code interacts with a third-party API to document and validate your beliefs about how that API works. This can greatly help your learning process as you get the hang of an unfamiliar library, as well as alerting you to changes introduced by new versions of the third-party code.

Only use assertions where coercion/enforcement is not an option. If it is possible to coerce a value into what you need, or to provide a sensible default for a missing value, prefer those approaches to making assertions.

Don’t assert exact types. Idiomatic Ruby doesn’t care about an object’s class; only that it supports the needed protocol (methods). Prefer value comparisons to respond_to?() checks, respond_to?() checks to kind_of?(), and kind_of?() to instance_of?().

assert(s.instance_of?(String)) # bad
assert(s.kind_of?(String)) # better
assert(s.respond_to?(:downcase)) # better
assert(s =~ /[[:alnum]]{6,20}/) # best

Finally, if you like the idea of using more assertions in your code, you might be interested in FailFast, a gem I wrote which provides a number convenience methods for concise assertion checking.

Published by Avdi Grimm

26 Comments

  1. Callout quote contains a typo: “Tust [sic], but verify”

    …but at least it does so confidently. ;>

    Reply
  2. Interesting write-up. I tend to agree with this approach, it'd be nice to see more of this kind of approach. One quick question though: why is kind_of? better than is_a?, don't they both do the same thing?

    Reply
    • “don't they both do the same thing?”

      I thought they did different things, but I just looked at the ruby docs and it looks like one is an alias of the other.

      Reply
    • kind_of?() asks if the receiver inherits the given class or module somewhere in its ancestor chain. is_a?() only returns true if the receiver is of exactly that type – not a subcless. I almost never use is_a() for this reason – my code shouldn't break just because someone subclassed String.

      Reply
    • Update: phiggy is correct. Looks like I was confusing instance_of?() with is_a?(). I've updated the article accordingly.

      Reply
  3. I think this fits in nicely with the mantra of “fail early”.

    It also helps save time with debugging in two respects: you have a clearer picture of where, in the code, there was an error; additionally, you know that the preceeding assertions all passed.

    Reply
  4. Trust, but verify? No. Can't people learn that Reagan was a moron? Just trust. That is the way of dynamic programming.

    Fail fast often takes care of itself, but if not, help it along. Just don't help it along like your examples show.

    Your #initialize example is static and brittle. If you subclass, you can't extend like so:

    def initialize
    super

    # handle new option that depends on old options being processed.
    @uncaffeinated = options.delete(:caffeinated) { not_feeling_creative }
    end

    Reply
    • I subclass and add new options all the time. It's just a matter of pulling out the added options before calling super. If for some reason you must have the superclass initialized before processing the added options, pull the option out into a local before calling super and then process it after.

      Your subclasses shouldn't be permitting values to get to the superclass constructor which they don't expect it to be able to handle.

      Reply
    • I'd also say that your example more brittle and in a more subtle way than mine. Supposing I'm the maintainer of the superclass, and you maintain the subclass. One day, having no knowledge of your code, I decided that in the next release my superclass is going to use the :caffeinated option internally. The next time you update you have to figure out why your option isn't taking effect any more and I get a grumpy complaint from you that I've appropriated “your” option.

      Much better to be courteous to superclasses and clean out subclass-specific stuff from the arguments before invoking them.

      Reply
  5. Callout quote contains a typo: “Tust [sic], but verify”

    …but at least it does so confidently. ;>

    Reply
  6. Interesting write-up. I tend to agree with this approach, it'd be nice to see more of this kind of approach. One quick question though: why is kind_of? better than is_a?, don't they both do the same thing?

    Reply
  7. I think this fits in nicely with the mantra of “fail early”.

    It also helps save time with debugging in two respects: you have a clearer picture of where, in the code, there was an error; additionally, you know that the preceeding assertions all passed.

    Reply
  8. “don't they both do the same thing?”

    I thought they did different things, but I just looked at the ruby docs and it looks like one is an alias of the other.

    Reply
  9. kind_of?() asks if the receiver inherits the given class or module somewhere in its ancestor chain. is_a?() only returns true if the receiver is of exactly that type – not a subcless. I almost never use is_a() for this reason – my code shouldn't break just because someone subclassed String.

    Reply
  10. Good catch! Looks like I've been confusing instance_of?() and is_a?()

    Reply
  11. That's a very good point – it gives you more confidence as a developer when you know that a value e.g. can't be nil beyond a certain point.

    Reply
  12. Update: phiggy is correct. Looks like I was confusing instance_of?() with is_a?(). I've updated the article accordingly.

    Reply
  13. Trust, but verify? No. Can't people learn that Reagan was a moron? Just trust. That is the way of dynamic programming.

    Fail fast often takes care of itself, but if not, help it along. Just don't help it along like your examples show.

    Your #initialize example is static and brittle. If you subclass, you can't extend like so:

    def initialize
    super

    # handle new option that depends on old options being processed.
    @uncaffeinated = options.delete(:caffeinated) { not_feeling_creative }
    end

    Reply
  14. I subclass and add new options all the time. It's just a matter of pulling out the added options before calling super. If for some reason you must have the superclass initialized before processing the added options, pull the option out into a local before calling super and then process it after.

    Your subclasses shouldn't be permitting values to get to the superclass constructor which they don't expect it to be able to handle.

    Reply
  15. I'd also say that your example more brittle and in a more subtle way than mine. Supposing I'm the maintainer of the superclass, and you maintain the subclass. One day, having no knowledge of your code, I decided that in the next release my superclass is going to use the :caffeinated option internally. The next time you update you have to figure out why your option isn't taking effect any more and I get a grumpy complaint from you that I've appropriated “your” option.

    Much better to be courteous to superclasses and clean out subclass-specific stuff from the arguments before invoking them.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *