Dynamic method calls in a Clojure macro?

好久不见. 提交于 2019-12-20 18:01:15

问题


I'm attempting to write a macro which will call java setter methods based on the arguments given to it.

So, for example:

(my-macro login-as-fred {"Username" "fred" "Password" "wilma"})

might expand to something like the following:

(doto (new MyClass)
  (.setUsername "fred")
  (.setPassword "wilma"))

How would you recommend tackling this?

Specifically, I'm having trouble working out the best way to construct the setter method name and have it interpreted it as a symbol by the macro.


回答1:


The nice thing about macros is you don't actually have to dig into the classes or anything like that. You just have to write code that generates the proper s-expressions.

First a function to generate an s-expression like (.setName 42)

(defn make-call [name val]
  (list (symbol (str ".set" name) val)))

then a macro to generate the expressions and plug (~@) them into a doto expression.

(defmacro map-set [class things]
  `(doto ~class ~@(map make-call things))

Because it's a macro it never has to know what class the thing it's being called on is or even that the class on which it will be used exists.




回答2:


Please don't construct s-expressions with list for macros. This will seriously hurt the hygiene of the macro. It is very easy to make a mistake, which is hard to track down. Please use always syntax-quote! Although, this is not a problem in this case, it's good to get into the habit of using only syntax-quote!

Depending on the source of your map, you might also consider to use keywords as keys to make it look more clojure-like. Here is my take:

(defmacro configure
  [object options]
  `(doto ~object
     ~@(map (fn [[property value]]
              (let [property (name property)
                    setter   (str ".set"
                                  (.toUpperCase (subs property 0 1))
                                  (subs property 1))]
                `(~(symbol setter) ~value)))
            options)))

This can then be used as:

user=> (macroexpand-1 '(configure (MyClass.) {:username "fred" :password "wilma"}))
(clojure.core/doto (MyClass.) (.setUsername "fred") (.setPassword "wilma"))



回答3:


Someone (I believe Arthur Ulfeldt) had an answer posted that was almost correct, but it's been deleted now. Please accept it instead of mine if he posts again. (Or accept pmf's.) This is a working version:

(defmacro set-all [obj m]
  `(doto ~obj ~@(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:


You have to bite the bullet and use clojure.lang.Reflector/invokeInstanceMethod like this:

(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

No need for a macro (as far as I know, a macro would not allow to circumvent reflection, either; at least for the general case).



来源:https://stackoverflow.com/questions/1710970/dynamic-method-calls-in-a-clojure-macro

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!