Clojure: Capturing Function Invocations

Anyone learning Clojure like I am is recommended to read the source code to Jay Field’s Expectations library.  I was particularly interested to read his article on scenarios.  There’s some very interesting ideas there, but at the moment I wanted something a bit more basic.  “with-redefs” already provides the ability to replace one function with another, specifically for mocking purposes.  So really all I wanted was the ability to capture function invocations.

So, here’s a working mechanism.  You use with-captures exactly the way you would use with-redefs, only you can call the “captures” function on the redefined function and it will return the list of previous function invocations.

The most interesting aspect of the code is the use of a record implementing IFn.  Implementing records well remains a bit verbose (well, by Clojure standards).  I’d highly recommend checking out defrecord2 by David McNeil, which is looking to simplify working with records.

(ns hendrix.test.capture)

(defn ignore
  "Ignores the inputs and returns the outputs.  Useful as mock target."
  [& args] nil)

; Capture

(defn capture-invoke [{:keys [f captures]} args]
  (let [r (apply f args)]
    (swap! captures conj args)))

(defrecord capture-t [f captures]
  clojure.lang.IFn
  (invoke [this] (capture-invoke this []))
  (invoke [this a] (capture-invoke this [a]))
  (invoke [this a b] (capture-invoke this [a b]))
  (invoke [this a b c] (capture-invoke this [a b c]))
  (invoke [this a b c d] (capture-invoke this [a b c d]))
  (applyTo [this args]
    (clojure.lang.AFn/applyToHelper this args)))

(defn new-capture [f]
  (new capture-t f (atom [])))

(defn to-capture [[v f]]
  (new-capture (if (= f :v) (var-get v) f)))

(defn to-capture-map [h]
  (zipmap (keys h) (->> h (map to-capture))))

(defn captures [c]
  (-> c :captures deref))

(defn with-captures-fn [bindings action]
  "Like with-redefs-fn, only you can call 'captures' on the redefined functions."
  (-> bindings
      to-capture-map
      (with-redefs-fn action)))

; Code ripped off from with-redefs
(defmacro with-captures
  "Like with-redefs, only you can call 'captures' on the redefined functions."
  [bindings & body]
  `(with-captures-fn ~(zipmap (map #(list `var %) (take-nth 2 bindings))
                              (take-nth 2 (next bindings)))
     (fn [] ~@body)))

(defn add-two [x] (+ x 2))

(defn example []
  (with-captures [identity :v
                  add-two ignore]
    (identity 3)
    (identity 6)
    (add-two 7)
    {:ignore (captures add-two)
     :passthrough (captures identity)}))
Technorati Tags: ,