2

I have a file which contains some trusted clojure source code:

((+ a b) (* a b) (- a b))

For each of the items in the list I want to generate an anonymous function:

(fn [a b] (+ a b))
(fn [a b] (* a b))
(fn [a b] (- a b))

If I call the following marco

(defmacro create-fn
  [args exprs]
    `(fn ~args ~exprs))

directly with some clojure code it works perfectly:

user=> (macroexpand-1 '(create-fn [a b] (* a b)))
(clojure.core/fn [a b] (* a b))

But when I bind the context of the file to a local and try to map my macro it will not work. On access of the first generated function I get the error message "java.lang.RuntimeException: Unable to resolve symbol: a in this context"

(Please note that I had to put an extra eval into the macro to get the value of the symbol e which is used in the anonymous function used by map)

(defmacro create-fn
  [args exprs]
   `(let [e# (eval ~exprs)]
     (fn ~args
       e#)))

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (create-fn [a b] e))
             exprs)]
  (first fns))

Any help is very much appreciated!

Miikka
  • 4,573
  • 34
  • 47
nhenrich
  • 268
  • 2
  • 7
  • `exprs` is bound to `(+ a b)`, you call `eval` on it, the error is correct – noisesmith Mar 21 '15 at 20:07
  • If I put a `(println exprs)` in the macro `create-fn` it prints "e". My understanding is that during macro expansion, `exprs` is bound to the symbol `e`, not the the value of it. Therefore I think I need to call `eval` on `exprs` to yield the value of the symbol `e`... – nhenrich Mar 22 '15 at 09:16

1 Answers1

3

Let's look at the whole code after macro expansion. This code:

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (create-fn [a b] e))
             exprs)]
  (first fns))

Expands to this, where e__900__auto__ is the symbol generated by e#:

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (let [e__900__auto__ (eval e)]
                        (fn [a b] e__900__auto__))
             exprs)]
  (first fns))

Why doesn't this work? Well, one reason is that a and b aren't even in the scope of (eval e). You might be tempted to try this next:

(defmacro create-fn [args exprs] `(fn ~args (eval ~exprs)))

After expansion, the generated function looks like this:

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (fn [a b] (eval e)))
             exprs)]
  (first fns))

This looks good, but it won't work because eval evaluates in an empty lexical environment. In other words, eval won't see a and b even with this code.

You could ditch the macro and just manually mangle the code into something you can eval, like this:

(map
 (fn [e] (eval (concat '(fn [a b]) (list e))))
 exprs)

Alternatively, you could declare the variables a and b as dynamic and then use binding to set them before evaluating the expressions.

(declare ^:dynamic a ^:dynamic b)

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (fn [a1 b1] (binding [a a1 b b1] (eval e))))
             exprs)]
  (first fns))

If you do not want to have a and b in your namespace, you could set up another namespace and evaluate the code there.


My suggested solutions do not use macros. They're not useful here, because macros are expanded at compile time, but the expressions are read at runtime. If you really do want to use macros here, you'll need to move the read-string and file handling code inside the defmacro.

Community
  • 1
  • 1
Miikka
  • 4,573
  • 34
  • 47
  • Thank you very much for your detailed answer! This helped me a lot understanding lexical scope during macro evaluation. That being sad, I will use the non-macro solution as it really looks a lot easier :-) – nhenrich Mar 22 '15 at 16:05