Pure Danger Tech


Abstracting in Clojure

29 Apr 2010

I was working on some JDBC-related Clojure code this week and had a nice refactoring/abstraction experience in a case where I would have punted (or not even considered it) in Java.

The code in question is reading a bunch of metadata from DatabaseMetaData and producing output. The interesting methods in DBMD all takes some filtering parameters and return a ResultSet. For example (using the clojure.sql lib):

(defn process-tables
    (reduce conj '() 
      (map #(processTableRow %)
          (.getTables (.getMetaData (connection)) nil nil "%" nil)))))

Given a connection, we (read from the inside-out):

  1. Get the DatabaseMetaData
  2. Call getTables() on it
  3. Convert the ResultSet into a sequence of rows (in the form of a map keyed by column name
  4. For each row map, call processTableRow which will return a sequence of output values
  5. Combine all of those output values into a single sequence with reduce

All very well, that works great. But then if you need to process catalogs you might write something like this:

(defn process-catalogs
    (reduce conj '() 
      (map #(processCatalogRow %)
          (.getCatalogs (.getMetaData (connection)))))))

As soon as I wrote the second one of these (wasn’t quite as clean as this) I could see that there was a pattern here where the two variable bits in the pattern are a) what method and parameters to call on DatabaseMetaData and b) how to process a row of the ResultSet produced by the call. The latter one is duh a function and would be easy to abstract in Java. The former would have been more annoying, requiring either a reflective method call or maybe an anonymous inner class. Honestly, I would have probably just copied the code in Java and been done with it.

However, in Clojure it’s trivial to abstract out that pattern:

(defn import-jdbc-metadata 
  [connection databaseMetadataCall rowFunction]
  (reduce concat '()
    (map #(rowFunction %)
        (databaseMetadataCall (.getMetaData (connection)))))))

Here the abstraction points are the databaseMetadataCall and rowFunction functions, both passed into the function. We can then rewrite the prior examples like this:

(defn import-tables
    (import-jdbc-metadata connection
      (fn [dbmd] (.getTables dbmd nil nil "%" nil))

(defn import-catalogs 
    (import-jdbc-metadata connection 
      (fn [dbmd] (.getCatalogs dbmd))

(process-metadata connection :tables [nil nil “%” nil] processTableRow)

And of course you don’t have to stop there. You could further say that it would be handy to create a macro that simplified those import method patterns as well but I’ll leave that for another day.