Вызов динамического метода в макросе Clojure?
Я пытаюсь написать макрос, который вызовет java setter методы, основанные на аргументах, предоставленных ему.
Итак, например:
(my-macro login-as-fred {"Username" "fred" "Password" "wilma"})
может расширяться примерно так:
(doto (new MyClass)
(.setUsername "fred")
(.setPassword "wilma"))
Как вы порекомендовали бы заняться этим?
В частности, у меня возникают проблемы с разработкой наилучшего способа создания имени метода setter и интерпретации его как символа макросом.
Ответы
Ответ 1
Самое приятное в макросах - вам вообще не нужно копаться в классах или что-то в этом роде. Вам просто нужно написать код, который генерирует правильные s-выражения.
Сначала функция для генерации s-выражения типа (.setName 42)
(defn make-call [name val]
(list (symbol (str ".set" name) val)))
затем макрос, чтобы сгенерировать выражения и plug (~ @) их в выражение doto
.
(defmacro map-set [class things]
`(doto ~class [email protected](map make-call things))
Потому что это макрос, он никогда не должен знать, какой класс является тем, на что он вызван, или даже тем, что класс, на котором он будет использоваться, существует.
Ответ 2
Пожалуйста, не создавайте s-выражения с list
для макросов. Это серьезно повредит гигиене макроса. Очень легко сделать ошибку, которую трудно отследить. Используйте всегда синтаксическую цитату! Хотя, это не проблема в этом случае, хорошо привыкнуть использовать только синтаксис-цитату!
В зависимости от источника вашей карты вы также можете использовать ключевые слова в качестве ключей, чтобы они выглядели более clojure -like. Вот мой прием:
(defmacro configure
[object options]
`(doto ~object
[email protected](map (fn [[property value]]
(let [property (name property)
setter (str ".set"
(.toUpperCase (subs property 0 1))
(subs property 1))]
`(~(symbol setter) ~value)))
options)))
Затем это можно использовать как:
user=> (macroexpand-1 '(configure (MyClass.) {:username "fred" :password "wilma"}))
(clojure.core/doto (MyClass.) (.setUsername "fred") (.setPassword "wilma"))
Ответ 3
Кто-то (я считаю, Артур Ульфельдт) получил ответ, который был почти прав, но теперь он был удален. Пожалуйста, примите его вместо моего, если он снова опубликует. (Или принять pmf's.) Это рабочая версия:
(defmacro set-all [obj m]
`(doto ~obj [email protected](map (fn [[k v]]
(list (symbol (str ".set" k)) v))
m)))
user> (macroexpand-1 '(set-all (java.util.Date.) {"Month" 0 "Date" 1 "Year" 2009}))
(clojure.core/doto (java.util.Date.) (.setMonth 0) (.setDate 1) (.setYear 2009))
user> (set-all (java.util.Date.) {"Month" 0 "Date" 1 "Year" 2009})
#<Date Fri Jan 01 14:15:51 PST 3909>
Ответ 4
Вам нужно укусить пулю и использовать clojure.lang.Reflector/invokeInstanceMethod
следующим образом:
(defn do-stuff [obj m]
(doseq [[k v] m]
(let [method-name (str "set" k)]
(clojure.lang.Reflector/invokeInstanceMethod
obj
method-name
(into-array Object [v]))))
obj)
(do-stuff (java.util.Date.) {"Month" 2}) ; use it
Нет необходимости в макросе (насколько мне известно, макрос не позволит обойти отражение, по крайней мере, для общего случая).