我正在尝试使用大量(~50)getter和setter方法(一些具有不规则名称)实现一个巨大的Java接口。我认为使用宏来减少代码量会很好。而不是
(def data (atom {:x nil}))
(reify HugeInterface
(getX [this] (:x @data))
(setX [this v] (swap! data assoc :x v)))
我希望能够写
(def data (atom {:x nil}))
(reify HugeInterface
(set-and-get getX setX :x))
这个set-and-get宏(或类似的东西)可能吗?我无法使它发挥作用。
答案 0 :(得分: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))
NB。 atom-bean
宏将emit-atom-g&ss
的实际编译时值传递给reify-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)))
两个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函数必须以reify
所期望的格式返回方法定义的seqables(例如向量):这样,为一个“魔术方法说明符”返回的多个方法定义的情况相对简单。如果在iface
'正文中ifaces
参数替换为~iface
而~@ifaces
替换为reify-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))])))))))
这在运行时调用编译器,这有点贵,但每个要实现的接口只需要执行一次。结果是一个函数,它接受一个原子作为参数,并使用get-set-map
参数中指定的getter和setter来实现给定接口的原子周围的包装器。 (以这种方式编写,这比以前的方法灵活性差,但上面的大部分代码都可以在这里重用。)
这是一个示例界面和一个getter / setter映射:
(definterface IFunky
(getFoo [])
(^void setFoo [v])
(getFunkyBar [])
(^void setWeirdBar [v]))
(def gsm
'{:foo [getFoo setFoo]
:bar [getFunkyBar setWeirdBar]})
和一些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
答案 1 :(得分:4)
重点是作为一个宏本身,它在你自己的set-and-get宏之前扩展 - 所以set-and-get方法不起作用。因此,在内部宏而不是reify中,你需要一个生成reify的“外部”宏。
答案 2 :(得分:0)
您也可以尝试force your macro to expand first:
(ns qqq (:use clojure.walk))
(defmacro expand-first [the-set & code] `(do ~@(prewalk #(if (and (list? %) (contains? the-set (first %))) (macroexpand-all %) %) code)))
(defmacro setter [setterf kw] `(~setterf [~'this ~'v] (swap! ~'data assoc ~kw ~'v)))
(defmacro getter [getterf kw] `(~getterf [~'this] (~kw @~'data)))
(expand-first #{setter getter}
(reify HugeInterface
(getter getX :x)
(setter setX :x)))
答案 3 :(得分:0)
由于诀窍是在 reify 看到之前扩展身体,因此更通用的解决方案可能是这样的:
(defmacro reify+ [& body]
`(reify ~@(map macroexpand-1 body)))