Clojure macros, reify and Java annotations. Oh my!

June 15, 2014 —

Using Java annotations with Clojure and deftype, definterface, etc. is a bit under-documented in my opinion. There's an example from Rich Hickey, but even so, some more examples in the official documentation would be quite nice. Especially so when using these functions combined with your own custom macros, say if you want to generate classes/interfaces at runtime and pass in dynamic values for the annotation's properties.

It just so happens I spent the bulk of the afternoon today when it was gorgeous outside sitting inside at my computer fiddling about with reify and macros and applying Java annotations to the generated anonymous class methods. I spent a fair bit of time on Google first but didn't find anyone who was doing quite the same thing ... there was some stuff on gen-class, but that wasn't really what I needed. Perhaps my Google-fu is weak.

Anyway, lets say that we are working with some random Java library and some method we want to use in it expects to be passed an instance of some class with a calculate method accepting two numbers. Yup, totally.

(definterface ICalculator
  (calculate [a b]))

(def adder
  (reify ICalculator
    (calculate [_ a b]
      (+ a b))))

(.calculate adder 1 2)
=> 3

Nothing special. And it works great too!

But what if said Java library would not run our calculate method unless it was decorated with some Java annotation? And that annotation has a property that needs to be set to different values based on the method of calculation! (Why, you ask? Because! Don't ask silly questions!)

The horror!

@Retention(RUNTIME)
@Target(METHOD)
public @interface AmazingAnnotation {
	String foobar();
}

Now applying it to reify is very similar to the example shown for deftype in the Gist linked above:

(def adder
  (reify ICalculator
    ({AmazingAnnotation {:foobar "important value"}}
     calculate [_ a b]
      (+ a b))))

We can verify that the annotation exists on the method:

(.getAnnotation
  (->> adder
       (.getClass)
       (.getDeclaredMethods)
       (filter #(= "calculate" (.getName %)))
       (first))
  AmazingAnnotation)
=> #<$Proxy6 @testapp.AmazingAnnotation(foobar=important value)>

Great! But we don't want to hard-code the :foobar value, we need that to be specified at runtime from some other source.

(defn make-adder [foobar]
  (reify ICalculator
    ({AmazingAnnotation {:foobar foobar}}
      calculate [_ a b]
      (+ a b))))
=> CompilerException java.lang.UnsupportedOperationException: Can't eval locals ... 

Hrm, well... I didn't know that it wasn't possible to do that, but now I do. So I guess we need a macro! And this for me was the fun part, because you have to use something like with-meta, vary-meta, etc. because you want to apply the annotation metadata to the form emitted by the macro, not the forms in the macro itself!

(defmacro make-adder [foobar]
  `(reify ICalculator
     (~(with-meta
         'calculate
         `{AmazingAnnotation {:foobar ~foobar}})
      [_ a# b#]
      (+ a# b#))))

And to verify that it does work:

(.getAnnotation
  (->> (make-adder "hooray!")
       (.getClass)
       (.getDeclaredMethods)
       (filter #(= "calculate" (.getName %)))
       (first))
  AmazingAnnotation)
=> #<$Proxy6 @testapp.AmazingAnnotation(foobar=hooray!)>

In the end, I think what messed me up for the longest time was getting the right combination of quoted/unquoted forms in the macro and wrapping the exact right form in the call to with-meta. Hope this helps someone else out there.