A little exercise with clojure macro

825 views

Published on

Presented at clj-syd meetup on 27 Sep 2012. Sharing a practical experience of creating a macro for generalize codes for java interop.

  • Be the first to comment

A little exercise with clojure macro

  1. 1. A little exercise with Macro Zehua Liu
  2. 2. Prerequisites• Basic macro (defmacro mywhen [cond-exp & clauses] `(if ~cond-exp (do ~@clauses)))• Java interop (String. "a string") (.substring "a string" 0 5)
  3. 3. Motivation• A script to test a server with a request /response protocol• To send a request to the server, reuse Java classes that represent requests in server code
  4. 4. Java Request Classes
  5. 5. Java Request Classes
  6. 6. Sending Login Request
  7. 7. Sending SendMessage Request
  8. 8. Sending Requests• Many other types of requests• Lots of duplicate codes• Refactor them!• function?
  9. 9. Refactor using function(defn send-request [req-pkt sid] (let [[new-sid resp-pkts] (send-req req-pkt sid)] (if-let [err-msg (check-resp-error resp-pkts)] (do (println (format "ERROR: %s failed, sid=%s,req=%s,err=%s" (.getName req-pkt) sid req-pkt err-msg)) [nil nil]) [new-sid resp-pkts])))
  10. 10. Refactor using function
  11. 11. Refactor using function• Not too bad• Can we do better / differently?• macro?
  12. 12. Login Request Again
  13. 13. Make it work for Login Request
  14. 14. Make it work for Login Request(defmacro send-request [request username password sid] `(let [req-pkt# (doto (requests.LoginRequest.) (.setUsername ~username) (.setPassword ~password)) [new-sid# resp-pkts#] (send-req req-pkt# ~sid)] (if-let [err-msg# (check-resp-error resp-pkts#)] (do (println (format "ERROR: %s failed, sid=%s,req=%s,err=%s" ~(name request) ~sid req-pkt# err-msg#)) [nil nil]) [new-sid# resp-pkts#])))(defn login [:Login username password sid] (send-request username password sid))
  15. 15. Make it work for Login Request
  16. 16. Make it work for Login Request(defmacro send-request [request username password sid]`(let [req-pkt# (doto (new ~(symbol (str "requests." (name request) "Request"))) (.setUsername ~username) (.setPassword ~password)) [new-sid# resp-pkts#] (send-req req-pkt# ~sid)] (if-let [err-msg# (check-resp-error resp-pkts#)] (do (println (format "ERROR: %s failed, sid=%s,req=%s,err=%s" ~(name request) ~sid req-pkt# err-msg#)) [nil nil]) [new-sid# resp-pkts#])))(defn login [username password sid] (send-request :Login username password sid))
  17. 17. Make it work for Login Request(.setUsername ~username) <==> (. setUsername ~username)(.setUsername ~username) <==>(. (symbol (str "set" (name :Username))) ~username)(.setUsername ~username)(.setPassword ~password) <==>~@(map (fn [[pn pv]] `(. ~(symbol (str "set" (name pn))) ~pv)) {:Username username :Password password})
  18. 18. Make it work for Login Request(defmacro send-request [request param-map sid] `(let [req-pkt# (doto (new ~(symbol (str "requests." (name request) "Request"))) ~@(map (fn [[pn pv]] `(. ~(symbol (str "set" (name pn))) ~pv)) param-map)) [new-sid# resp-pkts#] (send-req req-pkt# ~sid)] (if-let [err-msg# (check-resp-error resp-pkts#)] (do (println (format "ERROR: %s failed, sid=%s,req=%s,err=%s" ~(name request) ~sid req-pkt# err-msg#)) [nil nil]) [new-sid# resp-pkts#])))(defn login [username password sid] (send-request :Login {:Username username :Password password} sid))
  19. 19. Refactor using macro(defn login [username password sid] (send-request :Login {:Username username :Password password} sid))(defn send-message [message dest-username type sid] (send-request :SendMessage {:Message message :DestUsername dest-username :Type type} sid))
  20. 20. Refactor using macro• It works! Hooray!• Let’s use it for more fancy stuff.• Optional request fields?• On server side, SendMessage type default to 1, if not specified
  21. 21. Optional field(defn send-message [message dest-username type sid] (send-request :SendMessage {:Message message :DestUsername dest-username :Type type} sid))(defn send-message [message dest-username sid] (send-request :SendMessage {:Message message :DestUsername dest-username} sid))
  22. 22. Optional field(defn send-message ([message dest-username sid] (send-message message dest-username nil sid)) ([message dest-username type sid] (let [param-map-base {:Message message :DestUsername dest-username} param-map (if type (assoc param-map-base :Type type) param-map-base)] (send-request :SendMessage param-map sid))))
  23. 23. Optional field, FAILED(defn send-message ([message dest-username sid] (send-message message dest-username nil sid)) ([message dest-username type sid] (let [param-map-base {:Message message :DestUsername dest-username} param-map (if type (assoc param-map-base :Type type) param-map-base)] (send-request :SendMessage param-map sid))))CompilerException java.lang.IllegalArgumentException: Dontknow how to create ISeq from: clojure.lang.Symbol, compiling:(NO_SOURCE_PATH:10)
  24. 24. Optional field, FAILED
  25. 25. Optional field, FAILED• macro is evaluated at compile time, not at run time• macro evaluator only knows about symbols – {:Username username :Password password} is a map (keywords to symbols) – But param-map is a symbol – At compile time, macro evaluator does not know the run time value that the symbol param-map represents• How do we fix it?
  26. 26. Optional field, Fixing it• How do we fix it?• One thought: – Tell the macro the complete list of fields, and have it generate codes like below for every field:(if-let [v (:Type param-map)] (. setType v))• And then param-map can be a symbol whose value macro evalutor does not need to know, its value is only needed at run time.
  27. 27. Optional field, Fixing it(defmacro send-request [request param-list param-map sid] (let [param-map# param-map r (gensym)] `(let [req-pkt# (let [~r (new ~(symbol (str "requests." (name request) "Request")))] ~@(map (fn [pn] `(if-let [pv# (~pn ~param-map#)] (. ~r ~(symbol (str "set" (name pn))) pv#))) param-list)) [new-sid# resp-pkts#] (send-req req-pkt# ~sid)] (if-let [err-msg# (check-resp-error resp-pkts#)] (do (println (format "ERROR: %s failed, sid=%s,req=%s,err=%s" ~(name request) ~sid req-pkt# err-msg#)) [nil nil]) [new-sid# resp-pkts#]))))
  28. 28. Optional field, Fixing it(defn login [username password sid] (send-request :Login [:Username :Password] {:Username username :Password password} sid))(defn send-message ([message dest-username sid] (send-message message dest-username nil sid)) ([message dest-username type sid] (let [param-map-base {:Message message :DestUsername dest-username} param-map (if type (assoc param-map-base :Type type) param-map-base)] (send-request :SendMessage [:Message :DestUsername :Type] param-map sid))))
  29. 29. Optional field, Fixing it(macroexpand (send-request :SendMessage [:Message :DestUsername :Type] param-map nil))(let* [req-pkt__625__auto__ (clojure.core/let [G__671 (new requests.SendMessageRequest)] (clojure.core/if-let [pv__624__auto__ (:Message param-map)] (. G__671 setMessage pv__624__auto__)) (clojure.core/if-let [pv__624__auto__ (:DestUsername param-map)] (. G__671 setDestUsername pv__624__auto__)) (clojure.core/if-let [pv__624__auto__ (:Type param-map)] (. G__671 setType pv__624__auto__))) vec__672 (scratch.core/send-req req-pkt__625__auto__ nil) new-sid__626__auto__ (clojure.core/nth vec__672 0 nil) resp-pkts__627__auto__ (clojure.core/nth vec__672 1 nil)] (clojure.core/if-let [err-msg__628__auto__ (scratch.core/check-resp-error resp-pkts__627__auto__)] (do (clojure.core/println (clojure.core/format "ERROR: %s failed, sid=%s,req=%s,err=%s" "SendMessage" nil req-pkt__625__auto__ err-msg__628__auto__)) [nil nil]) [new-sid__626__auto__ resp-pkts__627__auto__]))
  30. 30. Lessons Learned• macro is evaluated at compile time, not at run time• macro evaluator treats code as data – {:Username username :Password password} is a map of keywords to symbols, not keywrods to strings (or whatever type username/password might be) – But param-map is a symbol• At compile time, macro evaluator treats ref/var as symbol, knowing nothing about their run time values

×