(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
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. –
Wow. Grazie per l'eccellente risposta. –