(Обновлено со вторым подходом - см. Ниже второе горизонтальное правило - а также некоторые пояснительные замечания по поводу: первого.)
Интересно, может ли это быть шагом в правильном направлении:
(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
; аргументы функций - это карта «последствий» (в основном любая и вся информация, которая должна быть доступна для всех определений методов в форме reify-from-maps
, например, имя атома в данном конкретном случае), за которой следуют любые аргументы к «спецификатору магического метода» в форме reify-from-maps
. Как упомянуто выше, reify-from-maps
должно видеть фактическое ключевое слово -> карту функции, а не ее символическое имя; таким образом, он действительно может использоваться только с литеральными картами, внутри других макросов или с помощью eval
.
Обычные определения методов все еще могут быть включены и будут обрабатываться как в обычной форме reify
, при условии, что ключи, соответствующие их именам, не встречаются в emit-map
. Функции emit должны возвращать секвенции (например, векторы) определений методов в формате, ожидаемом reify
: таким образом, случай с множественными определениями методов, возвращенными для одного «спецификатора магического метода», относительно прост. Если аргумент 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
. (Написанный таким образом, он менее гибок, чем предыдущий подход, но большая часть кода выше может быть использована здесь снова.)
Вот пример интерфейса и карта получения / установки:
(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