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…
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.
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.
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.
There is two special cases concerning methods:
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!
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)>
So here a short summary.
proxy
gen-class
this
and proxy-super
detailsgen-class
In most cases a combination of proxy
and gen-interface
is to be preferred
over gen-class
.
get-proxy-class
)Object
will be the super class.Published by Meikel Brandmeyer on .
I'm a long-time Clojure user and the developer of several open source projects mostly involving Clojure. I try to actively contribute to the Clojure community.
My most active projects are at the moment VimClojure, Clojuresque and ClojureCheck.
Copyright © 2009-2014 All Right Reserved. Meikel Brandmeyer