7

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.

Kevin
  • 24,871
  • 19
  • 102
  • 158

2 Answers2

20

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:

  1. 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.

  2. 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)))

But even better would be to 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:

(doseq [i (range 5)]
  (intern *ns* 
          (-> (symbol (str "_" i))
              (with-meta {:placeholder true}))
          #(nth %& i)))

1 Very, very rarely

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • Could you maybe elaborate why `do` is better here than `eval`? It seems they are not that different in this particular use case (code would still be *evaled* at compile time, right?) – Paul Oct 21 '11 at 20:13
  • 3
    If you're going to use `eval`, there's no reason to get a macro involved at all. You might as well just have a function that maps `eval` over some code that it generates. But...that's what the compiler does! It takes some input forms and compiles them. Introducing `map` creates additional complexities: it's lazy. Your snippet will work in the REPL, because the REPL forces the output sequence, but not when compiling a full file. Further, `eval` has limitations: it doesn't know about enclosing lexical scope, so you can't use closures, etc etc. Eval is a terrible crutch; avoid it if possible. – amalloy Oct 21 '11 at 21:07
3

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?

Community
  • 1
  • 1
Paul
  • 7,836
  • 2
  • 41
  • 48
  • what you say might be correct, and I am after all fairly new to clojure/lisps, but my understanding was that macros are good for (among other things) code generation. Which is what I'm trying to do. If you look at the hand written functions for what I want, they're very redundant, and seem like a prime candidate for this type of thing. Is this type of thing really considered not a good idea? – Kevin Oct 21 '11 at 17:42
  • Well, they (Lisps) are certainly good at code generation. I think if you are willing to use eval then you could actually get this to work (still thinking about this). It might be possible without eval. But especially in your example, it looks like the only thing the functions will do is to call (nth arg i). Is that really better than using that function directly? In functional programming these functions are not used all that much, see [here](http://tech.puredanger.com/2011/10/12/2-is-a-smell/). – Paul Oct 21 '11 at 18:03
  • map over eval, nice. Yes, I would like to hear what people think of this. I can see how it might seem dicey. – Kevin Oct 21 '11 at 18:54
  • @Kevin It was a nice exercise ;) I asked if it is a good idea [here](http://stackoverflow.com/q/7854027/346587). – Paul Oct 21 '11 at 19:01