Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling Java new with a list of constructor args instead of args themselves (in Clojure)

I know I can instantiate a Java class like this in Clojure:

(new Classname args*)

Suppose I'm handed a list of the args that the constructor uses. How do I instantiate the class then? I can't use apply since new isn't a function.

like image 220
Paul Reiners Avatar asked Jan 31 '12 17:01

Paul Reiners


1 Answers

There are two basic approaches:

  1. Reflection:

    (clojure.lang.Reflector/invokeConstructor Klass (to-array [arg ...]))
    

    Slow, but completely dynamic.

  2. Unpack the arguments beforehand:

    (let [[arg1 arg2 ...] args]
      (Klass. arg1 arg2 ...))
    

    ((Klass. ...) is the idiomatic way of writing (new Klass ...); it gets converted to the latter form at macro expansion time.)

    This will be faster if the compiler can deduce which constructor will be used (you'll probably need to provide appropriate type hints -- use (set! *warn-on-reflection* true) to see if you've got it right).

The second approach is, of course, slightly unwieldy. If you expect to construct a lot of Klass instances at a lot of places in your code, you can write an appropriate factory function. If you expect to deal with many classes in this way, you can abstract away the process of defining factory functions:

(defmacro deffactory [fname klass arg-types]
  (let [params (map (fn [t]
                      (with-meta (gensym) {:tag t}))
                    arg-types)]
    `(defn ~(with-meta fname {:tag klass}) ~(vec params)
       (new ~klass ~@params))))

Finally, if the process of defining factory functions itself needs to be completely dynamic, you can do something like Chouser's second approach to this question: define a function rather than a macro and have it eval something like the syntax-quoted (defn ...) form above (syntax-quoted = with backtick in front of it; I'm not sure how to include a literal backtick in an SO post), except you'll want to use fn rather than defn and possibly skip the fname. The call to the compiler will be expensive, but the function returned will perform as any Clojure function would; see the aforementioned answer by Chouser for a slightly longer discussion.

For the sake of completeness, if you're using Clojure 1.3 or later and the Java class involved is actually a Clojure record, then a positional factory function will have already been created under the name of ->RecordName.

like image 115
Michał Marczyk Avatar answered Sep 18 '22 20:09

Michał Marczyk