2010-12-14 1 views
6

Я пытаюсь реализовать огромный интерфейс Java с многочисленными методами (~ 50) getter и setter (некоторые с неправильными именами). Я подумал, что было бы неплохо использовать макрос, чтобы уменьшить количество кода. Таким образом, вместоИспользуйте макрос clojure, чтобы автоматически создавать геттеры и сеттеры внутри вызова reify.

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

Является ли этот набор-и-получить макрос (или что-то подобное) возможно? Я не смог заставить его работать.

ответ

9

(Обновлено со вторым подходом - см ниже второго горизонтального положения - а также некоторые пояснительные замечания повторно: первый.)


Интересно, если это может быть шагом в правильном направлении:

(defmacro reify-from-maps [iface implicits-map emit-map & ms] 
    `(reify ~iface 
    [email protected](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 [email protected])) 

NB. что макрос atom-bean передает фактическое время компиляции значение от emit-atom-g&ss по reify-from-maps. Как только форма atom-bean скомпилирована, любые последующие изменения в emit-atom-g&ss не влияют на поведение созданного объекта.

Пример macroexpansion из РЕПЛ (с некоторыми разрывами строк и отступами добавлен для ясности):

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. Испускающие функции должны возвращать значения (например, векторы) определений методов в формате, ожидаемом reify: таким образом случай с несколькими определениями методов, возвращенный для одного «спецификатора магического метода», относительно прост. Если аргумент iface был заменен на ifaces и ~iface с [email protected] в корпусе reify-from-maps, для реализации можно было бы указать несколько интерфейсов.


Вот другой подход, возможно, легче рассуждать о:

(defn compile-atom-bean-converter [ifaces get-set-map] 
    (eval 
    (let [asym (gensym)] 
    `(fn [~asym] 
     (reify [email protected] 
      [email protected](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 
+0

Я думал, что я помню, как Чаусер демонстрировал в основном такое же использование 'eval' на SO и, конечно же, [здесь это] (http://stackoverflow.com/questions/3748559/clojure-creating-new-instance-from -string класса имя/3752276 # 3752276). Рассматриваемый сценарий отличается, но его объяснение связанного с производительностью компромисса очень уместно для нынешней ситуации. –

+0

Ничего себе. Спасибо за отличный ответ. –

4

Дело в том, материализовать будучи сам макрос, который раскрывается перед собственный набор-и-получить макро - так установил и получить подход не работает. Таким образом, вместо внутреннего макроса внутри reify вам нужен макрос «снаружи», который также генерирует reify.

+0

Это хороший момент. Благодарю. –

0

Вы также можете попробовать force your macro to expand first:

(ns qqq (:use clojure.walk)) 
(defmacro expand-first [the-set & code] `(do [email protected](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))) 
0

Поскольку Хитрость заключается в том, чтобы расширить тело, прежде чем материализовать видит, более общее решение может быть что-то вдоль этих линий:

(defmacro reify+ [& body] 
    `(reify [email protected](map macroexpand-1 body)))