I'm trying to write a macro that will generate n functions. Here's what I have so far:
; only defined this because if I inline this into make-placeholders
; it's unable to expand i# in ~(symbol (str "_" i#))
(defmacro defn-from [str mdata args & body]
`(defn ~(symbol str) ~mdata ~args ~@body))
; use list comprehension to generate n functions
(defmacro make-placeholders [n]
`(for [i# (range 0 ~n)] (defn-from (str "_" i#) {:placeholder true} [& args] (nth args i#))))
; expand functions _0 ... _9
(make-placeholders 9)
The error I get is:
java.lang.ClassCastException: clojure.lang.Cons cannot be cast to java.lang.String
And I'm not really sure what that means, but I have this vague notion that (for ...) isn't working the way I think it is inside a macro.
You are getting confused about the distinction between runtime and compile-time, and between macros and functions. Solving a macro problem with eval
is never1 the right answer: instead, make sure that you return code that does what you want to happen. Here's a minimal change to make your original version work.
The main changes are:
defn-from
is a function, not a macro - you just want a convenient way to create lists, which the main macro is responsible for inserting into the result form. You do not want a macro here, because you don't want it expanded into the body of make-placeholders
.
make-placeholders
starts with a do
, and does its for
outside of a syntax-quote. This is the most important part: you want the code returned to the user to look like (do (defn ...))
, as if they'd typed it all in by hand - not (for ...)
, which could only ever def a single function.
(defn defn-from [str mdata args & body]
`(defn ~(symbol str) ~mdata ~args ~@body))
; use list comprehension to generate n functions
(defmacro make-placeholders [n]
(cons `do
(for [i (range 0 n)]
(defn-from (str "_" i) {:placeholder true}
'[& args]
`(nth ~'args ~i)))))
user> (macroexpand-1 '(make-placeholders 3))
(do (clojure.core/defn _0 {:placeholder true} [& args] (clojure.core/nth args 0))
(clojure.core/defn _1 {:placeholder true} [& args] (clojure.core/nth args 1))
(clojure.core/defn _2 {:placeholder true} [& args] (clojure.core/nth args 2)))
1 Very, very rarely
You can also do this completely without macros, by using a function to create functions and using the lower-level operation intern
instead of def
. It turns out to be much simpler, in fact:
(letfn [(placeholder [n]
(fn [& args]
(nth args n)))]
(doseq [i (range 5)]
(intern *ns* (symbol (str "_" i))
(placeholder i))))
Macroexpanding once the (make-placeholders 9)
expression I get this:
(for
[i__1862__auto__ (range 0 9)]
(defn-from
(str "_" i__1862__auto__)
{:placeholder true}
[& args]
(nth args i__1862__auto__)))
So defn-from
expects a string as first argument, but, because it is a macro, (str "_" i__1862__auto__)
is not evaluated and is thus past in as a list.
I played around with this for a while and came up with this:
(defmacro make-placeholders [n]
`(map eval
'~(for [cntr (range 0 n)]
`(defn ~(symbol (str "_" cntr))
{:placeholder true} [& args] (nth args ~cntr)))))
Macroexpanding (make-placeholders 3)
gives
(map eval
'((defn _0 {:placeholder true} [& args] (nth args 0))
(defn _1 {:placeholder true} [& args] (nth args 1))
(defn _2 {:placeholder true} [& args] (nth args 2))))
which is what I intended and evaluating this defines the functions _0
, _1
and _2
:
;=> (_0 1 2 3)
1
;=> (_1 1 2 3)
2
;=> (_2 1 2 3)
3
Ok, so this works, but I am still not sure its a good idea to do this.
First off eval is evil. Ok, it could also be done without eval
, but using do
instead (replace map eval
in my solution with do
). But you are possibly making your code hard to understand because you create functions that are not defined anywhere in your code. I remember that when I just started using Clojure I was looking through some library for a function and I couldn't find it. I started to think yikes, this guy must have defined a macro somewhere that defines the function I am looking for, how am I ever going to understand what's going on? If this is how people are using Clojure then it is going to be one hell of a mess and dwarf everything people have said about Perl... It turned out I was just looking at the wrong version - but you may be setting yourself and others up for some hardship.
Having said this, there may be appropriate uses for this. Or you could use something similar to generate code and put that into some file (then the source code would be available for inspection). Maybe somebody more experienced can chime in?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With