Object Commando languages, development and design

7Mar/102

Design By Contract with Clojure

I just learned about the design by contract features of Clojure, and I'm impressed by the simplicity. It's implemented using regular Clojure metadata (i.e. no new language constructs to support this). {Small correction to this previous statement. It looks like metadata, and can be read as metadata, but is actually compiled into the function (i.e. can't be modified at runtime). Thanks for the correction Alex.} Several times I desired DbC in Java and have tried some of the libraries written for Java. The Java ones were generally built on comments or annotations. Bottom line is that they just didn't feel like they seamlessly integrated into the language, and they seemed to have a short shelf life. By short shelf life, I mean there were a lot of proof of concepts and abandoned projects, but none that were viable over the long term.

DbC in Clojure

Pretty slick how it's implemented in Clojure. First we take a normal function definition;

(defn pos-add [& args]
(apply + args))

It doesn't really do anything interesting, just delegates to the plus operator, but should only be used for positive integers. If you've not seen the & symbol, it just collects all function arguments in as a sequence. So a precondition of this function is that all arguments passed into pos-add should be zero or greater. To add this, the code looks like:

(defn pos-add [& args]
{:pre [(not-any? neg? args)]
:post [(<= 0 %)]}
(apply + args))

So there are two new pieces, a :pre that takes expressions, all of the expressions must return true for the pre-condition to pass. In the example above, there is only one expression, and it ensures that there are not-any negative numbers in the argument parameters. It also insures the the result is 0 or greater. The post condition isn't of much value here, but I added it to demonstrate where it would go. Calling the function is the same as calling any other function, but if the pre/post conditions are not met, an AssertionError is thrown. Below are some basic tests for the function:

(is (= 10 (pos-add 1 2 3 4)))
(is (zero? (pos-add 0 0 0 0)))
(is (= 5 (pos-add 1 1 2 1)))
(is (thrown? AssertionError (pos-add 1 2 3 -4)))

What led me to Clojure's DbC features was reading On Clojure and there was a proposal for a new DbC syntax. I like DbC in the original style, but I think that the one at On Clojure has some additional benefits because it can provide some hints as to what types are expected in the function. If you read Smalltalk Best Practice Patterns by Kent Beck, he recommends to name the variables after the type that is expected. So if the method is findByName, the parameter would be aString to give the caller a hint as to what is expected. What was detailed in the On Clojure blog not only provided hints about the type but also would also let you know acceptable values just by looking at the function declaration and the accepted parameters.

I would like to see the pre/post condition information somehow worked into the documentation generated by Clojure. Seems like it would be a very useful feature for callers of APIs.

Comments (2) Trackbacks (0)
  1. I have been looking at the clojure pre and post conditions too. One caveat, they look like regular metadata but they are not.

    Asking on the #clojure IRC channel revealed they are currently implemented by compilation into the function at the time of definition.

    The original condition definitions are available as metadata on the arglist of the function (where the arglist is itself metadata of function) e.g.:

    (map meta (:arglists (meta #’foo)))

    But that is just reporting, you can’t currently modify the pre/post conditions associated with a function.

  2. Thanks for the correction! I updated the post to add the caveat.


Leave a comment

(required)

No trackbacks yet.