Clojure runs on the JVM. Sometimes you have to interface to Java libraries
(or be interfaced). In such a case one supporting helper is gen-class
.
However it's use is not straight-forward. So let's gain some understanding
in how it works and how to use it.
Java – being object oriented – is based classes and interfaces. To leverage
existing Java libraries with Clojure it is sometimes necessary to hook into
the class system, eg. deriving from an existing class or by implementing an
interface. Besides proxy
Clojure provides the gen-class
utility for this
purpose.
However using gen-class
is not always straight forward. So we have to
first understand what is going on behind the scenes.
Let's see how a class is defined with gen-class
. For convenience we
can to this similar to :use
or :require
in the ns
clause.
(ns some.Example
(:gen-class))
(defn -toString
[this]
"Hello, World!")
This defines a class called some.Example
. Since we didn't specify a
super class the class implicitly inherits from Object
. We implement
a custom .toString
method by means of the -toString
function. Huh?
„function“? Yes. -toString
is a usual Clojure function. Note how the
function gets the actual object as first argument!
user=> (-toString (some.Example.))
"Hello, World!"
user=> (.toString (some.Example.))
"Hello, World!"
How does this fit together? The ns
clause defines a class, but it also
defines a namespace. The class defined simply contains stubs, which refer
to the Clojure functions in the namespace. To find the correct functions
we need some naming convention. Here it is the prefix -
. But we could
just as well choose a different prefix.
(ns some.Example
(:gen-class
:prefix method-))
(defn method-toString
[this]
"Hello, World!")
In fact not only the prefix is configurable. Also the namespace itself is not tied to the class name. So we can define several classes in a single namespace.
(ns some.name.space)
(gen-class
:name some.name.space.ClassA
:prefix classA-)
(gen-class
:name some.name.space.ClassB
:prefix classB-)
(defn classA-toString
[this]
"I'm an A.")
(defn classB-toString
[this]
"I'm a B.")
Just working with Object
is not very exciting nor very helpful. So
let's implement an interface. We choose Clojure's IDeref
.
(ns some.Example
(:gen-class
:implements [clojure.lang.IDeref]))
(defn -deref
[this]
"Hello, World!")
We can test, whether it works.
user=> @(some.Example.)
"Hello, World!"
So implementing an interface is easy. How about deriving from another class?
(ns some.Example2
(:gen-class
:extends some.Example))
And again the test to see that some.Example2
really inherits the
deref
implementation.
user=> @(some.Example2.)
"Hello, World!"
So far it's still quite boring. We only get to see „Hello, World!“. Pff. So let's add some state to our class. However we cannot add fields like in Java. We have only on single field available called – surprise! – state.
(ns some.Example
(:gen-class
:implements [clojure.lang.IDeref]
:state state
:init init
:constructors {[String] []}))
(defn -init
[message]
[[] message])
(defn -deref
[this]
(.state this))
Now we get some new stuff here. Let's see:
:state
defines a method which will return the object's state.:init
defines the name of the initialiser. This is a function
which has to return a vector. The first element is again a vector
of arguments to the super class constructor. In our case this is
just the empty vector. The second element is the object's state.:constructors
finally maps the arguments of the class' constructor
to the arguments of the super class' constructor. This is used to
determine which constructor is supposed to be called.Again the test:
user=> @(some.Example. "Hallo, Welt!")
"Hallo, Welt!"
But we have still one gotcha: the state is – in typical Clojure fashion – immutable. To change it after object creation, we have to resort to Clojure's state managing facilities.
(defn -init
[message]
[[] (atom message)])
(defn -deref
[this]
@(.state this))
And the obligatory test:
user=> (def o (some.Example. "Hallo, Welt!"))
#'user/o
user=> @o
"Hallo, Welt!"
user=> (reset! (.state o) "Здравей, свят")
"Здравей, свят"
user=> @o
"Здравей, свят"
So using an atom or a ref combined with a map we can save arbitrary things in the state of our object.
Sometimes it is also required to define custom methods. They are declared
in the gen-class
clause and implemented by Clojure functions in the usual
way.
(ns some.Example
(:gen-class
:methods [[show [] void]
[showMessage [String] void]]))
(defn -show
[this]
(println "Hello, World!"))
(defn -showMessage
[this msg]
(println msg))
However, you should not declare methods which are already present in implemented interfaces or extended super classes.
By adding metadata – via #^{:static true}
– to a method declaration
you can also define static methods.
As long as you don't redefine the class' signature you have to compile the class only once. The „method“ functions can be re-defined as often as you like, eg. when working with SLIME or VimClojure.
If you have two generated classes where one depends on the other, you should add also a require in the defining namespace. This resolves order issues for AOT compilation.
AOT compilation ties the generated files to the Clojure version
used for compilation. So if possible avoid gen-class
and use
proxy
.
You have to fully qualify all classnames. Only classes and
interfaces from the java.lang
package can be abbreviated.
There are abbreviations for the primitive types, eg. void
instead of Void/TYPE
or int
for Integer/TYPE
.
There are a lot of features like expose-methods
, main
and others
which don't fit here. But the above should make it easier to understand
how they work. So dive in and experiment a little. gen-class
is a bit
special but still tamable.
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