Wednesday, July 25, 2012

Calling a Clojure Function With :keys Destructuring From Java

I had the occasion to do some hardcore Java-on-Clojure interop at my day job today.

Like this: we have a Clojure function with a signature like

(defn clojure-function [& {:keys [host port]
                           :or {host "localhost", port 8080}}]
  (println "host:" host)
  (println "port:" port))
So parameters to clojure-function are expected to take the form (:host "myhost" :port 1234). Via the magic of the :keys directive, the values are bound to local variables named "host" and "port", corresponding to the keywords :host and :port from the input. The :or bit provides default values if one or the other (or both) of the expected keys is not specified.

This is a very useful idiom in Clojure, one which permits named or "keyword" parameters, which I think are easier to deal with than regular positional parameters. But the need to invoke this method from Java poses some ... shall we say "fun" challenges.

Challenge the First: There is nothing in Java that corresponds to the Clojure keyword. Try to invoke a method like LonoCloudClass.clojure-function(:host, "myhost", :port, myport) and the compiler will immediately complain that :host is invalid syntax. Great. So we'll probably want to pass those things in as strings.

Challenge the Second: The :keys directive and behavior implies a key-value mapping. So it would seem reasonable to pass in an associative data structure like, say, an instance of java.util.Map. Or even a Clojure map literal. That's easy enough to do from the Java side, but, perversely, the Clojure side then complains with a message like "java.lang.IllegalArgumentException: No value supplied for key: {:host "somehost", :port 1234}".

Notice what the exception message claims is the key with no value: it's the entire map! So the map is not what gets destructured by the :keys directive; what gets destructured is a sequence of things that can be grouped in pairs. The first item of each pair needs to be a Clojure keyword, which gets a local variable named after it. The second item of each pair becomes the value of that variable.

Armed with this knowledge, we can take a shot at meeting both of these challenges.

On the Java side:

- Build a java.util.Map with java.util.String instances as keys and some Object type as values.

- Invoke -javaFunction (see definition below) with the Map as its parameter

On the Clojure side:

- Write a function (-javaFunction) that takes a java.util.Map as its only parameter

- Convert the map into a Clojure sequence in which every other item is a keyword

- Pass the sequence as the parameters to the actual Clojure function

(defn- keywordize [coll]
  (apply concat (for [[k v] (into {} coll)] [(keyword k) v])))

(defn -javaFunction [args]
  (apply clojure-function (keywordize args)))

Note that in order to complete the cycle you will need to AOT (ahead-of-time compile) the Clojure namespace that defines -javaFunction and clojure-function. You will also need to add a :gen-class directive to your namespace declaring javaFunction as a static method in the generated Java class. Something like this:

(:gen-class
 :methods [#^{:static true} [javaFunction [java.util.Map] void]])

Also note that the strings that serve as the keys/keywords need to not be prefaced by a colon on the Java side. The Clojure function (keyword "thing") will return a result of :thing, which is exactly what we want in this case.