Search code examples
macrosclojurereify

Use a clojure macro to automatically create getters and setters inside a reify call


I am trying to implement a huge Java interface with numerous (~50) getter and setter methods (some with irregular names). I thought it would be nice to use a macro to reduce the amount of code. So instead of

(def data (atom {:x nil}))
(reify HugeInterface
  (getX [this] (:x @data))
  (setX [this v] (swap! data assoc :x v))) 

I want to be able to write

(def data (atom {:x nil}))
(reify HugeInterface
  (set-and-get getX setX :x))

Is this set-and-get macro (or something similar) possible? I haven't been able to make it work.


Solution

  • (Updated with a second approach -- see below the second horizontal rule -- as well as some explanatory remarks re: the first one.)


    I wonder if this might be a step in the right direction:

    (defmacro reify-from-maps [iface implicits-map emit-map & ms]
      `(reify ~iface
         ~@(apply concat
             (for [[mname & args :as m] ms]
               (if-let [emit ((keyword mname) emit-map)]
                 (apply emit implicits-map args)
                 [m])))))
    
    (def emit-atom-g&ss
      {:set-and-get (fn [implicits-map gname sname k]
                      [`(~gname [~'this] (~k @~(:atom-name implicits-map)))
                       `(~sname [~'this ~'v]
                          (swap! ~(:atom-name implicits-map) assoc ~k ~'v))])})
    
    (defmacro atom-bean [iface a & ms]
      `(reify-from-maps ~iface {:atom-name ~a} ~emit-atom-g&ss ~@ms))
    

    NB. that the atom-bean macro passes the actual compile-time value of emit-atom-g&ss on to reify-from-maps. Once a particular atom-bean form is compiled, any subsequent changes to emit-atom-g&ss have no effect on the behaviour of the created object.

    An example macroexpansion from the REPL (with some line breaks and indentation added for clarity):

    user> (-> '(atom-bean HugeInterface data
                 (set-and-get setX getX :x))
              macroexpand-1
              macroexpand-1)
    (clojure.core/reify HugeInterface
      (setX [this] (:x (clojure.core/deref data)))
      (getX [this v] (clojure.core/swap! data clojure.core/assoc :x v)))
    

    Two macroexpand-1s are necessary, because atom-bean is a macro which expands to a further macro call. macroexpand would not be particularly useful, as it would expand this all the way to a call to reify*, the implementation detail behind reify.

    The idea here is that you can supply an emit-map like emit-atom-g&ss above, keyed by keywords whose names (in symbolic form) will trigger magic method generation in reify-from-maps calls. The magic is performed by the functions stored as functions in the given emit-map; the arguments to the functions are a map of "implicits" (basically any and all information which should be accessible to all method definitions in a reify-from-maps form, like the name of the atom in this particular case) followed by whichever arguments were given to the "magic method specifier" in the reify-from-maps form. As mentioned above, reify-from-maps needs to see an actual keyword -> function map, not its symbolic name; so, it's only really usable with literal maps, inside other macros or with help of eval.

    Normal method definitions can still be included and will be treated as in a regular reify form, provided keys matching their names do not occur in the emit-map. The emit functions must return seqables (e.g. vectors) of method definitions in the format expected by reify: in this way, the case with multiple method definitions returned for one "magic method specifier" is relatively simple. If the iface argument were replaced with ifaces and ~iface with ~@ifaces in reify-from-maps' body, multiple interfaces could be specified for implementation.


    Here's another approach, possibly easier to reason about:

    (defn compile-atom-bean-converter [ifaces get-set-map]
      (eval
       (let [asym (gensym)]
         `(fn [~asym]
            (reify ~@ifaces
              ~@(apply concat
                  (for [[k [g s]] get-set-map]
                    [`(~g [~'this] (~k @~asym))
                     `(~s [~'this ~'v]
                          (swap! ~asym assoc ~k ~'v))])))))))
    

    This calls on the compiler at runtime, which is somewhat expensive, but only needs to be done once per set of interfaces to be implemented. The result is a function which takes an atom as an argument and reifies a wrapper around the atom implementing the given interfaces with getters and setters as specified in the get-set-map argument. (Written this way, this is less flexible than the previous approach, but most of the code above could be reused here.)

    Here's a sample interface and a getter/setter map:

    (definterface IFunky
      (getFoo [])
      (^void setFoo [v])
      (getFunkyBar [])
      (^void setWeirdBar [v]))
    
    (def gsm
      '{:foo [getFoo setFoo]
        :bar [getFunkyBar setWeirdBar]})
    

    And some REPL interactions:

    user> (def data {:foo 1 :bar 2})
    #'user/data
    user> (def atom-bean-converter (compile-atom-bean-converter '[IFunky] gsm))
    #'user/atom-bean-converter
    user> (def atom-bean (atom-bean-converter data))
    #'user/atom-bean
    user> (.setFoo data-bean 3)
    nil
    user> (.getFoo atom-bean)
    3
    user> (.getFunkyBar data-bean)
    2
    user> (.setWeirdBar data-bean 5)
    nil
    user> (.getFunkyBar data-bean)
    5