tags:

views:

444

answers:

4

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.

+4  A: 

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

pmf
The macro does not circumvent reflection at lease not in spirit. it generates s-expressions blindly with out regard to the fact that they happen to reference classes.
Arthur Ulfeldt
+7  A: 

the nice thing about macros is you dont actually have to dig into the classes or anything like that. you just have to write code that gnerates the propper 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 its a macro it never has to know what class the thing its being called on is or even that the class on which it will be used exists.

Arthur Ulfeldt
Doesn't work. For one thing you can't take the value of `.` like that.
Brian Carper
fixed to include Brian Carper's changes
Arthur Ulfeldt
+2  A: 

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>
Brian Carper
ok, i undeleated it.
Arthur Ulfeldt
+1  A: 

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"))
kotarak