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.