11

I'm writing a Clojure wrapper for the Braintree Java library to provide a more concise and idiomatic interface. I'd like to provide functions to instantiate the Java objects quickly and concisely, like:

(transaction-request :amount 10.00 :order-id "user42")

I know I can do this explicitly, as shown in this question:

(defn transaction-request [& {:keys [amount order-id]}]
  (doto (TransactionRequest.)
    (.amount amount)
    (.orderId order-id)))

But this is repetitive for many classes and becomes more complex when parameters are optional. Using reflection, it's possible to define these functions much more concisely:

(defn set-obj-from-map [obj m]
  (doseq [[k v] m]
    (clojure.lang.Reflector/invokeInstanceMethod
      obj (name k) (into-array Object [v])))
  obj)

(defn transaction-request [& {:as m}]
  (set-obj-from-map (TransactionRequest.) m))

(defn transaction-options-request [tr & {:as m}]
  (set-obj-from-map (TransactionOptionsRequest. tr) m))

Obviously, I'd like to avoid reflection if at all possible. I tried defining a macro version of set-obj-from-map but my macro-fu isn't strong enough. It probably requires eval as explained here.

Is there a way to call a Java method specified at runtime, without using reflection?

Thanks in advance!

Updated solution:

Following the advice from Joost, I was able to solve the problem using a similar technique. A macro uses reflection at compile-time to identify which setter methods the class has and then spits out forms to check for the param in a map and call the method with it's value.

Here's the macro and an example use:

; Find only setter methods that we care about
(defn find-methods [class-sym]
  (let [cls (eval class-sym)
        methods (.getMethods cls)
        to-sym #(symbol (.getName %))
        setter? #(and (= cls (.getReturnType %))
                      (= 1 (count (.getParameterTypes %))))]
    (map to-sym (filter setter? methods))))

; Convert a Java camelCase method name into a Clojure :key-word
(defn meth-to-kw [method-sym]
  (-> (str method-sym)
      (str/replace #"([A-Z])"
                   #(str "-" (.toLowerCase (second %))))
      (keyword)))

; Returns a function taking an instance of klass and a map of params
(defmacro builder [klass]
  (let [obj (gensym "obj-")
        m (gensym "map-")
        methods (find-methods klass)]
    `(fn [~obj ~m]
       ~@(map (fn [meth]
               `(if-let [v# (get ~m ~(meth-to-kw meth))] (. ~obj ~meth v#)))
              methods)
       ~obj)))

; Example usage
(defn transaction-request [& {:as params}]
  (-> (TransactionRequest.)
    ((builder TransactionRequest) params)
    ; some further use of the object
  ))
2
  • Without reflection? Almost certainly not. Commented Feb 28, 2012 at 19:38
  • 2
    Well, it is possible to translate a map into method calls without reflection using a macro. I only used reflection upon realizing that the macro couldn't take a symbol holding the map, but only the raw map itself. I should probably have been more clear in stating that I'd like to avoid reflection at runtime, like @joost-diepenmaat describes below. Commented Feb 28, 2012 at 21:20

2 Answers 2

8

You can use reflection at compile time ~ as long as you know the class you're dealing with by then ~ to figure out the field names, and generate "static" setters from that. I wrote some code that does pretty much this for getters a while ago that you might find interesting. See https://github.com/joodie/clj-java-fields (especially, the def-fields macro in https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj).

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, this was really helpful. I needed to turn my approach upside-down and rather than taking any map params then trying to reflect at runtime, enumerate setters at compile-time instead. An added bonus is that only valid params are checked.
1

The macro could be as simple as:

(defmacro set-obj-map [a & r] `(doto (~a) ~@(partition 2 r)))

But this would make your code look like:

(set-obj-map TransactionRequest. .amount 10.00 .orderId "user42")

Which I guess is not what you would prefer :)

1 Comment

True, this syntax is not ideal and also can't be used by callers of your function. You would still need to map caller arguments to this syntax somehow.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.