2010-12-14 9 views
6

Estoy tratando de implementar una gran interfaz Java con numerosos (~ 50) métodos getter y setter (algunos con nombres irregulares). Pensé que sería bueno usar una macro para reducir la cantidad de código. Así que en lugar deUsar una macro clojure para crear automáticamente getters y setters dentro de una llamada de reificación

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

Quiero ser capaz de escribir

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

Es este conjunto-y-get macro (o algo similar) posible? No he podido hacer que funcione.

Respuesta

9

(Actualizado con un segundo enfoque - véase más adelante la segunda regla horizontal - así como algunas observaciones explicativas re: la primera.)


Me pregunto si esto podría ser un paso en la direcci ón correcta que la macro atom-bean pasa el valor de en tiempo de compilación de emit-atom-g&ss al reify-from-maps. Una vez que se compila un formulario particular atom-bean, los cambios posteriores a emit-atom-g&ss no tienen ningún efecto sobre el comportamiento del objeto creado.

Un ejemplo macroexpansion de la REPL (con algunos saltos de línea y indentación añade para mayor claridad):

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

Dos macroexpand-1 s son necesarios, porque atom-bean es una macro que se expande a una llamada macro más. macroexpand no sería particularmente útil, ya que ampliaría todo el camino hasta una llamada al reify*, el detalle de implementación detrás de reify.

La idea aquí es que puede suministrar un emit-map como emit-atom-g&ss anterior, con una clave de palabras cuyos nombres (en forma simbólica) activarán la generación de métodos mágicos en las llamadas reify-from-maps. La magia se realiza mediante las funciones almacenadas como funciones en el emit-map dado; los argumentos para las funciones son un mapa de "implicits" (básicamente toda y toda información que debe ser accesible para todas las definiciones de métodos en una forma reify-from-maps, como el nombre del átomo en este caso particular) seguido por los argumentos que se le dieron al "especificador de método mágico" en el formulario reify-from-maps. Como se mencionó anteriormente, reify-from-maps necesita ver una palabra clave real -> mapa de funciones, no su nombre simbólico; por lo tanto, solo es realmente utilizable con mapas literales, dentro de otras macros o con la ayuda de eval.

Las definiciones de métodos normales todavía se pueden incluir y se tratarán como en un formulario normal reify, siempre que las claves que coincidan con sus nombres no aparezcan en el emit-map. Las funciones de emisión deben devolver seqables (por ejemplo, vectores) de las definiciones de métodos en el formato esperado por reify: de este modo, el caso con múltiples definiciones de método devueltas para un "especificador de método mágico" es relativamente simple. Si el argumento iface se reemplazó por ifaces y ~iface con [email protected] en el cuerpo de reify-from-maps, se podrían especificar múltiples interfaces para la implementación.


Aquí hay otro enfoque, posiblemente más fáciles de razonar acerca de:

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

Este pide al compilador en tiempo de ejecución, que es un poco caro, pero sólo hay que hacer una vez al conjunto de interfaces para ser implementado. El resultado es una función que toma un átomo como argumento y reifica un contenedor alrededor del átomo que implementa las interfaces dadas con getters y setters como se especifica en el argumento get-set-map. (Escrito de esta manera, esto es menos flexible que el enfoque anterior, pero la mayor parte del código anterior podría ser reutilizado aquí.)

Aquí está una interfaz de muestra y un/mapa colocador captador:

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

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

Y algunos interacciones 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

Pensé que recordaba que Chouser demostró básicamente el mismo tipo de uso de 'eval' en SO y, por supuesto, [aquí está] (http://stackoverflow.com/questions/3748559/clojure-creating-new-instance-from -string-class-name/3752276 # 3752276). El escenario bajo consideración allí es diferente, pero su explicación del compromiso de desempeño involucrado es muy pertinente a la situación actual. –

+0

Wow. Gracias por la excelente respuesta. –

4

el punto es cosificar siendo una macro en sí que se expande antes de su puesta a punto y obtener macro - por lo que la puesta a punto y llegar enfoque no funciona. Entonces, en lugar de una macro interna dentro de reify, necesitas una macro en el "exterior" que genere la reificación también.

+0

Ese es un buen punto. Gracias. –

0

También puede tratar de 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

Desde el truco consiste en ampliar el cuerpo antes de Reify lo ve, una solución más general podría ser algo como lo siguiente:

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