3

I would like to set a group of fields in a Java object from Clojure without using reflection at runtime.

This solution (copied from one of the solutions) is close to what I am after:

(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})

This works fine but I want the macro to be able to process a map of fields and values passed in as a var or as a local binding (i.e. not passed directly to the macro as above). The fields should be represented as keywords so the following should work:

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

I can't figure out how to do this using set! and the special dot form within a macro (or any solution that works as above without using reflection at runtime).

2 Answers 2

2

For this, I would do compile-time reflection coupled with polymorphism.

(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#))}))))

This allows you to extend field setters per class.

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

Now set-field! works on either and can be used with reduce-kv on a map.

(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]>

Where in the rect example the width and height fields were left unaltered since not specified in the map.

Sign up to request clarification or add additional context in comments.

8 Comments

In my case, I have already a Clojure map which has keywords corresponding to public non-static field names and values. I wanted a simple and fast way to use this map to set an object. The challenge around writing this macro seems to be around getting the structure right for the target keyword of the dot form. Try modifying the working example above to meet the case I am after to see what I mean. My answer below works but I feel it's a bit ugly.
The problem with determining the fields based on an input map is that these are run-time values. That forces you to use eval and invoke the compiler again at run-time. I prefer my approach of determining the set of possible fields at compile-time. I think one could easily take what I have written and modify to suit maps of subsets of those fields, hints above. Maybe I'll write it later.
Hi A Webb - I like your solution and it meets my requirements. Thanks! I agree with your comment. Eval is triggering an additional compile is ugly and probably the source of the performance problems. Doing it all in a macro is much better and I was struggling to figure out how. As a supplementary question, I am wondering what approach I could take if I wanted to avoid reflection to set the objects but still have the setters generated to work with arbitrary classes that are only known at runtime; I'm probably back to having to use eval with your macro. Any thoughts?
Just did a rewrite to do the close over generated map version I hinted at. This is more flexible and appears to be faster.
Fantastic! This is orders of magnitude faster than what I was using. Thanks
|
0

Ok, this works. I'm not sure if it is faster than reflection and I'd like to see it done more elegantly if anyone has any suggestions.

(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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.