Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic method calls in a Clojure macro?

Tags:

clojure

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.

like image 610
npad Avatar asked Nov 10 '09 20:11

npad


4 Answers

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.

like image 133
Arthur Ulfeldt Avatar answered Oct 19 '22 07:10

Arthur Ulfeldt


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"))
like image 45
kotarak Avatar answered Oct 19 '22 09:10

kotarak


Someone (I believe Arthur Ulfeldt) had an answer posted that was almost correct, but it's been deleted now.

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>
like image 5
Brian Carper Avatar answered Oct 19 '22 08:10

Brian Carper


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

like image 3
pmf Avatar answered Oct 19 '22 07:10

pmf