Reputation: 4816
Given the following in one file:
(ns demo.first)
(defmacro ^:private private-macro [a] a)
And the following in another file:
(ns demo.second
(:require [demo.first :as first]))
(first/private-macro 10)
The call to the private macro in demo.second will throw: var: #'demo.first/private-macro is not public
, as I expect.
Now, is there a way to have this call succeed, without making the macro public?
For functions, I can do:
(#'first/private-macro 10)
But with a macro, it throws: Wrong number of args (1) passed to: first/private-macro
.
I'm looking to unit test this private macro, and personally prefer using private meta over an impl
namespace. Which is why I'm hoping there's a solution to this.
Thank You.
UPDATE:
I found out that since defmacro
is itself a macro, it first expands into a form which creates the symbol and Var for the macro and add its metadata to it.
Thus:
(defmacro ^:private private-macro [a] a)
First is processed by the defmacro
macro, and expanded into:
(do
(clojure.core/defn ^{:private true} private-macro
([&form &env a] a))
(. (var ^{:private true} private-macro)
^{:line 487, :column 49}
(setMacro))
(var ^{:private true} private-macro))
As you can see, what then happens is that:
private-macro
fn is declared with defn
, and set to private.[&form &env a]
. This is why we get the wrong number of argument (1) exception when using #'
to call the macro.private-macro
var is set as a macro by calling its setMacro
method.private-macro
var is returned.In essence, what is happening is that if you call the function pointed to by the private-macro
var, such as is the case when using the (#'private-macro)
syntax, you're actually calling the function you see above, which takes 3 arguments. If your macro itself took more than one argument, that function would take 2 + the number of args of your macro.
So I still don't know how to call a private macro:
At first I thought stubbing out &form
and &env
with nils would work:
(#'first/private-macro nil nil 10)
And for my simple macro above it does, and return 10. But on more complicated macros, which need to be expanded further, it doesn't, and instead I get the macro-expansion returned to me ?!?
Then I thought I could use alter-meta!
to remove the private meta from the macro temporarily before calling it. As such:
(alter-meta! #'first/private-macro
(fn [meta] (dissoc meta :private)))
(first/private-macro 10)
(alter-meta! #'first/private-macro
(fn [meta] (assoc meta :private true)))
But this only works at the REPL. Try to compile your code afterwards, and it seems the Compiler itself will throw the var: #'demo.first/private-macro is not public
error, even before the alter-meta!
has a chance to run, thus failing compilation.
I don't really know why #'
doesn't work the same as a normal call to the macro, and why passing nil to the &form
and &env
doesn't work for all macros. And how to make alter-meta!
work at compile time. So if someone does know, please answer away!
Upvotes: 4
Views: 830
Reputation: 92117
And for my simple macro above it does, and return 10. But on more complicated macros, which need to be expanded further, it doesn't, and instead I get the macro-expansion returned to me ?!?
Yes. As you discovered, when you write (defmacro m [x] (list x x))
, you:
m
that consumes forms as input and produces forms as output(m a)
and replace them with the result of calling your m
function at compile timeBy calling #'m
instead, you bypass step 2: there is no call to the macro m
, and so the compiler does not call it at compile time or replace the calling code with the result. Since #'m
is just a regular function which takes code as input and produces code, when you bypass the special compiler behavior and call it at runtime, you of course get code as a result (which you can't do much with because it's runtime already).
Good news, though: there's rarely a compelling reason to make a macro private anyway, since it can do no harm to let other namespace call it. All the private macro does is expand into code the client could have written by hand anyway. So, if you control this macro, you should probably just make it public. If you don't, then you can just write whatever code the macro would have written for you.
If you absolutely insist on calling someone else's private macro, then you can split the parts (1) and (2) up, in a way: define your own macro whose implementation delegates to the function backing the private var in the other namespace:
(defmacro cheat [& args]
(apply #'m &form &env args))
Because cheat
is your own macro, you can call it in the usual way, engaging the compiler's "call this at compile time" mechanism. Then you delegate to the function that generates the code you want, passing &form
and &env
explicitly.
Upvotes: 2