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 forwhen-applied-to
. I can't get this to work withdef
, is there something I am missing?- Is there a better pattern I could use other than a "global"
ref
for thedescribed-entity
? I tried usinglet
and shadowing, but couldn't get it to work with macros - In
when-applied-to
, there's a lot of quoting and aneval
, I did this to keepit
from pre-maturely executing. Is there a cleaner way to thunkit
?
1 Answer 1
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.