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

Posted

技术标签:

【中文标题】使用 clojure 宏在 reify 调用中自动创建 getter 和 setter【英文标题】:Use a clojure macro to automatically create getters and setters inside a reify call 【发布时间】:2011-05-25 00:13:28 【问题描述】:

我正在尝试使用众多(约 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 宏(或类似的东西)可能吗?我一直无法让它工作。

【问题讨论】:

【参考方案1】:

(更新了第二种方法——见下面的第二条横线——以及一些解释性评论:第一种。)


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

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

注意。 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-1s 是必需的,因为atom-bean 是一个扩展为进一步宏调用的宏。 macroexpand 不会特别有用,因为它将一直扩展为对 reify* 的调用,reify 背后的实现细节。

这里的想法是,您可以像上面的emit-atom-g&ss 一样提供emit-map,其名称(以符号形式)将在reify-from-maps 调用中触发魔术方法生成。魔术是由给定emit-map 中存储为函数的函数执行的;函数的参数是“隐式”的映射(基本上是reify-from-maps 形式的所有方法定义应该可以访问的任何和所有信息,例如在这种特殊情况下的原子名称),然后是给出的任何参数reify-from-maps 表单中的“魔术方法说明符”。如上所述,reify-from-maps 需要看到一个实际的关键字 -> 函数映射,而不是它的符号名称;所以,它只适用于文字映射、其他宏内部或eval 的帮助。

普通方法定义仍然可以包括在内,并将被视为普通的reify 形式,前提是与其名称匹配的键不会出现在emit-map 中。发射函数必须以reify 期望的格式返回方法定义的 seqables(例如向量):这样,为一个“魔术方法说明符”返回多个方法定义的情况相对简单。如果reify-from-maps'正文中的iface 参数被ifaces~iface 替换为~@ifaces,则可以指定多个接口来实现。


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

(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

【讨论】:

我想我记得 Chouser 在 SO 和 here it is 上演示了基本相同的 eval 用法。考虑的场景有所不同,但他对所涉及的性能权衡的解释与当前情况非常相关。【参考方案2】:

关键是 reify 本身是一个宏,它在您自己的 set-and-get 宏之前扩展 - 所以 set-and-get 方法不起作用。因此,您需要在“外部”上也有一个宏来生成 reify,而不是在 reify 内部使用内部宏。

【讨论】:

【参考方案3】:

你也可以试试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)))

【讨论】:

【参考方案4】:

由于诀窍是在 reify 看到之前扩展主体,因此更通用的解决方案可能是这样的:

(defmacro reify+ [& body]
  `(reify ~@(map macroexpand-1 body)))

【讨论】:

以上是关于使用 clojure 宏在 reify 调用中自动创建 getter 和 setter的主要内容,如果未能解决你的问题,请参考以下文章

在 Clojure 中模拟现有方法

如何在 Clojure 中对函数进行基准测试?

在 Clojure 中扩展 Java 接口时出错

在 Ktor 中使用 kotlin Reified 进行通用 api 调用

在绑定向量中注释 Clojure

@reify 每次调用都会执行数据库查询?