[英]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). 我正在尝试使用大量(~50)getter和setter方法(一些具有不规则名称)实现一个巨大的Java接口。 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? 这个set-and-get宏(或类似的东西)可能吗? I haven't been able to make it work. 我无法让它发挥作用。
(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. NB。 that the atom-bean
macro passes the actual compile-time value of emit-atom-g&ss
on to reify-from-maps
. atom-bean
宏将emit-atom-g&ss
的实际编译时值传递给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. 一旦编译了特定的atom-bean
表单,对emit-atom-g&ss
任何后续更改都不会影响所创建对象的行为。
An example macroexpansion from the REPL (with some line breaks and indentation added for clarity): 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)))
Two macroexpand-1
s are necessary, because atom-bean
is a macro which expands to a further macro call. 两个macroexpand-1
是必需的,因为atom-bean
是一个宏,它扩展到另一个宏调用。 macroexpand
would not be particularly useful, as it would expand this all the way to a call to reify*
, the implementation detail behind reify
. macroexpand
不会是特别有用,因为它会扩大这种一路调用reify*
,实现细节背后的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. 这里的想法是你可以提供像emit-atom-g&ss
一样的emit-map
,由关键字键控,其名称(符号形式)将在reify-from-maps
调用中触发魔术方法生成。 The magic is performed by the functions stored as functions in the given emit-map
; 魔术由存储为给定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. 函数的参数是一个“implicits”的映射(基本上是任何和所有信息都应该是reify-from-maps
形式的所有方法定义都可以访问,比如这个特殊情况下的原子名称)后面跟着哪个参数在reify-from-maps
的reify-from-maps
中给出了“魔术方法说明符”。 As mentioned above, reify-from-maps
needs to see an actual keyword -> function map, not its symbolic name; 如上所述, reify-from-maps
需要查看实际的关键字 - >功能图,而不是其符号名称; so, it's only really usable with literal maps, inside other macros or with help of eval
. 因此,它只适用于文字地图,其他宏内或借助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
. 正常的方法定义仍然可以包含在内,并且将被视为常规的reify
形式,前提是匹配其名称的键不会出现在emit-map
。 The emit functions must return seqables (eg 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. 所述发送功能必须在由期望的格式返回的方法定义seqables(例如载体) reify
:以这种方式,与多个方法定义的情况下返回一个“魔术方法指定符”是相对简单的。 If the iface
argument were replaced with ifaces
and ~iface
with ~@ifaces
in reify-from-maps
' body, multiple interfaces could be specified for implementation. 如果iface
参数用替换ifaces
和~iface
用~@ifaces
在reify-from-maps
的身体,多个接口可以为实现中指定。
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. 结果是一个函数,它接受一个原子作为参数,并使用get-set-map
参数中指定的getter和setter实现给定接口的原子周围的包装器。 (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: 这是一个示例界面和一个getter / setter映射:
(definterface IFunky
(getFoo [])
(^void setFoo [v])
(getFunkyBar [])
(^void setWeirdBar [v]))
(def gsm
'{:foo [getFoo setFoo]
:bar [getFunkyBar setWeirdBar]})
And some REPL interactions: 和一些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
The point is reify being a macro itself which is expanded before your own set-and-get macro - so the set-and-get approach doesn't work. 重点是作为一个宏本身,它在你自己的set-and-get宏之前扩展 - 所以set-and-get方法不起作用。 So, instead of an inner macro inside reify, you need a macro on the "outside" that generates the reify, too. 因此,在内部宏而不是内部宏中,你需要在“外部”上生成一个用于生成reify的宏。
Since the trick is to expand the body before reify sees it, a more general solution could be something along these lines: 由于诀窍是在reify看到它之前扩展身体,更通用的解决方案可能是这样的:
(defmacro reify+ [& body]
`(reify ~@(map macroexpand-1 body)))
You can also try to 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)))
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.