2010-12-14 43 views
6

我試圖用許多(〜50)getter和setter方法(一些不規則的名字)實現一個巨大的Java接口。我認爲使用宏來減少代碼量會很好。因此,而不是使用clojure宏來自動創建getify和setter在一個reify調用中

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

我希望能寫

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

是這種設置和獲取宏(或類似的東西)可能嗎?我一直無法使它工作。

回答

9

(用第二種方法更新 - 見第二水平線以下 - 以及一些解釋性說明重新:第一個。)


我不知道這可能是一個一步在正確的方向:

(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。 atom-bean宏將emit-atom-g&ss的實際編譯時間的值傳遞到reify-from-maps。一旦編譯了特定的atom-bean表單,對emit-atom-g&ss的任何後續更改都不會影響創建的對象的行爲。

從REPL一個例子宏擴展(有一些行和縮進添加爲清楚起見):

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

兩個macroexpand-1 s爲必要的,因爲atom-bean是其擴展到另外的宏調用的宏。 macroexpand將不會特別有用,因爲它會一直擴展到致電reify*reify背後的實現細節)。

這裏的想法是,你可以提供一個emit-map像上面emit-atom-g&ss,通過他們的名字(符號形式)將觸發reify-from-maps調用魔術方法產生的關鍵字關鍵字。神奇是由在給定的emit-map中存儲爲函數的函數執行的;這些函數的參數是一個「implicits」的映射(基本上任何和所有的信息都應該可以通過一個reify-from-maps表單中的所有方法定義來訪問,就像這個特定情況下的atom的名字一樣),接下來是哪個參數被賦予「魔術方法說明符」在reify-from-maps表單中。如上所述,reify-from-maps需要看到一個實際的關鍵字 - >功能圖,而不是它的符號名;所以,它只能在文字地圖,其他宏或eval的幫助下使用。

正常的方法定義仍然可以包含,並將按照正規的reify表格的形式處理,只要在emit-map中不出現與其名稱匹配的名稱。 emit函數必須按照reify所期望的格式返回方法定義的seqable(例如向量):這樣,對於一個「magic方法說明符」返回多個方法定義的情況相對比較簡單。如果iface參數被ifaces~iface替換爲reify-from-maps[email protected],則可以指定多個接口來實現。


這裏的另一種方法,可能更容易推理:

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

這就要求編譯器在運行時,這是有點貴,但只需要每一套做一次的接口是實現。結果是一個函數,它將一個原子作爲參數,並根據參數get-set-map中的規定,使用getter和setter實現給定接口的原子的包裝器。 (寫這樣一來,這比以前的方法靈活,但最上面的代碼在這裏可以重複使用。)

這裏有一個樣品接口和一個getter/setter方法地圖:

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

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

有些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

我以爲我記得Chouser在SO上演示基本上同樣的'eval'的使用,當然,[這是它](http://stackoverflow.com/questions/3748559/clojure-creating-new-instance-from -string類名/ 3752276#3752276)。所考慮的情況有所不同,但他對性能權衡的解釋與當前情況非常相關。 – 2010-12-14 07:30:27

+0

哇。謝謝你的出色答案。 – 2010-12-15 06:13:25

4

點是具體化是它之前你自己的擴展宏本身設置和獲取宏觀 - 這樣的設置和獲取方法是行不通的。所以,你不需要在reify裏面有一個內部的宏,而是需要在外部生成reify的宏。

+0

這是一個很好的觀點。謝謝。 – 2010-12-15 06:14:30

0

您也可以嘗試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

由於關鍵是要擴大身體之前具體化看來,一個更通用的解決方案可能是沿着這些路線的東西:

(defmacro reify+ [& body] 
    `(reify [email protected](map macroexpand-1 body))) 
相關問題