gen-class – how it works and how to use it
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.
Why Classes?
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.
The Anatomy of gen-class
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.")
Interfaces and Classes
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!"
State
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:
:statedefines a method which will return the object's state.:initdefines 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.:constructorsfinally 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.
Custom Methods
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.
Hints and Quirks
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-classand useproxy.You have to fully qualify all classnames. Only classes and interfaces from the
java.langpackage can be abbreviated.There are abbreviations for the primitive types, eg.
voidinstead ofVoid/TYPEorintforInteger/TYPE.
Further Features
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.