Факторный код Clojure, устанавливающий множество различных полей в объекте Java с использованием карты параметров, привязанной к переменной или локальному

Я хотел бы установить группу полей в объекте Java из Clojure без использования отражения во время выполнения.

Это решение (скопировано с одного решений) близок к тому, что мне нужно:

(defmacro set-all! [obj m]
    `(do ~@(map (fn [e] `(set! (. ~obj ~(key e)) ~(val e))) m) ~obj))

(def a (java.awt.Point.))
(set-all! a {x 300 y 100})

Это работает нормально, но я хочу, чтобы макрос мог обрабатывать карту полей и значений, переданных как переменную или как локальную привязку (т.е. не передаваемую непосредственно в макрос, как указано выше). Поля должны быть представлены в виде ключевых слов, поэтому следующее должно работать:

(def a (java.awt.Point.))
(def m {:x 300 :y 100})
(set-all! a m)

Я не могу понять, как это сделать с помощью set! и специальная точечная форма в макросе (или любое решение, которое работает, как указано выше, без использования отражения во время выполнения).


person optevo    schedule 16.04.2014    source источник


Ответы (2)


Для этого я бы сделал отражение во время компиляции вместе с полиморфизмом.

(defprotocol FieldSettable (set-field! [this k v]))

(defmacro extend-field-setter [klass] 
  (let [obj (with-meta (gensym "obj_") {:tag klass})
        fields (map #(symbol (.getName ^java.lang.reflect.Field %)) 
                    (-> klass str (java.lang.Class/forName) .getFields))
        setter (fn [fld] 
                 `(fn [~obj v#] (set! (. ~obj ~fld) v#) ~obj))] 
    `(let [m# ~(into {} (map (juxt keyword setter) fields))] 
       (extend ~klass 
         FieldSettable 
         {:set-field! (fn [~obj k# v#] ((m# k#) ~obj v#))}))))

Это позволяет вам расширять установщики полей для каждого класса.

(extend-field-setter java.awt.Point)
(extend-field-setter java.awt.Rectangle)

Теперь set-field! работает с любым из них и может использоваться с reduce-kv на карте.

(def pt (java.awt.Point.))
(def rect (java.awt.Rectangle.))

(def m {:x 1, :y 2})

(reduce-kv set-field! pt m) 
;=> #<Point java.awt.Point[x=1,y=2]>

(reduce-kv set-field! rect m) 
;=> #<Rectangle java.awt.Rectangle[x=1,y=2,width=0,height=0]>

Где в примере rect поля width и height были оставлены без изменений, поскольку не указаны на карте.

person A. Webb    schedule 16.04.2014
comment
В моем случае у меня уже есть карта Clojure, в которой есть ключевые слова, соответствующие публичным нестатическим именам и значениям полей. Мне нужен был простой и быстрый способ использовать эту карту для установки объекта. Проблема при написании этого макроса, похоже, заключается в том, чтобы правильно структурировать целевое ключевое слово в форме точки. Попробуйте изменить рабочий пример, приведенный выше, в соответствии с моей ситуацией, чтобы понять, что я имею в виду. Мой ответ ниже работает, но мне кажется, что это немного некрасиво. - person optevo; 17.04.2014
comment
Проблема с определением полей на основе входной карты заключается в том, что это значения времени выполнения. Это заставляет вас использовать eval и снова вызывать компилятор во время выполнения. Я предпочитаю свой подход к определению набора возможных полей во время компиляции. Я думаю, что можно легко взять то, что я написал, и изменить в соответствии с картами подмножеств этих полей, как указано выше. Может позже напишу. - person A. Webb; 17.04.2014
comment
Привет, А. Уэбб. Мне нравится ваше решение, и оно соответствует моим требованиям. Спасибо! Я согласен с вашим комментарием. Eval запускает дополнительную компиляцию, это некрасиво и, вероятно, является источником проблем с производительностью. Делать все это в макросе намного лучше, и я изо всех сил пытался понять, как это сделать. В качестве дополнительного вопроса мне интересно, какой подход я мог бы использовать, если бы я хотел избежать отражения для установки объектов, но все же иметь сгенерированные сеттеры для работы с произвольными классами, которые известны только во время выполнения; Я, наверное, вернулся к тому, чтобы использовать eval с вашим макросом. Есть предположения? - person optevo; 17.04.2014
comment
Просто переписал, чтобы закрыть сгенерированную версию карты, на которую я намекал. Это более гибкий и кажется более быстрым. - person A. Webb; 17.04.2014
comment
Фантастика! Это на порядки быстрее, чем то, что я использовал. Спасибо - person optevo; 17.04.2014
comment
@optevo Re: мысли? Трудно представить себе получение класса во время выполнения, которого вы не ожидали во время компиляции, для которого вы все еще знаете некоторые поля, которые хотите установить. Более вероятно, что существует набор классов с общим подмножеством полей, и вы не знаете, какой из этого набора вы можете получить. В этом случае определите протокол и используйте полиморфизм. - person A. Webb; 17.04.2014
comment
@optevo Просто изменил код, чтобы использовать полиморфизм. Теперь все классы, для которых он был расширен, реагируют на set-field! через полиморфизм, вместо того, чтобы иметь свои собственные индивидуально именованные сеттеры. Мне это сейчас нравится, и я могу использовать это сам! - person A. Webb; 17.04.2014
comment
это фантастический ответ и отличный учебник по макросам. Я просто хотел избавиться от целой кучи кода, который устанавливает значения java-объектов. простое вырезание / вставка этого ответа дает мне серьезное сокращение кода без потери функциональности. Благодарность! - person zach; 30.11.2015

Хорошо, это работает. Я не уверен, что это быстрее, чем отражение, и я бы хотел, чтобы это было сделано более элегантно, если у кого-то есть предложения.

(defmacro set-fn-2 [f]
    `(fn [o# v#] (set! (. o# ~f) v#)))

(defmacro set-fn-1 [f]
    `(list 'set-fn-2 (symbol ~f)))

(defn set-fn-0 [f]
    (eval (set-fn-1 (symbol (name f)))))

(def set-fn (memoize set-fn-0))

(def a (java.awt.Point.))

(def val-map {:x 1 :y 2})

(defn set-all! [o m]
    (doseq [k (keys m)] ((set-fn k) o (m k))))

(set-all! a val-map)

a
person optevo    schedule 16.04.2014