Why does adding metadata to a function definition work differently than other forms, in Clojure?

Viewed 121

Why does this fail:

(eval (with-meta '(fn [] 0) {:stack (gensym "overflow")}))
; Syntax error compiling at (REPL:1:1).
; Unable to resolve symbol: overflow210 in this context

when none of the following fail?

(eval (with-meta '(do [] 0) {:stack (gensym "overflow")}))
; 0
(eval (with-meta '(let [] 0) {:stack (gensym "overflow")}))
; 0
(eval (with-meta '(if true 0 1) {:stack (gensym "overflow")}))
; 0
(eval (with-meta '(println "hello") {:stack (gensym "overflow")}))
; hello
; nil

The examples above are my attempt to find a minimal, reproducible example. I ran into this question when working on a macro, with a simplified example here:

(defmacro my-macro []
  (with-meta '(fn [] 0) {:stack (gensym "overflow")}))

(my-macro)
; Syntax error compiling at (REPL:1:1).
; Unable to resolve symbol: overflow156 in this context

while attempting to follow the model explained in this post on testing Clojure macros.

2 Answers

The problem is your usage of gensym, not the metadata. Observe:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(dotest
  (binding [*print-meta* true]
    (let [
          fn-code-plain  '(fn [] 42)
          fn-code-meta   (with-meta '(fn [] 42) {:alpha true})

          fn-code-sym-meta   (with-meta '(fn [] 42) {:alpha (gensym "dummy")})
          ]
      (prn fn-code-plain)
      (prn fn-code-meta)
\
      (prn (eval fn-code-plain))
      (prn (eval fn-code-meta))
      (newline)
      (prn fn-code-sym-meta)

      (throws? (eval fn-code-sym-meta))

      )
    ))

with result:

^{:line 7, :column 27} (fn [] 42)
^{:alpha true}         (fn [] 42)

#object[user$eval19866$fn__19867 0xac891b5 "user$eval19866$fn__19867@ac891b5"]
^{:alpha true} #object[user$eval19870$fn__19871 0x2d1ab7e0 "user$eval19870$fn__19871@2d1ab7e0"]

^{:alpha dummy19865} (fn [] 42)

The problem is that eval sees the symbol dummy19865 and tries to resolve it. The fact that it was created by gensym is irrelevant. No problem with a keyword:

  fn-code-kw-meta   (with-meta '(fn [] 42) {:alpha :dummy-42})
  <snip>
  (prn fn-code-kw-meta)
  (prn (eval fn-code-kw-meta))

producing:

^{:alpha :dummy-42} (fn [] 42)
^{:alpha :dummy-42} #object[user$eval19953$fn__19954 0x13253ac7 "user$eval19953$fn__19954@13253ac7"]

or a symbol that is defined:

(def mysym "Forty-Two!")
<snip>

  fn-code-mysym-meta   (with-meta '(fn [] 42) {:alpha mysym})
  <snip>

(prn fn-code-mysym-meta)
(prn (eval fn-code-mysym-meta))

with result:

^{:alpha "Forty-Two!"} (fn [] 42)
^{:alpha "Forty-Two!"} #object[user$eval20082$fn__20083 0x24a63de5 "user$eval20082$fn__20083@24a63de5"]

Summary:

You have demonstrated that eval only attempts symbol resolution of metadata for the fn form, but not other special forms like do, let, if, or with pre-existing functions such as println. If you wish to explore further, you should probably inquire at the Clojure email list:

clojure@googlegroups.com 

The above code is based on this template project.

Great question! This was fun to dig into.

It's helpful to first start by trying to set the metadata map to something that works, and retrieve it in each of your examples:

(meta (eval (with-meta '(fn [] 0) {:ten 10})))
;;=> {:ten 10}
(meta (eval (with-meta '(do [] 0) {:ten 10})))
;;=> nil
(meta (eval (with-meta '(let [] 0) {:ten 10})))
;;=> nil
(meta (eval (with-meta '(if true 0 1) {:ten 10})))
;;=> nil
(meta (eval (with-meta '(println "hello") {:ten 10})))
;; printed: hello
;;=> nil

Hopefully this gives you some idea of what's happening here: the metadata isn't returned as part of the value of the non-fn forms, so it's not evaluated. We can test this hypothesis with another value that uses its metadata, like a vector:

(meta (eval (with-meta '[1] {:ten 10})))
;;=> {:ten 10}

but with a gensym:

(eval (with-meta '[1] {:stack (gensym "overflow")}))
;;=> Syntax error compiling at (tmp:localhost:35479(clj)*:25:7).
;;=> Unable to resolve symbol: overflow6261 in this context

You can see where this is emitted in the Clojure compiler, and searching for new MetaExpr will show you the other places where metadata evaluation is emitted (I could see vectors, maps, sets, functions, and reify).

tl,dr: Clojure evaluates the metadata for function forms because it attaches the evaluated metadata to the resulting function. This is also true of the data literals supported in Clojure's syntax. Any other metadata on forms is stripped away by compilation, so it doesn't get evaluated and thus doesn't cause symbol resolution errors.

Related