Search code examples
clojureclojure.spec

Exercising macros with named arguments through Clojure Spec


Lets say we have a macro which takes one required argument followed by optional positional arguments like

(require '[clojure.spec     :as spec]
         '[clojure.spec.gen :as gen])

(defmacro dress [what & clothes]
  `(clojure.string/join " " '(~what ~@clothes)))

(dress "me")
=> "me"
(dress "me" :hat "favourite")
=> "me :hat favourite"

and we write it a spec for it like

(spec/def ::hat string?)
(spec/fdef dress
           :args (spec/cat :what string?
                           :clothes (spec/keys* :opt-un [::hat]))
           :ret string?)

we'll find that spec/exercise-fn fails to exercise the macro

(spec/exercise-fn `dress)
;1. Unhandled clojure.lang.ArityException
;   Wrong number of args (1) passed to: project/dress

even though the data generated by the functions generator is accepted just fine by the macro:

(def args (gen/generate (spec/gen (spec/cat :what string?
                                            :clothes (spec/keys* :opt-un [::hat])))))
; args => ("mO792pj0x")
(eval `(dress ~@args))
=> "mO792pj0x"
(dress "mO792pj0x")
=> "mO792pj0x"

Defining a function and exercising it the same way works fine on the other hand:

(defn dress [what & clothes]
  (clojure.string/join " " (conj clothes what)))

(spec/def ::hat string?)
(spec/fdef dress
           :args (spec/cat :what string?
                           :clothes (spec/keys* :opt-un [::hat]))
           :ret string?)
(dress "me")
=> "me"
(dress "me" :hat "favourite")
=> "me :hat favourite"
(spec/exercise-fn `dress)
=> ([("") ""] [("l" :hat "z") "l :hat z"] [("") ""] [("h") "h"] [("" :hat "") " :hat "] [("m") "m"] [("8ja" :hat "N5M754") "8ja :hat N5M754"] [("2vsH8" :hat "Z") "2vsH8 :hat Z"] [("" :hat "TL") " :hat TL"] [("q4gSi1") "q4gSi1"])

And if we take a look at the built in macros with similar definition patterns we'll see the very same issue:

(spec/exercise-fn `let)
; 1. Unhandled clojure.lang.ArityException
;    Wrong number of args (1) passed to: core/let

One interesting thing is that exercise-fn works fine when there's always one required named argument present:

(defmacro dress [what & clothes]
  `(clojure.string/join " " '(~what ~@clothes)))

(spec/def ::hat string?)
(spec/def ::tie string?)
(spec/fdef dress
           :args (spec/cat :what string?
                           :clothes (spec/keys* :opt-un [::hat] :req-un [::tie]))
           :ret string?)
(dress "me" :tie "blue" :hat "favourite")
=> "me :tie blue :hat favourite"
(spec/exercise-fn `dress)

In other words: There seems to be some hidden arguments always passed to macros during normal invocation which aren't passed by spec. Sadly I'm not experienced enough with Clojure to know about such details, but a little bird told me that there are things named &env and &form.

But my question boils down to: Is it possible to spec a macro with named arguments in such a way that spec/exercise-fn can give it a good workout?

Addendum:

Wrapping keys* with an and seems to break exercise-fn again, even if it has a required named arg.


Solution

  • You can't use exercise-fn with macros as you can't use apply with macros. (Note that it's called exercise fn :).

    This is exactly like (apply dress ["foo"]), which yields the familiar "can't take value of a macro". The different error message you see is because it's applying to the var rather than the macro, as what's really happening is like (apply #'user/dress ["foo"]).