One of the most amazing features of Clojure is the ability to decorate references to objects with metadata, as in the following example:
user> (def my-user {:login "pcalcado" :password "secret123"})
#'user/my-user
user> my-user
{:login "pcalcado", :password "secret123"}
user> ^my-user
nil
user> (def my-cached-user (with-meta my-user {:cached-at (java.util.Date. )}))
#'user/my-cached-user
user> my-cached-user
{:login "pcalcado", :password "secret123"}
user> ^my-cached-user
{:cached-at #}
user> my-user
{:login “pcalcado”, :password “secret123″}
user> ^my-user
nil
That feature makes the language really extensible. The combination of macros and metadata is something that lets you change the language at will.
A big problem though is that metadata is only available for symbols and collections. Those are pretty much all types Clojure introduces to the Java ecosystem, possibly allowing one to say that all types defined by Clojure itself have metadata. But a program written in Clojure is very likely to use objects written in the Java language all the time –even if you don’t write code in Java at all- so it is really annoying that you can’t add metadata to references pointing to those.
user> (def string-with-meta (with-meta "my string" {:alive true}))
; Evaluation aborted.
java.lang.ClassCastException: java.lang.String (NO_SOURCE_FILE:1)
[Thrown class clojure.lang.Compiler$CompilerException]
user> (def date-with-meta (with-meta (java.util.Date. ) {:alive true}))
; Evaluation aborted.
java.lang.ClassCastException: java.util.Date (NO_SOURCE_FILE:1)
[Thrown class clojure.lang.Compiler$CompilerException]
Update: Rich Hickey commented about a better way, using proper interfaces. This text was changed to reflect that.
Adding Metadata to Java objects
If you need to add metadata to Java objects there is a simple solution. The way metadata currently works in Clojure is based on two interfaces: IMeta and IObj. IMeta defines the meta() method that returns metadata and IObj implements that and adds the withMeta() method that adds metadata to something.
Reflecting Clojure’s immutable state philosophy, instances don’t change state when withMeta() is called. The usual implementation is to create a copy of the current object, add the metadata and return it. Symbol is an example:
public class Symbol extends AFn implements Comparable, Named, Serializable{
// ...
public Obj withMeta(IPersistentMap meta){
return new Symbol(meta, ns, name);
}
//...
}
Once you understand this protocol it is quite easy to follow that in your own objects. Even if officially only symbols, lists and maps are supposed to have metadata you can easily write an object with the desired feature.
In order to be able to add metadata to you class just let it implement one of those interfaces. I would recommend implementing IObj as it fulfils the expectations of a Clojure developer that she can add and remove metadata to instances.
So our example would become something like this:
public class User implements IObj {
private String login;
private String password;
private IPersistentMap meta;
public User(IPersistentMap meta, String login, String password) {
this.meta = meta;
this.login = login;
this.password = password;
}
public IObj withMeta(IPersistentMap meta) {
return new User(meta, login, password);
}
public IPersistentMap meta() {
return meta;
}
}
After that you can add metadata to an instance:
user=> (def plain-user (User. nil "pcalcado" "banana123"))
#'user/plain-user
user=> ^plain-user
nil
user=> (def annotated-user (with-meta plain-user {:active false}))
#'user/annotated-user
user=> ^annotated-user
{:active false}
Adding Metadata to Proxies
Another issue is with proxies. Clojure has a very nice way to define proxies to Java classes, it works like a charm:
user> (def proxied-date
(proxy [java.util.Date] []
(toString [] "My date")))
#'user/proxied-date
user> (. proxied-date toString)
"My date"
But just like Java objects proxies can’t have metadata associated to them.
user> (def meta-proxy (with-meta proxied-date {:cached true}))
; Evaluation aborted.
java.lang.ClassCastException: clojure.proxy.java.util.Date (NO_SOURCE_FILE:1)
[Thrown class clojure.lang.Compiler$CompilerException]
Proxies in Clojure have the incredibly useful feature of binding functions to Java methods defined in the proxied class. Given that we know how the language implements the metadata feature it is clear that we can just let the proxy implement the needed interfaces:
user> (defn my-proxy-with [metadata]
(proxy [Object clojure.lang.IObj] []
(withMeta [new-meta] (my-proxy-for proxied (merge metadata new-meta)))
(meta [] metadata)))
#'user/my-proxy-with
user> ^(my-proxy-with {:a 1})
{:a 1}
user> ^(with-meta (my-proxy-with {:1 1}) {:2 2})
{:2 2, :1 1}
Another option -useful if you are proxying built-in classes- is to use a constructor that receives metadata as its argument. By doing that you bypass the fact that meta() is marked as final in the Obj superclass. Most built-in classes have some metadata receiving constructor.
Let’s create a function that creates a proxy for a Clojure map:
user> (defn map-proxy-with [metadata]
(proxy [clojure.lang.APersistentMap] [metadata]
(withMeta [new-meta] (my-proxy-for proxied (merge metadata new-meta)))))
#'user/map-proxy-with
user> ^(map-proxy-with {:a 2})
{:a 2}
user> ^(with-meta (map-proxy-with {:a 1}) {:b 2})
{:b 2, :a 1}

>>or – better - have a Java interface that describes objects that can have metadata
Right, and the interfaces are already there! While the Obj abstract class may give you a free implementation of the metadata protocol, the right place to do extensions is actually just one level up - with the IObj and IMeta interfaces, which should support what you want to do without any hacks.
very neat hack
Hmm.. you are right, Rich. I will update the article accordingly.
But am I right to assume that any interface on clojure.lang is an extension point then?
thanks
I understand the general method but have some difficulties with the details : what is the “my-proxy-for” function ?
Meanwhile thanks for your work that allow me to add metadata to functions : http://tinyurl.com/ygo65me.