2012-02-28 6 views
10

Sto scrivendo un wrapper Clojure per la libreria Braintree Java per fornire un'interfaccia più concisa e idiomatica. Mi piacerebbe per fornire funzioni di istanziare oggetti Java in modo rapido e conciso, come:Macro Clojure per chiamare setter Java in base a una mappa?

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

So che posso fare questo in modo esplicito, come mostrato nella this question:

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

Ma questo è ripetitivo per molte classi e diventa più complesso quando i parametri sono opzionali. Utilizzando riflessione, è possibile definire queste funzioni molto più conciso:

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

Ovviamente, mi piacerebbe evitare di riflessione, se possibile. Ho provato a definire una versione macro di set-obj-from-map ma il mio macro-fu non è abbastanza forte. Probabilmente richiede eval come spiegato here.

C'è un modo per chiamare un metodo Java specificato in fase di esecuzione, senza utilizzare la riflessione?

Grazie in anticipo!

soluzione Aggiornato:

Seguendo il consiglio di Joost, sono stato in grado di risolvere il problema utilizzando una tecnica simile. Una macro utilizza la riflessione al momento della compilazione per identificare i metodi setter della classe e quindi sputa i moduli per verificare il parametro in una mappa e chiamare il metodo con il suo valore.

Ecco la macro e un esempio l'uso:

; 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

Senza riflessione? Quasi certamente no. –

+1

Bene, è possibile tradurre una mappa in chiamate di metodo _ senza riflessione_ utilizzando una macro. Ho usato la riflessione solo dopo aver capito che la macro non poteva prendere un simbolo che regge la mappa, ma solo la mappa grezza stessa. Probabilmente avrei dovuto essere più chiaro affermando che mi piacerebbe evitare la riflessione su _runtime_, come descritto in @ joost-diepenmaat di seguito. – bkirkbri

risposta

8

È possibile utilizzare riflessione al momento della compilazione ~ fino a quando si conosce la classe si sta trattando per allora ~ per capire i nomi dei campi, e generare setter "statici" da quello. Ho scritto un codice che fa più o meno questo per i Getters qualche tempo fa che potresti trovare interessante. Vedere https://github.com/joodie/clj-java-fields (in particolare, la macro def-field in https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj).

+0

Questo è un approccio pulito. – amalloy

+0

Grazie, questo è stato davvero utile. Avevo bisogno di trasformare il mio approccio sottosopra e invece di prendere qualsiasi parametro della mappa, cercando di riflettere in fase di esecuzione, invece di enumerare i setter in fase di compilazione. Un ulteriore vantaggio è che vengono controllati solo parametri validi. – bkirkbri

1

La macro potrebbe essere semplice come:

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

Ma questo sarebbe rendere il vostro look codice come:

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

che immagino non è quello che si preferisce :)

+0

Vero, questa sintassi non è ideale e non può essere utilizzata dai chiamanti della funzione. Avresti comunque bisogno di mappare gli argomenti del chiamante in questa sintassi in qualche modo. – bkirkbri

+0

wow fantastico. questo è il motivo per cui amo imparare il clojure. – Core

Problemi correlati