Pure Danger Tech


Implementing Java interfaces with Clojure records

23 Nov 2010

One of the Clojure 1.2 features that I use all the time is the new record construct. Records are an improved version of the old struct construct (in most ways).

Records are designed for cases where you want to collect a bunch of related information in a map-like way. In Java you would create a class with fields, getters, setters, etc. In Clojure you create a record which generates a class on the fly with the proper fields, a constructor taking all of them, equals, hashCode, Serializable, etc. The fields in the record are immutable (as with all good Clojure data structures). Record instances are created with the normal Java constructor syntax.

(defrecord Unicorn [color size])
;; user.Unicorn
(def uni (Unicorn. :white :giant))
;; #'user/uni
(println "color=" (:color uni) " size=" (:size uni))
;; color= :white size= :giant

Things start to get interesting when you consider all of the other native Clojure abstractions that are layered onto this object. Records are really an associative data structure (just like a map). In fact, they ARE a map in every way that matters. You can get items from the record with :keywords, use get, assoc, merge, etc. A function like assoc will create a new record instance with all of the old fields values plus any assoc’ed key/value pairs over the top (no structural sharing here like you get with maps).

(def red-uni (assoc uni :color :red))
;; #'user/red-uni
(println "color=" (:color red-uni))
;; color= :red

Finally, getting to my point, it’s also useful to implement Clojure protocols or Java interfaces directly in the record. You can do that by just adding the interface or protocol name and the method definitions after the fields. One possibly tricky thing is that all definitions must take the record instance as the first arg – this even applies to Java interface method implementations. So a Java method close() becomes (close [this] …). [It’s conventional to use “this” as the name here (it has no special meaning in Clojure).]

So here we make a Thing record that implements FileNameMap (a fairly arbitrary interface that I picked out because it just has one method that takes a String and returns a String).

(import java.net.FileNameMap)
;; java.net.FileNameMap
(defrecord Thing [a] 
    (getContentTypeFor [this fileName] (str a "-" fileName)))
;; user.Thing
(def thing (Thing. "foo"))
;; #'user/thing
(instance? FileNameMap thing)
;; true
(map #(println %) (.getInterfaces Thing))
;; java.net.FileNameMap
;; clojure.lang.IObj
;; clojure.lang.ILookup
;; clojure.lang.IKeywordLookup
;; clojure.lang.IPersistentMap
;; java.util.Map
;; java.io.Serializable
(.getContentTypeFor thing "bar")
;; "foo-bar"

This will actually generate a Thing class that has a method getContentTypeFor so performance-wise it’s a direct call just like in Java. You can implement protocols in the same way as the interface here, but you don’t have to do it in defrecord. You can use the various extend macros to extend a protocol later on too if you want.