Clojure макрос для вызова Java-сеттеров на основе карты? - PullRequest
10 голосов
/ 28 февраля 2012

Я пишу оболочку Clojure для библиотеки Java Braintree, чтобы обеспечить более лаконичный и идиоматический интерфейс.Я хотел бы предоставить функции для быстрого и краткого создания экземпляров объектов Java, например:

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

Я знаю, что могу сделать это явно, как показано в этом вопросе :

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

Но это повторяется для многих классов и становится более сложным, когда параметры являются необязательными.Используя рефлексию, можно определить эти функции гораздо более кратко:

(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))

Очевидно, я бы хотел избежать рефлексии, если это вообще возможно.Я попытался определить макро-версию set-obj-from-map, но мой макро-фу недостаточно силен.Вероятно, требуется eval, как объяснено здесь .

Есть ли способ вызова метода Java, указанного во время выполнения, без использования отражения?

Заранее спасибо!

Обновленное решение:

Следуя совету Joost, я смог решить проблему, используя аналогичную технику.Макрос использует отражение во время компиляции, чтобы определить, какие методы установщика имеет класс, а затем выплевывает формы, чтобы проверить параметр на карте и вызвать метод с его значением.

Вот макрос и пример использования:

; 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 ]

8 голосов
/ 28 февраля 2012

Вы можете использовать отражение во время компиляции ~ до тех пор, пока вы знаете класс, с которым вы имеете дело к тому времени ~, чтобы выяснить имена полей и генерировать "статические" сеттеры из этого. Я написал некоторый код, который в значительной степени делает это для получателей, который может показаться вам интересным. См. https://github.com/joodie/clj-java-fields (особенно макрос def-fields в https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj).

1 голос
/ 29 февраля 2012

Макрос может быть таким простым:

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

Но это сделало бы ваш код похожим на:

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

Что я думаю, не то, что вы бы предпочли:)

...