proxy – gen-class little brother

Clojure provides two basic ways to interface with the host platform. The more comprehending is gen-class which I touched in [a previous post]gc. It's little brother is proxy. Although less powerful it is more dynamic than gen-class. Let's see how it works…

The Anatomy of `proxy`

So what is actually the difference between gen-class and proxy? gen-class creates a named class while proxy does not 1. This has some serious consequences. The most notable is that proxy cannot add methods to objects it creates. So you can only implement interfaces and extend classes by implementing their methods. On the other hand you don't need AOT compilation.

Otherwise proxy is similar to gen-class. The object instantiated by proxy gets stubs for the methods which just look up usual clojure functions in a map. This means proxy-methods are normal (though anonymous) clojure functions and in particular they close over their environment. If a method is not found in the proxy's map the stub throws an UnsupportedOperationException or calls the super's method.

PIA – `proxy` in action

Let's compare proxy to our previous examples.

(defn make-some-example
  []
  (proxy [Object] []
    (toString [] "Hello, World!")))

As we don't get a named class from proxy but some anonymous object there is obviously no constructor to call. So we just define a function calling proxy.

user=> (.toString (make-some-example))
"Hello, World!"

In the above example we extended a pre-existing class – in this case Object. But we can also implement interfaces without specifying a super class 2.

(defn make-some-example
  []
  (proxy [clojure.lang.IDeref] []
    (deref [] "Hello, World!")))

As you would expect, we can now use @ and deref on our object.

user=> @(make-some-example)
"Hello, World!"

The next step for gen-class examples was to add some state to the object. While this was rather troublesome in the gen-class case – we had to add a constructor and some state holding field – the situation in the proxy case is much simpler. The methods are actually closures. So we can simply close over any non-constant attributes!

(defn make-some-example
  [msg]
  (proxy [clojure.lang.IDeref] []
    (deref [] msg)))

Now we simply pass the required parameters to the factory function.

user=> @(make-some-example "Hallo, Welt!")
"Hallo, Welt!"

And finally we can use the same trick to also allow modifications.

(defn make-some-example
  [msg]
  (let [state (atom msg)]
    (proxy [clojure.lang.IDeref] []
      (toString [] @state)
      (deref [] state))))

As before we now simply close over the atom to allow access later on.

user=> (def o (make-some-example "Hallo, Welt!"))
#'user/o
user=> (.toString o)
"Hallo, Welt!"
user=> (reset! @o "Здравей, свят")
"Здравей, свят"
user=> (.toString o)
"Здравей, свят"

Please note how we had to hijack the deref method from clojure.lang.IDeref since we cannot add a new method state as we did in the [gen-class examples]gc. I don't recommend doing so in a real world program.

Super Methods

Since we can also override the methods of a super class with proxy we have also the possibility of calling out to the super's method. One particular application I had for this was to define some offset for a LineNumberingPushbackReader.

(defn make-offset-reader
  [reader offset]
  (proxy [clojure.lang.LineNumberingPushbackReader] [reader]
    (getLineNumber [] (+ offset (proxy-super getLineNumber)))))

So what happens? We create a proxy to a LineNumberingPushbackReader. We use whatever reader we are given and pass it on to the super's constructor. Since we proxy a class all methods which are not defined for the proxy instance are forwarded to the corresponding super method.

We just intercept calls to .getLineNumber. Here we call the super method but add the offset before returning the result.

Multiple Arities

There is two special cases concerning methods:

  • a method with several arities
  • a method with different argument types

So consider a class like this one.

class Example {
    void someMethod(String x) {
        doSomethingWithString(x);
    }

    void someMethod(Integer x) {
        doSomethingWithInteger(x);
    }
}

Both methods are mapped to the same proxy method. So you have to test in your function to see, which version was the intended one.

(proxy [Example] []
  (someMethod
    [x]
    (condp instance? x
      String  (.doSomethingWithString this x)
      Integer (.doSomethingWithInteger this x))))

Another problem is connected to proxy-super. Again an example:

class Example {
    void someMethod(String x) {
        someMethod(x, null)
    }

    void someMethod(String x, String y) {
        doSomethingWith(x, y);
    }
}

You want to override the 2-ary version of the method. So you start out with a pretty straight forward proxy call.

(proxy [Example] []
  (someMethod [x y] (.doMoreStuff this x y)))

Arg. But this doesn't work, because the function is called for all arities. So we have to add the 1-ary version also. But we don't want to override the 1-ary version. No problem! We simply call proxy-super as we saw above.

(proxy [Example] []
  (someMethod
    ([x]   (proxy-super someMethod x))
    ([x y] (.doMoreStuff this x y))))

Cool, eh? But not working as expected. Our overriden 2-ary version is not called… To understand what's going wrong, we have to understand how proxy-super works.

And that is quite easy. When calling a method on a proxy object, the corresponding function is looked up in the proxy's method map. If there is no entry, the super's method is invoked (or an UnsupportedOperationException is thrown in case it was an interface method). So what does proxy-super do? It simply remove the map entry, calls the method in the proxy instance again and restores the map entry afterwards.

But wait! What happens when inside our class the 1-ary version calls the 2-ary version? The map entry is missing and the super's method is called! Not our override as expected!

The magic `this`

As the well-disposed reader might already have noticed there is another difference between proxy and gen-class in terms of how methods are defined. While the methods for gen-class take the object as first argument (and can thus it can be named whatever you like) proxy captures the symbol this in a similar way Java does. So in a proxy method this will always refer to the instance at hand.

However there is a gotcha! Clojure does not stop you from creating a local this. Together with proxy-super dangerous waters lie ahead.

(proxy [Object] []
  (toString
    []
    (let [this "huh?"]
      (proxy-super toString))))

In this case we get an exception. But in general there might lurk subtle bugs. So watch out!

user=> (.toString (proxy [Object] []
                    (toString
                      []
                      (let [this "huh?"]
                        (proxy-super toString)))))
#<CompilerException java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IProxy (REPL:2)>

Upshot

So here a short summary.

  • proxy
    • does not require AOT compilation
    • less ceremony for parameters
    • more dynamic than gen-class
    • can only implement pre-defined methods
    • beware of this and proxy-super details
  • gen-class
    • generates a named class
    • can define custom methods
    • allows to carry state in a special field
    • more ceremony
    • requires AOT compilation

In most cases a combination of proxy and gen-interface is to be preferred over gen-class.

Footnotes

  • This is strictly speaking not true. There is an underlying class created by proxy. But in general you cannot (and should not) access it directly. (cf. get-proxy-class)
  • In this case Object will be the super class.

Published by Meikel Brandmeyer on .