Monday, May 02, 2011

Clojure: Converting a Java Object to a Clojure Map

Clojure has a great library that helps facilitate most things you'd want to do, but it's also easy to roll your own solution if you need something more specific. This blog entry is about converting an object to a map, something Clojure already has a function for: bean. From the docs:
Takes a Java object and returns a read-only implementation of the
map abstraction based upon its JavaBean properties.
The bean function is easy enough to use, as the following example shows (formatted for readability).
user=> (import 'java.util.Calendar)
java.util.Calendar
user=> (-> (Calendar/getInstance) .getTime bean)
{:seconds 16,
:date 2,
:class java.util.Date,
:minutes 35,
:hours 12,
:year 111,
:timezoneOffset 240,
:month 4,
:day 1,
:time 1304354116038}
The bean function is simple and solves 99% of the problems I encounter. Generally I'm destructuring a value that I pull from a map that was just created from an object; bean works perfectly for this.

However, I have run across two cases where I needed something more specialized.

The first case involved processing a large amount of data in a very timely fashion. We found that garbage collection pauses were slowing us down too much, and beaning the objects that were coming in was causing a large portion of the garbage. Since we were only dealing with one type of object we ended up writing something specific that only grabbed the fields we cared about.

The second case involved working with less friendly Java objects that needed to be converted to maps that were going to be used many times throughout the system. In general it isn't a big deal working with ugly Java objects. You can destructure the map into nice var names, or you can use the rename-keys function if you plan on using the map more than once. This probably would have worked for me, but I had an additional requirement. The particular Java objects I was working with had many methods/fields and the maps contained about 80% more information than I needed. So, I could have used bean, then rename keys, then dissoc. But, this felt similar enough to the situation in the past where I was working with specific method names and I thought I'd look for a more general solution.

The following example shows the definition of obj->map and the REPL output of using it.
user=> (import 'java.util.Date)
java.util.Date
user=> (defmacro obj->map [o & bindings]
(let [s (gensym "local")]
`(let [~s ~o]
~(->> (partition 2 bindings)
(map (fn [[k v]]
(if (vector? v)
[k (list (last v) (list (first v) s))]
[k (list v s)])))
(into {})))))
#'user/obj->map
user=> (obj->map (Date. 2012 1 31)
:month .getMonth
:year [.getYear #(* 2 %)]
:day .getDate)
{:month 2, :year 4024, :day 2}
As you can see from the output, the macro grabs the values of the methods you specify and stores them in a map. At one point I also needed to do a bit of massaging of a value, so I included the ability to use a vector to specify the method you care about and a function to apply to the value (the result of the function will become the value). In the example, you can see that the :year is double the value of what .getYear returns.

In general you should stick with bean, but if you need something more discriminating, obj->map might work for you.
Post a Comment