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.