2010-12-14 6 views
6

Próbuję zaimplementować ogromny interfejs Java z licznymi (~ 50) metodami pobierającymi i ustawiającymi (niektóre z nieregularnymi nazwami). Pomyślałem, że byłoby miło użyć makra do zmniejszenia ilości kodu. Więc zamiastUżyj makaka clojure do automatycznego tworzenia obiektów pobierających i ustawiających wewnątrz wywołania reify

(def data (atom {:x nil})) 
(reify HugeInterface 
    (getX [this] (:x @data)) 
    (setX [this v] (swap! data assoc :x v))) 

Chcę móc napisać

(def data (atom {:x nil})) 
(reify HugeInterface 
    (set-and-get getX setX :x)) 

Jest to set-i-get makro (lub coś podobnego) możliwe? Nie byłem w stanie sprawić, żeby to działało.

Odpowiedz

9

(Aktualizacja z drugim podejściu - patrz poniżej drugiej linii poziomej - jak również kilka objaśniających re: pierwszy z nich.)


Zastanawiam się, czy to może być krokiem we właściwym kierunku:

(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. że makro atom-bean przechodzi rzeczywistą wartość czasu kompilacji z . Po skompilowaniu określonego formularza atom-bean wszelkie kolejne zmiany w emit-atom-g&ss nie mają wpływu na zachowanie utworzonego obiektu.

Przykład macroexpansion z REPL (z przerwami linii i wgłębienia dodaje się dla jasności)

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

Dwa macroexpand-1 S są konieczne, ponieważ atom-bean jest makro, który rozszerza się do dalszego makro połączenia. macroexpand nie byłby szczególnie przydatny, ponieważ rozszerzyłby to aż do połączenia z reify*, szczegółami implementacji za reify.

Pomysł polega na tym, że można dostarczyć powyższy kod , z kluczami utworzonymi przez słowa kluczowe, których nazwy (w formie symbolicznej) wywołają generowanie magicznej metody w wywołaniach reify-from-maps. Magia jest wykonywana przez funkcje przechowywane jako funkcje w danym emit-map; argumenty funkcji są mapą "implicits" (w zasadzie wszelkie informacje, które powinny być dostępne dla wszystkich definicji metod w formularzu reify-from-maps, jak nazwa atomu w tym konkretnym przypadku), a następnie niezależnie od tego, które argumenty zostały podane do "specyfikator metody magicznej" w formularzu reify-from-maps. Jak wspomniano powyżej, reify-from-maps musi zobaczyć rzeczywiste słowo kluczowe -> mapę funkcji, a nie jej symboliczną nazwę; tak naprawdę można go używać tylko w mapach literalnych, w innych makrach lub przy pomocy eval.

Normalne definicje metod mogą być nadal dołączane i będą traktowane jak zwykły formularz reify, pod warunkiem, że klucze odpowiadające ich nazwom nie występują w emit-map. Funkcje emitujące muszą zwracać seqables (np. Wektory) definicji metod w formacie oczekiwanym przez reify: w ten sposób przypadek z wieloma definicjami metod zwróconymi dla jednego "specyfikatora metody magicznej" jest stosunkowo prosty. Jeśli argument iface został zamieniony na ifaces i ~iface z ciałem [email protected] w ciele reify-from-maps, można było podać wiele interfejsów do implementacji.


Oto inne podejście, być może łatwiej rozumieć o:

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

Ten wzywa kompilator w czasie wykonywania, który jest nieco droższe, ale tylko należy zrobić raz na zestaw interfejsów być wdrożony. Wynikiem jest funkcja, która przyjmuje atom jako argument i rewiduje wrapper wokół atomu implementującego dane interfejsy z elementami pobierającymi i ustawiającymi, jak określono w argumencie get-set-map. (Napisany w ten sposób, to jest mniej elastyczny niż w poprzednim podejściu, ale większość kodu powyżej mogą być ponownie wykorzystane tutaj).

Oto interfejs próbki i getter/setter mapa:

(definterface IFunky 
    (getFoo []) 
    (^void setFoo [v]) 
    (getFunkyBar []) 
    (^void setWeirdBar [v])) 

(def gsm 
    '{:foo [getFoo setFoo] 
    :bar [getFunkyBar setWeirdBar]}) 

A niektóre rEPL interakcje:

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

Pomyślałem, że przypomniałem sobie, że Chouser demonstruje w zasadzie to samo użycie 'eval' na SO i, oczywiście, [tutaj jest] (http://stackoverflow.com/questions/3748559/clojure-creating-new-instance- from -string-class-name/3752276 # 3752276). Rozważany scenariusz jest inny, ale jego wyjaśnienie dotyczące kompromisu w zakresie wydajności jest bardzo istotne dla obecnej sytuacji. –

+0

Wow. Dziękuję za doskonałą odpowiedź. –

4

Chodzi o to zreifikować bycia makro sama, która jest rozszerzona przed własną set-i-get makro - więc ustaw i dostać podejście nie działa. Zamiast wewnętrznego repozytorium wewnętrznego, potrzebujesz makra na "zewnątrz", które również generuje reifikację.

+0

To jest dobra uwaga. Dzięki. –

0

Można także spróbować 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

Ponieważ Sztuką jest, aby rozwinąć ciało przed zreifikować widzi, bardziej ogólnie rozwiązaniem mogłoby być coś wzdłuż tych linii:

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