Help me write a Clojure macro which automatically adds metadata to a function definition
Asked Answered
G

1

19

I realize that the first rule of Macro Club is Don't Use Macros, so the following question is intended more as an exercise in learning Clojure than anything else (I realize this isn't necessarily the best use of macros).

I want to write a simple macro which acts as a wrapper around a regular (defn) macro and winds up adding some metadata to the defined function. So I'd like to have something like this:

(defn-plus f [x] (inc x))

...expand out to something like this:

(defn #^{:special-metadata :fixed-value} f [x] (inc x))

In principle this doesn't seem that hard to me, but I'm having trouble nailing down the specifics of getting the [args] and other forms in the defined function to be parsed out correctly.

As a bonus, if possible I'd like the macro to be able to handle all of the disparate forms of defn (ie, with or without docstrings, multiple arity definitions, etc). I saw some things in the clojure-contrib/def package that looked possibly helpful, but it was difficult to find sample code which used them.

Gutta answered 12/6, 2009 at 22:34 Comment(2)
Good intro... major props for that.Environ
Why not use macros? Are you thinking of the C preprocessor?Disdain
S
18

Updated:

The previous version of my answer was not very robust. This seems like a simpler and more proper way of doing it, stolen from clojure.contrib.def:

(defmacro defn-plus [name & syms]
  `(defn ~(vary-meta name assoc :some-key :some-value) ~@syms))

user> (defn-plus ^Integer f "Docstring goes here" [x] (inc x))
#'user/f
user> (meta #'f)
{:ns #<Namespace user>, :name f, :file "NO_SOURCE_PATH", :line 1, :arglists ([x]), :doc "Docstring goes here", :some-key :some-value, :tag java.lang.Integer}

#^{} and with-meta are not the same thing. For an explanation of the difference between them, see Rich's discussion on the Clojure mailing list. It's all a bit confusing and it's come up a bunch of times on the mailing list; see also here for example.

Note that def is a special form and it handles metadata a bit oddly compared with some other parts of the language. It sets the metadata of the var you're deffing to the metadata of the symbol that names the var; that's the only reason the above works, I think. See the DefExpr class in Compiler.java in the Clojure source if you want to see the guts of it all.

Finally, page 216 of Programming Clojure says:

You should generally avoid reader macros in macro expansions, since reader macros are evaluated at read time, before macro expansion begins.

Selfexamination answered 12/6, 2009 at 23:20 Comment(4)
Interesting! But is there a way to do it more functionally? Wrapping a defn in a do and then destructively modifying its metadata seems kind of weird to me. Then again, my experiments with using the #^{:k :v} syntax from inside a defmacro have all been utter failures so far...Gutta
Doing any kind of def is not a very functional thing to do, since it destructively mutates the state of a global dispatch table. :) But you're right, and I've updated my answer. My previous answer was dropping the #^Integer tag metadata that a def would normally take.Selfexamination
Thanks, this is a lot more understandable and those links look very helpful, although I'm still a bit mystified about why (macroexpand-1 '(defn-plus foo [bar] (baz))) doesn't display the with-meta tag.Gutta
Try (set! print-meta true) and then look at the macro expansion. You'll see the metadata has been applied to the symbol that names the function. with-meta is executed in the body of the macro, before the macro returns. Note that it's un-quoted in the macro.Selfexamination

© 2022 - 2024 — McMap. All rights reserved.