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
))
Senza riflessione? Quasi certamente no. –
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