How might you implement design-by-contract in Clojure specifically or functional languages in general?
Asked Answered
B

2

7

I'd prefer examples to be in a Lisp variant (bonus points for Clojure or Scheme) since that's what I'm most familiar with, but any feedback regarding DBC in functional lanugages would of course be valuable to the greater community.

Here's an obvious way:

(defn foo [action options]
    (when-not (#{"go-forward" "go-backward" "turn-right" "turn-left"} action)
              (throw (IllegalArgumentException.
                     "unknown action")))
    (when-not (and (:speed options) (> (:speed options) 0))
              (throw (IllegalArgumentException.
                     "invalid speed")))
    ; finally we get to the meat of the logic)

What I don't like about this implementation is that the contract logic obscures the core functionality; the true purpose of the function is lost in conditional checks. This is the same issue I raised in this question. In an imperative language like Java, I can use annotations or metadata/attributes embedded in documentation to move the contract out of the method implementation.

Has anyone looked at adding contracts to metadata in Clojure? How would higher-order functions be used? What other options are there?

Beater answered 8/12, 2009 at 7:39 Comment(3)
Have you looked at how contracts are implemented in plt-scheme? Take a look. docs.plt-scheme.org/guide/contracts.htmlAvigation
@Alexey - That's a spectacular resource! I'm pretty new to Scheme (working through the The Little/Seasoned books) and I didn't know this existed, so thank you.Klina
Not directly an answer for your question, but take a look at QuickCheck and its derivatives (ClojureCheck). It's basically property based testing, and in contracts you define properties, so you could get easily the tests generated too.Divestiture
T
4

Clojure already has support for pre and post conditions, unfortunately not well documented:

Should I use a function or a macro to validate arguments in Clojure?

Tabitha answered 8/12, 2009 at 7:39 Comment(1)
Pre/Post conditions will be an official part of Clojure 1.1, due to be released any day now.Romeu
M
3

I could imagine something like this in Clojure:

(defmacro defnc
  [& fntail]
  `(let [logic# (fn ~@(next fntail))]
     (defn ~(first fntail)
       [& args#]
       (let [metadata# (meta (var ~(first fntail)))]
         (doseq [condition# (:preconditions metadata#)]
           (apply condition# args#))
         (let [result# (apply logic# args#)]
           (doseq [condition# (:postconditions metadata#)]
             (apply condition# result# args#))
           result#)))))

(defmacro add-pre-condition!
  [f condition]
  `(do
     (alter-meta! (var ~f) update-in [:preconditions] conj ~condition)
     nil))

(defmacro add-post-condition!
  [f condition]
  `(do
     (alter-meta! (var ~f) update-in [:postconditions] conj ~condition)
     nil))

An example session:

user=> (defnc t [a test] (a test))
\#'user/t
user=> (t println "A Test")
A Test
nil
user=> (t 5 "A Test")
java.lang.ClassCastException: java.lang.Integer (NO_SOURCE_FILE:0)
user=> (add-pre-condition! t (fn [a _] (when-not (ifn? a) (throw (Exception. "Aaargh. Not IFn!")))))
nil
user=> (t 5 "A Test")
java.lang.Exception: Aaargh. Not IFn! (NO_SOURCE_FILE:0)
user=> (t println "A Test")
A Test
nil

So you can define the function and then afterwards define pre- and post-conditions whereever you like, without cluttering the function logic itself.

condition functions should throw an exception if something is wrong.

Motorman answered 8/12, 2009 at 7:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.