2010-12-14 13 views
6

Sto cercando di implementare un'enorme interfaccia Java con numerosi metodi (~ 50) getter e setter (alcuni con nomi irregolari). Ho pensato che sarebbe stato carino usare una macro per ridurre la quantità di codice. Così, invece diUtilizzare una macro clojure per creare automaticamente getter e setter all'interno di una chiamata reify

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

voglio essere in grado di scrivere

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

Is questo set-and-get macro (o qualcosa di simile) possibile? Non sono stato in grado di farlo funzionare.

risposta

9

(Aggiornato con un secondo approccio - vedi sotto la seconda regola orizzontale - così come alcune osservazioni esplicativi re: il primo.)


Mi chiedo se questo potrebbe essere un passo nella giusta direzione:

(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. che la macro atom-bean passi il valore effettivo di compilazione di emit-atom-g&ss a reify-from-maps. Una volta compilato un particolare modulo atom-bean, qualsiasi modifica successiva a emit-atom-g&ss non ha alcun effetto sul comportamento dell'oggetto creato.

Un esempio macroexpansion dal REPL (con alcune interruzioni di linea e rientro aggiunti per chiarezza):

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

Due macroexpand-1 s sono necessari perché atom-bean è una macro che si espande ad un ulteriore invito macro. macroexpand non sarebbe particolarmente utile, in quanto si espanderebbe fino alla chiamata a reify*, i dettagli di implementazione dietro reify.

L'idea è che è possibile fornire uno emit-map come emit-atom-g&ss sopra, digitato da parole chiave i cui nomi (in forma simbolica) attiveranno la generazione del metodo magico nelle chiamate reify-from-maps. La magia viene eseguita dalle funzioni memorizzate come funzioni nel dato emit-map; gli argomenti alle funzioni sono una mappa di "impliciti" (in pratica qualsiasi e tutte le informazioni che dovrebbero essere accessibili a tutte le definizioni dei metodi in un modulo reify-from-maps, come il nome dell'atomo in questo caso particolare) seguite da qualsiasi argomento è stato dato al "specificatore del metodo magico" nel modulo reify-from-maps. Come accennato in precedenza, reify-from-maps deve vedere una parola chiave effettiva -> mappa delle funzioni, non il suo nome simbolico; quindi, è utilizzabile solo con mappe letterali, all'interno di altre macro o con l'aiuto di eval.

Le definizioni dei metodi normali possono ancora essere incluse e verranno trattate come in un normale modulo reify, a condizione che le chiavi corrispondenti ai loro nomi non si verifichino nello emit-map. Le funzioni di emit devono restituire le sequenze (ad esempio i vettori) delle definizioni dei metodi nel formato previsto da reify: in questo modo, il caso con più definizioni di metodo restituite per un "identificatore del metodo magico" è relativamente semplice. Se l'argomento iface è stato sostituito con ifaces e ~iface con [email protected] nel corpo reify-from-maps, è possibile specificare più interfacce per l'implementazione.


Ecco un altro approccio, forse più facile ragionare su:

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

Questo invita il compilatore in fase di esecuzione, che è un po 'costoso, ma ha solo bisogno di essere fatto una volta per set di interfacce di essere implementato. Il risultato è una funzione che accetta un atomo come argomento e reifica un wrapper attorno all'atomo implementando le interfacce fornite con getter e setter come specificato nell'argomento get-set-map. (Scritto in questo modo, questo è meno flessibile rispetto all'approccio precedente, ma la maggior parte del codice di cui sopra potrebbe essere riutilizzato qui.)

Ecco un'interfaccia campione e un/map setter getter:

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

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

E alcuni interazioni 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

Pensavo di aver ricordato Chouser dimostrando fondamentalmente lo stesso tipo di uso di "eval" su SO e, abbastanza sicuro, [eccolo qui] (http://stackoverflow.com/questions/3748559/clojure-creating-new-instance-from -string-class-name/3752276 # 3752276). Lo scenario preso in considerazione è diverso, ma la sua spiegazione del compromesso in termini di prestazioni è molto pertinente alla situazione attuale. –

+0

Wow. Grazie per l'eccellente risposta. –

4

il punto è reificare essere una macro stessa che si espande prima il proprio set-and-get macro - in modo che il set-and-get approccio non funziona. Quindi, invece di una macro interna dentro reify, hai bisogno di una macro sul "fuori" che genera anche la reify.

+0

Questo è un buon punto. Grazie. –

0

si può anche provare a 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

Dal momento che il trucco è quello di espandere il corpo prima reificare lo vede, una soluzione più generale potrebbe essere qualcosa in queste righe:

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