2010-12-14 5 views
6

J'essaye d'implémenter une énorme interface Java avec de nombreuses méthodes getter et setter (~ 50) (certaines avec des noms irréguliers). J'ai pensé que ce serait bien d'utiliser une macro pour réduire la quantité de code. Ainsi, au lieu deUtilisez une macro clojure pour créer automatiquement des getters et des setters dans un appel reify

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

Je veux être capable d'écrire

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

Est-ce set-et-get macro (ou quelque chose de similaire) possible? Je n'ai pas réussi à le faire fonctionner.

Répondre

9

(Mise à jour avec une seconde approche - voir ci-dessous la deuxième règle horizontale - ainsi que quelques explications re: la première.)


Je me demande si cela pourrait être une étape dans la bonne direction:

(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. que la macro atom-bean transmet la valeur réelle à la compilation de emit-atom-g&ss sur reify-from-maps. Une fois qu'une forme particulière atom-bean est compilée, toutes les modifications suivantes apportées à emit-atom-g&ss n'ont aucun effet sur le comportement de l'objet créé.

Un exemple macroexpansion de la REMP (avec quelques sauts de ligne et indentation ajouté pour plus de clarté):

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

Deux macroexpand-1 s sont nécessaires, car atom-bean est une macro qui se développe à un autre appel macro. macroexpand ne serait pas particulièrement utile, car il s'étendrait à un appel à reify*, le détail de l'implémentation derrière reify.

L'idée ici est que vous pouvez fournir un emit-map comme emit-atom-g&ss ci-dessus, saisi par des mots-clés dont les noms (sous forme symbolique) déclencheront la génération de méthode magique en reify-from-maps appels. La magie est effectuée par les fonctions stockées en tant que fonctions dans le emit-map donné; les arguments aux fonctions sont une carte de "implicits" (essentiellement toutes les informations qui devraient être accessibles à toutes les définitions de méthodes dans un formulaire reify-from-maps, comme le nom de l'atome dans ce cas particulier) suivi par les arguments qui ont été donnés au "Spécificateur de méthode magique" dans le formulaire reify-from-maps. Comme mentionné ci-dessus, reify-from-maps doit voir un mot-clé réel -> carte de fonction, pas son nom symbolique; donc, il est seulement vraiment utilisable avec des cartes littérales, à l'intérieur d'autres macros ou avec l'aide de eval.

Les définitions de méthode normales peuvent toujours être incluses et seront traitées comme dans un formulaire reify normal, à condition que les clés correspondant à leur nom ne se trouvent pas dans le emit-map. Les fonctions emit doivent renvoyer des seqables (par exemple des vecteurs) de définitions de méthode au format attendu par reify: de cette manière, le cas avec plusieurs définitions de méthodes renvoyées pour un "spécificateur de méthode magique" est relativement simple. Si l'argument iface a été remplacé par ifaces et ~iface avec [email protected] dans le corps reify-from-maps, plusieurs interfaces peuvent être spécifiées pour l'implémentation.


Voici une autre approche, peut-être plus facile de raisonner sur:

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

Cela appelle le compilateur lors de l'exécution, ce qui est un peu cher, mais ne doit être fait une fois par ensemble d'interfaces pour être mis en œuvre. Le résultat est une fonction qui prend un atome comme argument et réifie un wrapper autour de l'atome implémentant les interfaces données avec les getters et setters comme spécifié dans l'argument get-set-map. (Écrit de cette façon, ce qui est moins souple que la précédente approche, mais la plupart du code ci-dessus pourrait être réutilisé ici.)

Voici une interface d'échantillon et un getter/carte setter:

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

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

Et certains interactions 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

Je pensais que je me souvenais de Chouser démontrant fondamentalement le même genre d'utilisation de 'eval' sur SO et, bien sûr, [ici c'est] (http://stackoverflow.com/questions/3748559/clojure-creating-new-instance-from -string-class-name/3752276 # 3752276). Le scénario à l'étude est différent, mais son explication du compromis de performance impliqué est très pertinente par rapport à la situation actuelle. –

+0

Wow. Merci pour l'excellente réponse. –

4

le point est réifier être une macro elle-même qui est détendu avant votre propre jeu et-get macro - de sorte que le jeu et-get approche ne fonctionne pas. Donc, au lieu d'une macro interne à l'intérieur de reify, vous avez besoin d'une macro sur le "dehors" qui génère la reify, aussi.

+0

C'est un bon point. Merci. –

0

Vous pouvez également essayer 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

Depuis l'astuce consiste à développer le corps avant réifier voit, une solution plus générale pourrait être quelque chose le long de ces lignes:

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