3
\$\begingroup\$

I wrote a very small little DSL for spec testing:

(ns funky-spec.core)

(def described-entity (ref nil))

(defn it [fun & v]
  (assert (apply fun (cons (deref described-entity) v))))

(def it-is it)
(def it-is-the it)

(defmacro describe [value nest]
  `(dosync (ref-set described-entity ~value)
           ~nest))

(defmacro when-applied [nest]
  `(dosync (ref-set described-entity ((deref described-entity)))
           ~nest))

(defmacro when-applied-to [& args-and-nest]
  `(dosync (ref-set described-entity (apply (deref described-entity) (take (dec (count (quote ~args-and-nest))) (quote ~args-and-nest))))
           (eval (last (quote ~args-and-nest)))))

This turned out to be very nice, for writing some simple, concise specs:

(ns funky-spec.core-test
  (:require [clojure.test :refer :all]
            [funky-spec.core :refer :all]))

(describe 42
  (it = 42))

(describe 99
  (it not= 42))

(describe 0
  (it-is zero?))

(describe 99
  (it-is-the (complement zero?)))

(defn answer [] 42)

(describe answer
  (when-applied
    (it = 42)))

(describe identity
  (when-applied-to 99
    (it = 99)))

(describe +
  (when-applied-to 42 35
    (it = 77)))

(describe +
  (when-applied-to 42 35 9 10
    (it = 96)))

I'm new to Clojure and macros so any thoughts on this code would be appreciated. I have a few concerns that you could start with:

  • when-applied could (I believe) be an alias for when-applied-to. I can't get this to work with def, is there something I am missing?
  • Is there a better pattern I could use other than a "global" ref for the described-entity? I tried using let and shadowing, but couldn't get it to work with macros
  • In when-applied-to , there's a lot of quoting and an eval, I did this to keep it from pre-maturely executing. Is there a cleaner way to thunk it?
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

This is a case where a dynamic variable is the ideal solution:

(def ^:dynamic *described-entity*)

(defn it [fun & v]
    (assert (apply fun *described-entity* v)))

(def it-is it)
(def it-is-the it)

(defmacro describe [value nest]
  `(binding [*described-entity* ~value]
     ~nest))

(defmacro when-applied-to [& args-and-nest]
  (let [args (butlast args-and-nest)
        nest (last args-and-nest)]
    `(binding [*described-entity* (*described-entity* ~@args)]
       ~nest)))

(defmacro when-applied [nest]
  `(when-applied-to
    ~nest))

Online resources can probably explain dynamic vars better than I can here. For the purposes here, think of it like your atom except that it's thread local and will restore the original value when the binding context exits.

To use a macro from another macro, you'll want to do it as shown here rather than trying to literally def it in terms of another macro. The reasons why should probably be saved for when you get a little more macro experience.

And of course, butlast.

In when-applied-to you can do your argument wrangling outside of the macro. Note though, I'd probably consider passing args in a vector like (when-applied-to [42 35] (it = 77)) to make the arguments more explicit and require less pre-processing.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.