with-meta and #^ – equivalent?

Clojure provides a set of reader macros which make life a little easier. They are basically short-hand notations for often used functions/macros. Here is a table:

Reader MacroExpansion
'x(quote x)
@x(deref x)
^x(meta x)
#(x)(fn [] (x))

The astute reader might already have noticed, that #^ is missing from the list. Isn't #^y x equivalent to (with-meta x y)?

Inner workings of reader macros

How does a reader macro actually work? As the name says the magic happens inside the reader. When it hits a certain character – eg. ', ^, @, " or # – it switches to some different mode. " for example starts parsing a String from the input stream. Some of these special characters don't influence the way the stream is actually read, but wrap the next read thing into function or macro. Most examples from the above table or of this category.

While this latter category of reader macros mostly provides some sugar for often used functions or macros, one has to take care to understand the implications. There were some interesting examples on the Clojure group recently. Take for example the following two expressions:

^#^{:meta :data} 'foo
^'#^{:meta :data} foo

What is the difference? The first line returns nil while the second returns {:meta :data}. Why is that so? Let's have a look at a partial expansion of the expressions.

(meta #^{:meta :data} (quote foo))
(meta (quote #^{:meta data} foo))

Here we see, that in the first expression the metadata is actually put on the list (quote foo) which is „the next thing read“. Since meta is a function, the expression gets evaluated to the Symbol foo, which does not have any metadata associated. So meta returns nil.

In the second expression the metadata is attached to foo which gets quoted by quote. But now meta can see the metadata and hence returns it.

For completeness sake: #^Classname is equivalent to #^{:tag Classname}.

with-meta and #^

I didn't expand #^ with with-meta in the above example. Why? Because here we have another subtle issue: they are simply not equivalent. Let's have a closer look with an example.

#^{:tag String} s
(with-meta s {:tag String})

So what happens in the first case? The reader hits #^ and recognises, that he should put the map following the #^ onto the next thing read as metadata. He reads the Symbol s and associates the map as metadata with the symbol. Nothing surprising. BUT: this happens in the reader! There is no evaluation whatsoever. In particular the String in the map is a Symbol and not the Java class „String“.

In the with-meta case, things are different. with-meta is a function it is called during runtime. s will be evaluated to the value it names. So depending on the situation it might be a Vector, nil or unbound. Furthermore the map argument gets also evaluated and the String in the map with the Java class „String“1.

So there are fundamentally different things going on here.

Upshot

Be careful with reader macros. „Understanding how things work“ is a basic requirement every serious programmer has to master. But with reader macros things get even more complicated due to the subtle interactions between the different processing stages when compiling the Clojure code.

Sidenotes

#^ is now deprecated. It is replaced by ^ and the previous functionality of ^ is gone. So no short-hand anymore for meta.

A short sidenote for macro writers: if you want to type-hint something in your macro, you have to quote the classname!

(defmacro my-string-defn
  [fn-name args & body]
  `(defn ~(with-meta fn-name {:tag `String}) ~args ~@body))

Excercises:

  1. What happens with the with-meta here?
  2. Why is String quoted with `` and not'`?

Published by Meikel Brandmeyer on .