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:

  • :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.

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-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.

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.

Published by Meikel Brandmeyer on .