For a while now, I've been searching for an efficient, pleasant way to
implement simple immutable data structures in mixed Java/Clojure
projects and environments. My use-case for such data structures is to
provide domain classes -- often referred to as beans in the Java space
-- those objects that are just a bag of properties.
Ideally, I'd like to be be able to:
(1) define these data structures in Clojure (for obvious reasons of
concision, expressivity, etc)
(2) create and use these data structures in Clojure and Java code
using "native" idioms on both sides -- keys to a map interface in
Clojure, and methods in Java
(3) retain compatibility with the JavaBean spec
(4) optionally be able to hang additional properties off of a domain
object at runtime beyond the scope of the predefined properties
(5) accomplish (1) - (4) without sacrificing a drop of performance
The current best options are to:
- define immutable beans in Java, or
- use clojure maps or structs, which can be readily turned into beans
via clojure/bean, or
- define a Java interface and implement it in clojure using proxy or
gen-class
None of these options meet all of my criteria. After a good deal of
thought (and trying a couple of other dead-ends), I settled on the
genbean implementation. Given one or more interfaces and an ordering
of the properties defined by those interfaces, genbean will:
(a) create a struct-map definition using the given set of property
names (which must match the "getter" methods defined by the provided
interface(s))
(b) define an implementation function for each of the interfaces'
methods in the current namespace, using struct-map accessors
(c) generate a class using gen-and-save-class with the same name as
the current namespace; this generated class implements all of the
specified interfaces, and subclasses clojure.lang.PersistentStructMap
(PSM). The generated class also defines two constructors: one
matching the signature of PSM, and another matching the types of the
properties defined by the given interfaces in the order specified in
the genbean form. Implementations of these constructors are defined
in the current namespace as well.
(d) define an override of a new PSM method, makeNew, which takes the
same arguments as PSM's constructor (patch to PSM below as well).
PSM's implementation of makeNew simply returns a new PSM instance
using those args; all other methods in PSM that return a PSM route
through this makeNew method to enable polymorphic instantiation by PSM
subclasses. The makeNew override defined for the PSM subclass
generated by genbean does just that, and returns a new instance of the
generated subclass.
The upshot of this is that a domain class defined using genbean
satisfies all of the criteria (1-5) presented above. Aside from the
definition of the interfaces in Java, all of the implementation is in
clojure, and it is extremely concise in its usage (my macrology might
be a different story!). The generated class can be created and used
from Java or Clojure very naturally (especially so in Clojure, as it
is just another map). It goes so far as to ensure that new derived
instances are of the same type, so you can use assoc et al. on the map
in clojure, and the resulting object's type remains constant; this is
a huge win w.r.t. Java interop IMO, as clojure code can munge a
genbean object's properties all it wants, and pass it back into Java
without conversion or composition. In addition, you can easily add
properties to a value object using the standard methods on PSM.
Some possible to-do's at this point:
- automatically generate a BeanInfo implementation for the generated
class (as I prefer to use get-less accessor function names, i.e. "foo"
instead of "getFoo")
- automatically translate javabean-compliant interface methods (e.g.
"getFoo") so that they're actually slotted under :foo in the struct-
map, making for more pleasant usage in Clojure
- perhaps provide Java-friendly getProperty and setProperty functions
(as assoc and valAt aren't exactly idiomatic there)
- provide a way to specify that a property is of type A, but the
generated class will be instantiated with a fn that returns an object
of type A (an IFn<A>, in generics terms, though we can't enforce that
in Clojure). This would allow one to create a genbean object using
delay thunks to lazily compute and cache return values.
- genbean currently requires that you provide a name for the struct-
map definition that will be def'ed in the current namespace; I'll
probably eliminate that and generate a name for the definition based
on the current namespace's name.
- in an ideal world, use a yet-to-be-implemented gen-and-save-
interface macro to define the interface(s) in clojure as well; this
would nicely eliminate the duplication of property names that
currently is required (once in the Java interfaces, and again in the
genbean definition).
There are two potential issues with the current implementation (which
is definitely at first-cut stage):
- Property values of different types than what are defined in the
specified interfaces can be swapped in via assoc at any time. I might
override assoc in the generated class to prevent this in the future.
- The equality semantics of genbean classes are broken from the
perspective of Java users that are only aware of the implemented
interfaces; in that context, equality really should depend solely upon
the values of the properties defined in the implemented interfaces,
but PSM's equals (via APersistentMap) takes all of the PSM's values
into account. This could be resolved by providing a standard
auxiliary property access interface that all genbean classes implement
(along the lines of getProperty and setProperty in the todo item
above).
Below is an example with usage, as well as the code for genbean (and
associated utilities) and the necessary patch for PSM. I should note
that one of those associated utilities (save-gen-class) is a very thin
wrapper for gen-and-save-class that we use to make gen-class amenable
to a repeatable build process, and that allows gen-class definitions
to coexist in the same file that refers to the generated class; some
may find that utility useful by itself (this, along with a couple of
other utilities we have for integrating Clojure into a build process
would make for a nifty ant plugin which I might put together
eventually).
Cheers,
- Chas
;;;;;;;;;;;; Example ;;;;;;;;;;;;;;
// Java interface /com/snowtide/Foo.java
package com.snowtide;
public interface Foo {
public String name ();
public int size ();
}
/////////
; Clojure file /com/snowtide/FooVal.clj
(in-ns 'com.snowtide.FooVal)
; excluding to avoid collision with what the
; generated class loads into the ns
(clojure/refer 'clojure
:exclude '(seq empty assoc count cons compare name))
(lib/use (utils :in "com/snowtide/clojure"
:ns 'com.snowtide.clojure.utils))
(genbean foo-struct com.snowtide.Foo name size)
;;;;;;;;;;;; Example in use ;;;;;;;;;;;;;
user=> (import '(com.snowtide Foo FooVal))
nil
user=> (def f (FooVal. 5 "wrong types"))
java.lang.ClassCastException
user=> (def f (FooVal. "my name" 12))
#'user/f
user=> (instance? Foo f)
true
user=> (map? f)
true
user=> f
{:name "my name", :size 12}
user=> (.name f)
"my name"
user=> (:name f)
"my name"
user=> (.size f)
12
user=> (:size f)
12
user=> (def modified (assoc f :name "my NEW name"))
#'user/modified
user=> modified
{:name "my NEW name", :size 12}
user=> (instance? Foo modified)
true
user=> (def modified (assoc f :bar "some auxiliary value"))
#'user/modified
user=> modified
{:name "my name", :size 12, :bar "some auxiliary value"}
user=> (instance? Foo modified)
true