使用clojure宏在reify调用中自动创建getter和setter

Art*_*ein 6 macros clojure reify

我正在尝试使用大量(~50)getter和setter方法(一些具有不规则名称)实现一个巨大的Java接口.我认为使用宏来减少代码量会很好.而不是

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

我希望能够写作

(def data (atom {:x nil}))
(reify HugeInterface
  (set-and-get getX setX :x))
Run Code Online (Sandbox Code Playgroud)

这个set-and-get宏(或类似的东西)可能吗?我无法让它发挥作用.

Mic*_*zyk 9

(更新了第二种方法 - 见下面第二条横向规则 - 以及一些解释性说明:第一条.)


我想知道这是否可能是朝着正确方向迈出的一步:

(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))
Run Code Online (Sandbox Code Playgroud)

NB.该atom-bean宏经过实际编译时emit-atom-g&ssreify-from-maps.atom-bean编译特定表单后,任何后续更改emit-atom-g&ss都不会影响所创建对象的行为.

REPL的示例宏展开(为清晰起见,添加了一些换行符和缩进):

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)))
Run Code Online (Sandbox Code Playgroud)

两个macroexpand-1是必需的,因为atom-bean是一个宏扩展到另一个宏调用.macroexpand不会特别有用,因为它会一直扩展到调用reify*后面的实现细节reify.

这里的想法是你可以提供emit-map类似emit-atom-g&ss上面的内容,由关键字键入,其名称(符号形式)将在reify-from-maps调用中触发魔术方法生成.魔术是由存储为给定函数的函数执行的emit-map; 函数的参数是"implicits"的映射(基本上是reify-from-maps表格中所有方法定义都应该可以访问的任何和所有信息,比如这个特定情况下的原子名称),后面跟着给"形式中的魔术方法说明符reify-from-maps.如上所述,reify-from-maps需要查看实际的关键字 - >功能图,而不是其符号名称; 所以,它只适用于文字地图,其他宏内或借助于eval.

正常的方法定义仍然可以包含在内,并且将被视为常规reify形式,前提是匹配其名称的键不会出现在emit-map.emit函数必须以预期的格式返回方法定义的seqables(例如向量)reify:这样,为一个"魔术方法说明符"返回的多个方法定义的情况相对简单.如果iface参数用替换ifaces~iface~@ifacesreify-from-maps'体,多个接口可以为实现中指定.


这是另一种方法,可能更容易推理:

(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))])))))))
Run Code Online (Sandbox Code Playgroud)

这在运行时调用编译器,这有点昂贵,但每个要实现的接口只需要执行一次.结果是一个函数,它接受一个原子作为参数,并使用参数中指定的getter和setter来实现给定接口的原子周围的包装器get-set-map.(以这种方式编写,这比以前的方法灵活性差,但上面的大部分代码都可以在这里重用.)

这是一个示例界面和一个getter/setter映射:

(definterface IFunky
  (getFoo [])
  (^void setFoo [v])
  (getFunkyBar [])
  (^void setWeirdBar [v]))

(def gsm
  '{:foo [getFoo setFoo]
    :bar [getFunkyBar setWeirdBar]})
Run Code Online (Sandbox Code Playgroud)

和一些REPL互动:

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
Run Code Online (Sandbox Code Playgroud)