Implementing Java interfaces with Clojure records

6

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] 
  FileNameMap 
    (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.

Comments

6 Responses to “Implementing Java interfaces with Clojure records”
  1. They’re handy but I don’t much like the fact that you need to import them if you need to refer to them from another namespace. I understand why – but it still feels a bit icky.

  2. Alex says:

    Yep, I agree that there is still a bit too much “hosti-ness” leaking into records for my taste, in particular with constructors and the imports.

  3. I guess one approach to avoid imports may be to write factory functions and just import the latter.

  4. Alex says:

    @Shantanu: yep, or actually build your own defrecord that creates constructor functions automatically (which is what we do). From talking to Rich, there will be automatically created constructor functions for records at some future point.

  5. Matt Stine says:

    Alex – can you compile a record that implements an interface down to a .class file?

  6. Alex says:

    @Matt: Records always compile to a class (just like everything else in Clojure). If you use AOT, it will be a class file you can see and call as with any Java .class.