2012-02-28 15 views
10

Piszę opakowanie Clojure dla biblioteki Java Braintree, aby zapewnić bardziej zwięzły i idiomatyczny interfejs. Chciałbym, aby zapewnić funkcje instancji obiektów Java szybko i zwięźle, jak:Clojure makro do wywoływania ustawiaczy Java na podstawie mapy?

(transaction-request :amount 10.00 :order-id "user42") 

wiem, że mogę to zrobić jawnie, jak pokazano na this question:

(defn transaction-request [& {:keys [amount order-id]}] 
    (doto (TransactionRequest.) 
    (.amount amount) 
    (.orderId order-id))) 

Ale to jest powtarzalny dla wiele klas i staje się bardziej złożona, gdy parametry są opcjonalne. Przy użyciu odbicia, to jest możliwe do zdefiniowania tych funkcji znacznie bardziej zwięźle:

(defn set-obj-from-map [obj m] 
    (doseq [[k v] m] 
    (clojure.lang.Reflector/invokeInstanceMethod 
     obj (name k) (into-array Object [v]))) 
    obj) 

(defn transaction-request [& {:as m}] 
    (set-obj-from-map (TransactionRequest.) m)) 

(defn transaction-options-request [tr & {:as m}] 
    (set-obj-from-map (TransactionOptionsRequest. tr) m)) 

Oczywiście, chciałbym, aby uniknąć refleksji, jeśli w ogóle możliwe. Próbowałem zdefiniować makro w wersji set-obj-from-map, ale moje makro-fu nie jest wystarczająco silne. Prawdopodobnie wymaga to eval zgodnie z wyjaśnieniami here.

Czy istnieje sposób wywołania metody Java określonej w środowisku wykonawczym, bez użycia odbicia?

Z góry dziękuję!

aktualizowane rozwiązanie:

Po poradę Joost, udało mi się rozwiązać ten problem stosując podobną technikę. Makro używa refleksji podczas kompilacji, aby zidentyfikować, które metody ustawiające ma klasa, a następnie wypluwa formularze w celu sprawdzenia paramu na mapie i wywołania metody z jej wartością.

Oto makro i przykład użycia:

; Find only setter methods that we care about 
(defn find-methods [class-sym] 
    (let [cls (eval class-sym) 
     methods (.getMethods cls) 
     to-sym #(symbol (.getName %)) 
     setter? #(and (= cls (.getReturnType %)) 
         (= 1 (count (.getParameterTypes %))))] 
    (map to-sym (filter setter? methods)))) 

; Convert a Java camelCase method name into a Clojure :key-word 
(defn meth-to-kw [method-sym] 
    (-> (str method-sym) 
     (str/replace #"([A-Z])" 
        #(str "-" (.toLowerCase (second %)))) 
     (keyword))) 

; Returns a function taking an instance of klass and a map of params 
(defmacro builder [klass] 
    (let [obj (gensym "obj-") 
     m (gensym "map-") 
     methods (find-methods klass)] 
    `(fn [~obj ~m] 
     [email protected](map (fn [meth] 
       `(if-let [v# (get ~m ~(meth-to-kw meth))] (. ~obj ~meth v#))) 
       methods) 
     ~obj))) 

; Example usage 
(defn transaction-request [& {:as params}] 
    (-> (TransactionRequest.) 
    ((builder TransactionRequest) params) 
    ; some further use of the object 
)) 
+0

Bez odbicie? Prawie na pewno nie. –

+1

Cóż, możliwe jest przetłumaczenie mapy na wywołania metod _ bez odbicia_ za pomocą makra. Użyłem tylko refleksji, gdy zdałem sobie sprawę, że makro nie może wziąć symbolu trzymającego mapę, a jedynie samą mapę surową. Prawdopodobnie powinienem być bardziej jasny, stwierdzając, że chciałabym uniknąć refleksji w _czasie_, jak @ joost-diepenmaat opisana poniżej. – bkirkbri

Odpowiedz

8

Można użyć refleksji w czasie kompilacji ~ tak długo, jak wiesz klasę masz do czynienia wtedy ~ dowiedzieć się nazwy pól, a z tego powodu generują "statyczne" setery. Napisałem trochę kodu, który robi to prawie dla getterów temu, co może ci się wydać interesujące. Zobacz https://github.com/joodie/clj-java-fields (w szczególności makro def def. W https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj).

+0

To jest schludne podejście. – amalloy

+0

Dzięki, to było naprawdę pomocne. Musiałem odwrócić moje podejście do góry nogami i zamiast wykonywać dowolne parametry map, a następnie próbować odzwierciedlić w czasie wykonywania, należy zamiast tego wyliczyć ustawiające w czasie kompilacji. Dodatkową korzyścią jest to, że sprawdzane są tylko poprawne parametry. – bkirkbri

1

Makro może być tak proste, jak:

(defmacro set-obj-map [a & r] `(doto (~a) [email protected](partition 2 r))) 

Ale byłoby to uczynić swój kod wyglądać tak:

(set-obj-map TransactionRequest. .amount 10.00 .orderId "user42") 

co chyba nie jest to, co wolisz :)

+0

Prawda, ta składnia nie jest idealna i nie może być używana przez osoby dzwoniące z twojej funkcji. Nadal będziesz musiał zmapować argumenty wywołującego do tej składni. – bkirkbri

+0

wow, niesamowite. właśnie dlatego uwielbiam uczyć się clojure. – Core