2

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.

1 Answer 1

1

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"]).

Sign up to request clarification or add additional context in comments.

2 Comments

Ah. So how do we exercise our macros? Is clojure.spec.test/check the way to go?
Keep in mind that a macro is (just) a function which takes code and returns code. It is not generally meaningful to exercise a macro the same way it is to exercise a function because "invoking" it just gives you more stuff to compile. You could use the :args generator to generate inputs and then eval an invocation with those inputs I suppose. check filters macros so also won't work with macro fdefs.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.