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.
7 comments:
Be careful with flatten! 99% of the time apply concat is what you'd like. The problem is, flatten works recursively, while apply concat only "flattens" the first level. Everything will appear to work fine, until you try to pass a nested map or a vector as an argument. Then boom!
Hi. Can you add clojure label to post, so it will be translated to Planet Clojure?
An alternative way of solving this problem might be the :strs de-structuring, as in:
(let [{:strs [host port]} {"host" "127.0.0.1" "port" "80"}]
(prn host port))
@allen: Point taken. I happen to know this isn't an issue this time, but apply concat will work as well and is a better habit to be in. Thanks for the tip!
@lost3d: Thanks for that. The Clojure function was written before we decided to do Java interop with it, and we're all so used to using :keys that we never considered :strs. I agree that :strs would be less hassle; we should probably reach for that first when we know we're going be calling from Java. Great tip!
If you look at the source code for Clojure, it seems like it should be possible to create Clojure keywords in Java.
https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Keyword.java
From there, we can see that there are three overloads of the method intern with a visibility of public static that return a Keyword instance. Surely either
public static Keyword intern(String ns, String name)
or...
public static Keyword intern(String nsname)
Would do what you want?
@ben: It is possible, and certainly could be made to work. One of our explicit goals is to have our library code be idiomatically callable from Java. We would rather not have to expose implementation details of either the Clojure language (having to import clojure.lang.*) or our functions (that they take arguments that are keywords). Thanks for the thought, though.
Post a Comment