3
votes

If I evaluate

(def ^:macro my-defn1 #'defn)

a macro named 'my-defn1' is defined, which I can use just like 'defn'.

However, if I evaluate instead

(if true
  (def ^:macro my-defn2 #'defn))

the var for 'my-defn2' doesn't have the :macro metadata set and I can't use it as a macro, even though the 'def' form is equal to the previous case.

Here is the complete code (http://cljbin.com/paste/52322ba5e4b0fa645e7f9243):

(def ^:macro my-defn1 #'defn)

(if true
  (def ^:macro my-defn2 #'defn))

(println (meta #'my-defn1))    ; => contains :macro

(println (meta #'my-defn2))    ; => doesn't contain :macro!

(my-defn1 hello1 []
          (println "hello 1"))

(hello1)                       ; => prints "hello 1"

(my-defn2 hello2 []            ; => CompilerException: Unable to resolve 
  (println "hello 2"))         ;    symbol: hello2 in this context

What makes the behaviour different?

1
I skimmed through the Clojure source for an hour. This seems really difficult. I suppose it is related to the fact that altering the root of a var that has :macro in it's meta-data removes the :macro entry. It is not related to the fact that an unbound var is created when the def form is parsed beacuse then it is created without meta data. I wish I had more time to get to the ground of this issue. It is certainly not good practice to use def in conditional expressions but I upvoted your question because we can learn about the mechanics of the JVM Clojure implementation from a satisfying answerLeon Grapenthin
Do you think Joost's answer matches your hypothesis? Maybe the conditional def is split into a declare followed by a alter-var-root, which ignores the :macro declaration? Thanks for replying!André Gustavo Rigon
No, I don't think he answered your question but avoided it - Sorry, Joost. Both if and def are special forms, meaning that their arguments are parsed independently of evaluation. You can find their implementations inside of the source code of the Clojure compiler (Compiler.java). Meta-Data is associated to the next Object/evaluated form by the reader if it implements IMeta. Def later associates these data with the var. It needs thorough research and careful study of the source code to imagine what happens if you use def inside of if. Certainly, def isn't intended to be used this way, andLeon Grapenthin
that's why the bug you have discovered is probably known by the developers but ignored for the sake of other benefits.Leon Grapenthin
Thank you for your replies. I created an issue on Clojure's Jira regarding this question. We'll see how it goes.André Gustavo Rigon

1 Answers

1
votes

Clojure's def cannot really be conditionally applied. The documentation for def is IMO insufficiently strong on that part. It's not just bad style, it can cause all kinds of subtle issues.

You should only use def at the top-level, or in a do or let form at the top-level. Conditionally applying def will result in the functionality being split up in something like declare and a subsequent conditional def, but not always in the way that you'd expect/like.

You might be better off using def at the top level here and then conditionally using alter-var-root. Or use (def my-var (if .. .. ..)). Think about why you'd ever want to have a global definition "disappear".