Skip to content

Instantly share code, notes, and snippets.

@zjhmale
Last active September 8, 2015 04:17
Show Gist options
  • Save zjhmale/8fb761d27de4c4ec2e5c to your computer and use it in GitHub Desktop.
Save zjhmale/8fb761d27de4c4ec2e5c to your computer and use it in GitHub Desktop.
something about clojure macro
(let [expression (read-string "(+ 1 2 3 4 5)")]
(cons (read-string "*")
(rest expression)))
;;=> (* 1 2 3 4 5)
(eval *1)
;;=> 120
(let [expression '(+ 1 2 3 4 5)]
(cons '* (rest expression)))
;;=> (* 1 2 3 4 5)
(eval *1)
;;=> 120
;;来看下为什么需要有macro的存在
(defn print-with-asterisks [printable-argument]
(print "*****")
(print printable-argument)
(println "*****"))
;;这是一个普通的函数 参数在被传入函数体之前就已经被求值了可以通过一个例子来验证
(print-with-asterisks "hi")
;;*****hi*****
;;=> nil
(print-with-asterisks
(do (println "in argument expression")
"hi"))
;;in argument expression
;;*****hi*****
;;=> nil
;;可以看到函数是对传入的参数先求值再传入函数体进行后续的求值操作的 如果想让"in argument expression"这句话的打印操作在两行星号之间那么就需要阻止传入参数的求值知道运行期再执行 就要用到宏了
(defmacro print-with-asterisks [printable-argument]
(list 'do (list 'print "*****")
(list 'print printable-argument)
(list 'println "*****")))
(print-with-asterisks
(do (println "in argument expression")
"hi"))
;;*****in argument expression
;;hi*****
;;=> nil
(clojure.pprint/pprint
(macroexpand
'(print-with-asterisks
(do (println "in argument expression")
"hi"))))
(do
(print "*****")
(print (do (println "in argument expression") "hi"))
(println "*****"))
;;=> nil
;;所以希望传入的参数不被求值 就只能在runtime的时候被求值了 也就是macro的意义所在
;;clojure.core/cond的实现
(defmacro cond
"Takes a set of test/expr pairs. It evaluates each test one at a
time. If a test returns logical true, cond evaluates and returns
the value of the corresponding expr and doesn't evaluate any of the
other tests or exprs. (cond) returns nil."
{:added "1.0"}
[& clauses]
(when clauses
(list 'if (first clauses)
(if (next clauses)
(second clauses)
(throw (IllegalArgumentException.
"cond requires an even number of forms")))
(cons 'clojure.core/cond (next (next clauses))))))
;;一个一开始比较疑惑的点就是为什么外面的需要用(list 'if ...)里面需要用(if ...)
;;(list 'if ...)是将代码当做数据结构操作生成运行时运行的代码
;;(if ...)这些都是编译期运行的代码是先执行的代码(next ...) (first ...) (cons ...)这些都是编译期运行的代码将先于(list 'if ...)这样的运行时代码执行
;;那么为什么要这样呢
;;因为宏的作用就是不想让你传入的form在编译期的时候就被求值而是可以在运行时才被求值从而可以做一些运行时的黑魔法重新生成新的AST
;;比如(< n 0)这样的form我如果不想它在编译期就被求值为一个确定的值那么就要用到macro将其放到运行时再运行
;;如果你乐意,可以把(< n 0)这种东西当string看,就是对于编译期来说就是个符号Macro就是用来生成这些可以运行的符号的,然后在runtime对这些编译期生成好的符号再求值
;;看一个例子
(clojure.pprint/pprint
(macroexpand '(cond
(< n 0) "negative"
(> n 0) "positive"
:else "zero")))
(if
(< n 0)
"negative"
(clojure.core/cond (> n 0) "positive" :else "zero"))
;;可以看到(< n 0)这个form就被保留了没有被求值(list 'if ...)也被完整的保留了,而其他(if ...) (cons ...) (first ...)就都在编译期时就被求值了
;;所以在macro中所有编译期行为只是为了重新组成一个新的form使其能在运行时运行也就是动态改写AST的魔力
;;这个macro中还有一个陷阱就是按照上面那个例子那么传入的clauses就是
'((< n 0) "negative"
(> n 0) "positive"
:else "zero")
;;那么
(first clauses) => '(< n 0) ;;请不要误以为是'(< n 0) "negative" 这是一个比较容易弄错的点
;;所以根据上面的那个例子可以看到macro中所有在编译期执行的代码会将被quote住的其他代码当做普通的数据结构那样来操作等执行完就得到一个重新生成的AST或者说是form从而在运行期执行求值得到最终的结果
;;difference between macroexpand-1 and macroexpand
;;if we have a macro that expands to another macro call, macroexpand will do that second expansion for us, but macroexpand-1 will not
(clojure.pprint/pprint
(macroexpand-1 '(when-falsy (= 1 2) (println "hi!"))))
(when (not (= 1 2)) (do (println "hi!")))
;;=> nil
(clojure.pprint/pprint
(macroexpand '(when-falsy (= 1 2) (println "hi!"))))
(if (not (= 1 2)) (do (do (println "hi!"))))
;;=> nil
(def a 4)
'(1 2 3 a 5)
;;=> (1 2 3 a 5)
(list 1 2 3 a 5)
;;=> (1 2 3 4 5)
`(1 2 3 a 5)
;;=> (1 2 3 user/a 5)
`(1 2 3 ~a 5)
;;=> (1 2 3 4 5)
`(1 2 3 '~a 5)
;;=> (1 2 3 (quote 4) 5)
`(1 2 3 (quote (unquote a)) 5)
;;=> (1 2 3 (quote (clojure.core/unquote user/a)) 5)
`(1 2 3 (quote (clojure.core/unquote a)) 5)
;;=> (1 2 3 (quote 4) 5)
;;' -> quote ` -> syntax-quote ~ -> unquote
;;use syntax-quote and unquote to simplify writting macros
(defmacro assert [x]
(when *assert* ;; check the dynamic var `clojure.core/*assert*` to make sure
;; assertions are enabled
(list 'when-not x
(list 'throw
(list 'new 'AssertionError
(list 'str "Assert failed: "
(list 'pr-str (list 'quote x))))))))
(defmacro assert [x]
(when *assert*
`(when-not ~x
(throw (new AssertionError (str "Assert failed: " (pr-str '~x)))))))
;;just use syntax-quote and unquote get rid of nested list quote expression
;;read-eval
(eval (read-string "(+ 1 2)"))
;;=> 3
(read-string "#=(+ 1 2)")
;;=> 3
;;unquote splicing ~@ do the concat job
(def other-numbers '(4 5 6 7 8))
;;=> #'user/other-numbers
`(1 2 3 ~other-numbers 9 10)
;;=> (1 2 3 (4 5 6 7 8) 9 10)
(concat '(1 2 3) other-numbers '(9 10))
;;=> (1 2 3 4 5 6 7 8 9 10)
`(1 2 3 ~@other-numbers 9 10)
;;=> (1 2 3 4 5 6 7 8 9 10)
;;syntax-quote just with the symbol namespace
'(a b c)
;;=> (a b c)
`(a b c)
;;=> (user/a user/b user/c)
(defmacro squares [xs] (list 'map '#(* % %) xs))
;;=> #'user/squares
(squares (range 10))
;;=> (0 1 4 9 16 25 36 49 64 81)
(range 10)
;;=> (0 1 2 3 4 5 6 7 8 9)
(defn squares-fn [xs] (map #(* % %) xs))
;;=> #'user/squares-fn
(squares-fn (range 10))
;;=> (0 1 4 9 16 25 36 49 64 81)
;;学到了一个小trick 从map中获取值可以提供一个默认值
({:a 1 :b 2} :c :default-value)
;;=> :default-value
({:a 1 :b 2} :a :default-value)
;;=> 1
`(* x x)
;;=> (clojure.core/* user/x user/x)
=> #'user/squares
`(* 'x 'x)
;;=> (clojure.core/* (quote user/x) (quote user/x))
`(* ~'x ~'x)
;;=> (clojure.core/* x x)
(defmacro squares [xs] `(map (fn [x] (* x x)) ~xs))
;;=> #'user/squares
(squares (range 10))
;;CompilerException java.lang.RuntimeException: Can't use qualified name as parameter: user/x, compiling:(/private/var/folders/yy/lm5sds992rn767l2js6vv8lr0000gn/T/form-init7060513637416030628.clj:1:1)
(macroexpand '(squares (range 10)))
;;=> (clojure.core/map (clojure.core/fn [user/x] (clojure.core/* user/x user/x)) (range 10))
(defmacro squares [xs] `(map (fn [~'x] (* ~'x ~'x)) ~xs))
;;=> #'user/squares
(macroexpand '(squares (range 10)))
;;=> (clojure.core/map (clojure.core/fn [x] (clojure.core/* x x)) (range 10))
(squares (range 10))
;;=> (0 1 4 9 16 25 36 49 64 81)
;;why we need gensym
(defmacro make-adder [x] `(fn [~'y] (+ ~x ~'y)))
(macroexpand-1 '(make-adder 10))
;;=> (clojure.core/fn [y] (clojure.core/+ 10 y))
(def y 100)
((make-adder (+ y 3)) 5)
;;=> 13
(macroexpand-1 '(make-adder (+ y 3)))
;;=> (clojure.core/fn [y] (clojure.core/+ (+ y 3) y))
;;可以看到因为(+ y 3)没有在编译期求值在macro内部的形参y把外部的y symbol给覆盖掉了 导致得到了一个意料之外的求值结果 要解决这一类问题就需要在macro内使用gensym机制了
;;gensym’s job is simple: it produces a symbol with a unique name
(gensym)
;;=> G__4905
(gensym "cleantha")
;;=> cleantha4954
(gensym "cleantha")
;;=> cleantha4973
;;use gensym to fix the problem we face
((make-adder (+ y 3)) 5)
;;=> 108
(macroexpand-1 '(make-adder (+ y 3)))
;;=> (clojure.core/fn [G__5184] (clojure.core/+ (+ y 3) G__5184))
;;syntax suger for gensym called auto-gensym
(defmacro make-adder [x]
`(fn [y#] (+ ~x y#)))
;;=> #'user/make-adder
((make-adder (+ y 3)) 5)
;;=> 108
(macroexpand-1 '(make-adder (+ y 3)))
;;=> (clojure.core/fn [y__5279__auto__] (clojure.core/+ (+ y 3) y__5279__auto__))
;;目的就是不要让macro内部的临时symbol覆盖了外部的symbol或者是状态
;;这让我们写的macro更加安全而不会与外部的代码发生冲突
;;&env and &form
;;this two value can only be used inside a macro
;;&form记录了当前macro被调用的形式 比如
(defmacro hehe [a b]
(pprint {:form &form}))
;;=> #'user/hehe
(hehe 3 3)
{:form (hehe 3 3)}
;;=> nil
;;&env记录了当前macro被调用的参数列表
(defmacro info-about-caller []
(pprint {:form &form :env &env})
`(println "macro was called"))
;;=> #'user/info-about-caller
(info-about-caller)
{:form (info-about-caller), :env nil}
macro was called
;;=> nil
(let [foo "bar"]
(info-about-caller))
{:form (info-about-caller),
:env {foo #<LocalBinding clojure.lang.Compiler$LocalBinding@55e1e7e3>}}
macro was called
;;=> nil
(let [foo "bar"
baz "quux"]
(info-about-caller))
{:form (info-about-caller),
:env
{baz #<LocalBinding clojure.lang.Compiler$LocalBinding@3f961187>,
foo #<LocalBinding clojure.lang.Compiler$LocalBinding@4cda814e>}}
macro was called
;;=> nil
;;再来看一个比较绕的例子
(defmacro inspect-caller-locals []
(->> (keys &env)
(map (fn [k] [`'~k k]))
(into {})))
(let [foo "bar"
baz "quux"]
(inspect-caller-locals))
;;=> {baz "quux", foo "bar"}
;;可能要问为什么要用`'~k这么奇怪的写法
;;如果定义成这样
(defmacro inspect-caller-locals []
(->> (keys &env)
(map (fn [k] [`~k k]))
(into {})))
;;得到结果是
(let [foo "bar"
baz "quux"]
(inspect-caller-locals))
;;=> {"quux" "quux", "bar" "bar"}
;;再看一下
(defmacro inspect-caller-locals []
(->> (keys &env)
(map (fn [k] (println k)))
(into {})))
(let [foo "bar"
baz "quux"]
(inspect-caller-locals))
;;baz
;;foo
;;=> {}
(defmacro inspect-caller-locals []
(->> (keys &env)
(map (fn [k] ['k k]))
(into {})))
(let [foo "bar"
baz "quux"]
(inspect-caller-locals))
;;CompilerException java.lang.RuntimeException: Unable to resolve symbol: k in this context
;;首先还是要明确一点就是macro只是构造出了一个等待在runtime被求值的form表达式 然后被扔回来的form在runtime是会被求值的
所以在macro中如果是foo这个symbol那么被扔出macro之后就会被求值为"bar"这个值,而我们要做的其实就是(list 'quote k)也就是`(quote ~k)
其实`~'k `(quote k) (list 'quote k)这三者真的是一样一样的 这三个写法的主要作用就是可以保证symbol的名字被传出macro的时候依然保持原样而不会被求值
;;http://blog.jayfields.com/2011/02/clojure-and.html
;;&form还有一个黑魔法 经常在测试框架的编写中被用到 就是可以保存被调用时处于源代码的第几行第几列
;;再来复习下&form这个值的本质
(defmacro inspect-called-form [& arguments]
{:form (list 'quote (cons 'inspect-called-form arguments))})
;;=> #'user/inspect-called-form
(inspect-called-form 1 2 3)
;;=> {:form (inspect-called-form 1 2 3)}
(defmacro inspect-called-form [& arguments]
{:form (list 'quote &form)})
;;=> #'user/inspect-called-form
(inspect-called-form 1 2 3)
;;=> {:form (inspect-called-form 1 2 3)}
^{:doc "this is good stuff"} (inspect-called-form 1 2 3)
;;=> {:form (inspect-called-form 1 2 3)}
(meta (:form *1))
;;=> {:doc "this is good stuff", :line 1, :column 1}
;;*1在repl中表示上一个表达式的值 因为是在repl中所以line和column都是1
;;如果是在项目中比如emacs或者是cursive中
44 (defmacro inspect-called-form [& argument]
45 {:form (list 'quote &form)})
46
47 (meta (:form (inspect-called-form 1 2 3)))
;;=> {:line 47, :column 14}
(defmacro show-form [a b]
(list 'quote &form))
(defmacro show-form [a b]
`'~&form)
(meta (show-form 50 100))
;;=> {:line 63, :column 7}
(defmacro show-form [a b]
(list 'quote (first &form)))
(defmacro show-form [a b]
`(println ~@(next &form)))
;;像这样只去&form值一部分好像是得不到调用时行号列号这两个meta信息的 特别注意
;;macro与fn有一个非常重要的区别就是fn是一个值 而macro并不是一个值无法被传来传去
;;these two macros are the same
(defmacro square [x] (list '* x x))
(defmacro square [x] `(* ~x ~x))
(defn square [x]
(* x x))
;;=> #'user/square
(map square (range 10))
;;=> (0 1 4 9 16 25 36 49 64 81)
(defmacro square [x] `(* ~x ~x))
;;=> #'user/square
(map square (range 10))
;;CompilerException java.lang.RuntimeException: Can't take value of a macro: #'user/square
;;如果一定要把一个macro应用到map filter之类的高阶函数中来进行计算的组合 那么需要包在一个惰性的context中 比如一个匿名函数
(map (fn [n] (square n)) (range 10))
;;=> (0 1 4 9 16 25 36 49 64 81)
(defmacro do-multiplication [expression]
(cons `* (rest expression)))
;;=> #'user/do-multiplication
(do-multiplication (+ 3 4))
;;=> 12
(map (fn [x] (do-multiplication x)) ['(+ 3 4) '(- 2 3)])
;;CompilerException java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol
;;when Clojure tries to macroexpand (do-multiplication x), it can’t do its job, because x is a symbol
;;在macro看来x就是一个symbol不是'(+ 3 4)这样的列表表达式 所以出错了
(defmacro square [x] `(* ~x ~x))
@#'square
;;=> #<user$square user$square@72d0eb68>
(fn? @#'square)
;;=> true
(@#'square 9)
;;ArityException Wrong number of args (1) passed to: user/square clojure.lang.AFn.throwArity (AFn.java:429)
;;remember &form and &env 所以我们只能手动传入&form &env这两个值
(@#'square nil nil 9)
;;=> (clojure.core/* 9 9)
(eval (@#'square nil nil 9))
;;=> 81
;;可以发现@#'这个操作有点像macroexpand
(require '[clojure.string :as string])
(defmacro log [& args]
`(println (str "[INFO]" (string/join ":" ~(vec args)))))
(log "that went well")
;;[INFO]that went well
;;=> nil
(log "item #1 created" "by user #42")
;;[INFO]item #1 created:by user #42
;;=> nil
(defmacro log [& args]
`(println (str "[INFO]" (string/join ":" ~args))))
(log "item #1 created" "by user #42")
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn
;;因为被macro扔回来的form中所有list都会在runtime时被求值 所以如果一个list里都是数据 不想被求值就需要在macro中就被quote住
;;下面这样写也是对的 或者像上面这样直接把list转成vector就不会被求值了
(defmacro log [& args]
`(println (str "[INFO]" (string/join ":" '~args))))
(log "item #1 created" "by user #42")
;;[INFO]item #1 created:by user #42
;;=> nil
(defn send-email [user messages]
(Thread/sleep 1000))
(def admin-user "kathy@example.com")
(def current-user "colin@example.com")
(defn notify-everyone [messages]
(apply log messages)
(send-email admin-user messages)
(send-email current-user messages))
;;CompilerException java.lang.RuntimeException: Can't take value of a macro: #'user/log
;;还是因为macro没法作为一个值无法传给apply函数 所以只能再用一个macro去调用这个macro
(defmacro notify-everyone [messages]
`(do
(send-email admin-user ~messages)
(send-email current-user ~messages)
(log ~@messages)))
(notify-everyone ["item #1 processed" "by worker #72"])
;;[INFO]item #1 processed:by worker #72
;;=> nil
;;当然对于这样的需求 写成fn是最方便的
(require '[clojure.string :as string])
(defn log [& args]
(println (str "[INFO]" (string/join ":" args))))
(log "hi" "there")
;;[INFO]hi:there
;;=> nil
(apply log ["hi" "there"])
;;[INFO]hi:there
;;=> nil
;;macro中一个经常遇到的坑
;;以clojure.core/and为例 正确的clojure.core/and实现如下
(defmacro right-and
([] true)
([x] x)
([x & next]
`(let [arg# ~x]
(if arg# (right-and ~@next) arg#))))
(right-and (do (println "hi there") (= 1 2)) (= 3 4))
;;hi there
;;=> false
(macroexpand-1 '(right-and (do (println "hi there") (= 1 2)) (= 3 4)))
;;=> (clojure.core/let [arg__12909__auto__ (do (println "hi there") (= 1 2))] (if arg__12909__auto__ (user/right-and (= 3 4)) arg__12909__auto__))
;;但是在写的时候可能会犯一个不太会被发现的错误
(defmacro wrong-and
([] true)
([x] x)
([x & next]
`(if ~x (wrong-and ~@next) ~x)))
(wrong-and (do (println "hi there") (= 1 2)) (= 3 4))
;;hi there
;;hi there
;;=> false
(macroexpand-1 '(wrong-and (do (println "hi there") (= 1 2)) (= 3 4)))
;;=> (if (do (println "hi there") (= 1 2)) (user/wrong-and (= 3 4)) (do (println "hi there") (= 1 2)))
;;假如真实项目中的副作用不仅仅是在控制台打印几句话而是想持久化的数据库中或者分布式存储中存入日志信息或者其他数据那么重复执行就会带来问题了 这个要特别注意 要先把第一部分求值掉并且在macro中把值保存着 不要重复求值
;;dynamic binding
;;some difference between let and binding
(def ^:dynamic foo 10)
(defn print-foo [] (println foo))
(let [foo 2] (print-foo))
;;10
;;=> nil
(binding [foo 2] (print-foo))
;;2
;;=> nil
(print-foo)
;;10
;;=> nil
;;可以看到从dynamic scoping里出来以后就还是原来的值了
;;binging可以产生一个比let更加local的scope,并且只是对当前线程可见的,let对任意线程可见,而且1.5之后binding只能作用于^:dynamic
;;let is called lexical scoping and binding is called dynamic scoping
(defn circle-area [radius]
(* Math/PI (* radius radius)))
;;Here radius is a lexically scoped local variable: its value is determined solely by the argument passed into the function. That makes it easy to see at a glance what the dependencies are. Function parameters, along with let and loop bindings, are common examples of lexical binding.
(declare ^:dynamic *radius*)
(defn circle-area []
(* Math/PI (* *radius* *radius*)))
(binding [*radius* 10.0]
(circle-area))
;;=> 314.1592653589793
;;Note that the asterisks in *radius* are called earmuffs and are just a naming convention for dynamically scoped vars in Clojure—they’re not required by the language.
;;so why should we use dynamic binding
(defn log [message]
(let [timestamp (.format (java.text.SimpleDateFormat. "yyyy-MM-dd'T'HH:mmZ")
(java.util.Date.))]
(println timestamp "[INFO]" message)))
(defn process-events [events]
(doseq [event events]
(log (format "Event %s has been processed" (:id event)))))
(process-events [{:id 88896} {:id 88898}])
;;2015-06-17T13:57+0800 [INFO] Event 88896 has been processed
;;2015-06-17T13:57+0800 [INFO] Event 88898 has been processed
;;=> nil
;;一般没有特别说明println都是直接向控制台打印内容
(let [file (java.io.File. "/Users/zjh/Documents/event-stream.log")]
(with-open [file (clojure.java.io/writer file :append true)]
(binding [*out* file]
(process-events [{:id 88896} {:id 88898}]))))
;;如果在dynamic scoping中修改*out*就可以重定向println函数打印内容的目的地了
;;如果把with-open binding这些细节封装在内部对调用者会更加友好一些
(defmacro with-out-file [file & body]
`(with-open [writer# (clojure.java.io/writer ~file :append true)]
(binding [*out* writer#]
~@body)))
(let [file (java.io.File. "/Users/zjh/Documents/event-stream.log")]
(with-out-file file
(process-events [{:id 88894} {:id 88895} {:id 88897}])
(process-events [{:id 88896} {:id 88898}])))
;;=> nil
;;这样只要关注要写入的文件的位置在哪里就行了而不需要手动再去with-open操作以及显式地去重定向*out*变量
(defmacro with-out-str
[& body]
`(let [s# (new java.io.StringWriter)]
(binding [*out* s#]
~@body
(str s#))))
(with-out-str (println "cleantha"))
;;=> "cleantha\n"
(with-out-str (println "cleantha") (println "hehe"))
;;=> "cleantha\nhehe\n"
;;将println打印的内容重定向到了StringWriter中用来构造字符串
(defmacro with-in-str
[s & body]
`(with-open [s# (-> (java.io.StringReader. ~s)
clojure.lang.LineNumberingPushbackReader.)]
(binding [*in* s#]
~@body)))
(defn join-input-lines [seperator]
(print (clojure.string/replace (slurp *in*) "\n" seperator)))
(let [result (with-in-str "hello there\nhi\nsup\nohai"
(with-out-str (join-input-lines ",")))]
(assert (= "hello there,hi,sup,ohai" result)))
;;=> nil
;;*in* *out*有点像标准输入输出 就是可以随意被重定向从而获得输入输出的字符流
;;当然macro无法被当做值的特性有时候也会很麻烦 用函数照样可以很优雅
(defn with-out-file [file body-fn]
(with-open [writer (clojure.java.io/writer file :append true)]
(binding [*out* writer]
(body-fn))))
(let [file (java.io.File. "/Users/zjh/Documents/event-stream.log")]
(with-out-file file
(fn []
(process-events [{:id 88894} {:id 88895} {:id 88897}])
(process-events [{:id 88896} {:id 88898}]))))
;;=> nil
;;为了让真正打印内容的在重定向*out*值之前先不要执行所以只能先包在一个惰性的匿名函数结构中 而在macro中则可以直接传入 这也正是macro的意义
;;comment也是一个宏 它的作用就是组织被传入的表达式求值 当然这个宏有它自己的语法糖;和#_
(defmacro my-comment
[& body])
(my-comment (println "hehe"))
;;=> nil
;;因为macro本身就会yield住传入的表达式 如果什么都不扔回去那么就是一个comment了
;;dosync这个宏会让传入的form在执行的时候包在一个带有事物机制的上下文(context)中
;;http://www.lispcast.com/3-things-java-can-steal-from-clojure
(defmacro sync
[flags-ignored-for-now & body]
`(. clojure.lang.LockingTransaction
(runInTransaction (fn [] ~@body))))
(defmacro dosync
[& exprs]
`(sync nil ~@exprs))
(def ant-1 (ref {:id 1 :x 0 :y 0}))
(def ant-2 (ref {:id 2 :x 0 :y 0})
(dosync
(alter ant-1 update-in [:x] inc)
(alter ant-1 update-in [:y] inc)
(alter ant-2 update-in [:x] dec)
(alter ant-2 update-in [:y] dec))
@ant-1
;;=> {:y 1, :id 1, :x 1}
@ant-2
;;=> {:y -1, :id 2, :x -1}
;;关于STM http://chaifeng.com/clojure-stm-what-why-how/ 其实就是看一个量的版本号 如果版本不对说明有其他线程已经操作过这个变量了那么我就先回滚然后在新的值的基础上重新执行一遍我的过程
;;future and promise
;;http://clojure-api-cn.readthedocs.org/en/latest/clojure.core/future.html
;;http://clojure-api-cn.readthedocs.org/en/latest/clojure.core/promise.html
;;http://www.blogjava.net/killme2008/archive/2010/08/08/328230.html
;;future对象会在另一个线程里对body进行求值,并将求值结果保存到缓存中,之后对这个future对象的所有强迫求值都会返回这个缓存值,如果强迫求值时body的计算还没完成,那么调用者将被阻塞,直到计算完成,或者deref/@设置的过期时间到达为止. @是deref的语法糖
(defmacro future
[& body]
`(future-call (^{:once true} fn* [] ~@body)))
(def a (/ 1 0))
;;CompilerException java.lang.ArithmeticException: Divide by zero
(def a (future (/ 1 0)))
@a
;;ArithmeticException Divide by zero clojure.lang.Numbers.divide (Numbers.java:156)
;;^:keyword-here, by the way, is just a shorthand for the common ^{:keyword-here true} pattern for Clojure metadata.
;;^{:once true}这一个metadata标明了后面的fn只能被执行一次 It makes sure that any closed-over locals in the function get cleared after the function is called. This way, Clojure doesn’t have to hold onto those values indefinitely, wrongly thinking that you might call the function again. 如果不设置这个metadata clojure会无限期的持有一部分内存 以准备这个fn会被再次调用 而指定了这个metadata 执行一次之后clojure就会把所有无用的内存释放 防止memory leak
(let [x :a
f (^:once fn* [] (println x))]
(f)
(f))
;;:a
;;nil
;;=> nil
(let [x :a
f (fn [] (println x))]
(f)
(f))
;;:a
;;:a
;;=> nil
;;In the ^:once-decorated version, after the first evaluation of f, the local x gets cleared(执行完一次之后所有本地变量都被release了), so the second evaluation has x bound to nil.Clearly this is useful only when the function will execute just once. Note that it’s necessary to use fn* here, not fn, to get the benefit of this optimization. Of course, if the function will execute multiple times, or if you don’t mind leaking the memory that your locals consume, you can always use the usual fn for your function definitions.
;;macro中用来处理异常
(defmacro try-expr
[msg form]
`(try ~(clojure.test/assert-expr msg form)
(catch Throwable t#
(clojure.test/do-report {:type :error, :message ~msg,
:expected '~form, :actual t#}))))
(defmacro is
([form] `(is ~form nil))
([form msg] `(try-expr ~msg ~form)))
(is (= 1 1))
;;=> true
(is (= 1 3))
;;FAIL in clojure.lang.PersistentList$EmptyList@1 (form-init7060513637416030628.clj:1)
;;expected: (= 1 3)
;; actual: (not (= 1 3))
;;=> false
(is (= 1 (do (throw (Exception.)) 1)))
;;ERROR in clojure.lang.PersistentList$EmptyList@1 (form-init7060513637416030628.clj:1)
;;expected: (= 1 (do (throw (Exception.)) 1))
;; actual: java.lang.Exception: null
;;当然我们可以用fn来实现这一套机制
(require '[clojure.test :as test])
(defn type-expr [msg f]
(try (eval (test/assert-expr msg (f)))
(catch Throwable t
(test/do-report {:type :error :message msg
:expected f, :actual t}))))
(defn my-is
([f] (my-is (f) nil))
([f msg] (try-expr msg f)))
(my-is (fn [] (= 1 1)))
;;=> true
(my-is (fn [] (= 1 3)))
;;FAIL in clojure.lang.PersistentList$EmptyList@1 (form-init7060513637416030628.clj:3)
;;expected: f
;; actual: false
;;=> false
;;但是有一个问题就是我们希望expected这一项中能返回表达式的具体内容信息 但是用fn这一套要做到就会很麻烦了 要传入的时候就先quote住,然后在内部eval得到求值结果 这一点用macro做就会自然很多
;;当我们需要操作外部数据流比如磁盘上的文件或者是数据库我们就需要建立连接 当操作昨晚之后需要关闭这些连接 或者说是释放掉 不管操作执行成功还是失败 所以在java代码中经常可以看到try catch最后还有一个finally block用来写关闭连接的代码 在clojure中操作外部资源依然需要手动关闭这些连接 但是这些重复的操作可以封装在macro内部 这样外部的代码看起来可以更简洁和优雅
;;https://crossclj.info/ns/org.clojure/clojure/1.7.0-RC1/clojure.core.html#_assert-args
(defmacro assert-args
[& pairs]
`(do (when-not ~(first pairs)
(throw (IllegalArgumentException.
(str (first ~'&form) " requires " ~(second pairs) " in " ~'*ns* ":" (:line (meta ~'&form))))))
~(let [more (nnext pairs)]
(when more
(list* `assert-args more)))))
(defmacro with-open
[bindings & body]
(assert-args
(vector? bindings) "a vector for its binding"
(even? (count bindings)) "an even number of forms in binding vector")
(cond
(= (count bindings) 0) `(do ~@body)
(symbol? (bindings 0)) `(let ~(subvec bindings 0 2)
(try
(with-open ~(subvec bindings 2) ~@body)
(finally
(. ~(bindings 0) close))))
:else (throw (IllegalArgumentException.
"with-open only allows Symbols in bindings"))))
(import 'java.io.FileInputStream)
(defn read-file [file-path]
(let [buffer (byte-array 4096)
contents (StringBuilder.)]
(with-open [file-stream (FileInputStream. file-path)]
(while (not= -1 (.read file-stream buffer))
(.append contents (String. buffer))))
(str contents)))
(read-file "/Users/zjh/Documents/event-stream.log")
;;=> "2015-06-17T13:49+0800 [INFO] Event 88896 has been processed\n2015-06-17T13:49+0800 [INFO] Event 88898 has been processed\n2015-06-17T14:32+0800 [INFO] Event 88894 has been processed\n2015-06-17T14:32+0800 [INFO] Event 88895 has been processed\n2015-06-17T14:32+0800 [INFO] Event 88897 has been processed\n2015-06-17T14:32+0800 [INFO] Event 88896 has been processed\n2015-06-17T14:32+0800 [INFO] Event 88898 has been processed\n2015-06-17T15:49+0800 [INFO] Event 88894 has been processed\n2015-06-17T15:49+0800 [INFO] Event 88895 has been processed\n2015-06-17T15:49+0800 [INFO] Event 88897 has been processed\n2015-06-17T15:49+0800 [INFO] Event 88896 has been processed\n2015-06-17T15:49+0800 [INFO] Event 88898 has been processed\n"
;;可以看到关闭外部连接的重复性操作已经被封装在with-open这个macro内部了
The main point here, and in the other examples in this chapter, is that macros allow you to eliminate the noisy details of cleaning up an open resource, or rescuing errors, or setting up dynamic bindings or other contexts for evaluation. By writing macros to abstract away these contextual details, you can clarify the core operations you’re performing to make your code’s purpose more obvious to the people who will read it in the future (including yourself!). This is a core ability of macros, and you’ll see lots of overlap between this category and ones we’ll cover later.
Now that you’ve seen some of the ways you can use macros to abstract away the context in which your code will evaluate, we’ll look at ways to use both abstraction and more macro-specific tools to improve the runtime performance of our Clojure code.
;;我们可以将一些复杂的context 比如处理异常 比如关闭数据库连接 比如STM这样的并发上下文 比如注释 比如处理动态作用域 这些重复且繁琐的处理上下文有关的代码都可以封装在macro内 这样外部的代码就会更加简洁 更加focus在真正需要变化的业务细节上
;;在编写clojure代码的时候简洁易读 高度抽象是一个追求的目标 但是性能问题也不能忽视 毕竟最终还是要用这些代码去做一些事情的 而不是写一些玩具自娱自乐
;;jdk自带了jvm性能监控工具jvisualvm 命令行直接输入jvisualvm就可以使用 并且支持监控远程jvm
;;clojure有一个库可以做到更低级别的性能监控 可以重复运行一段程序 得到平均运行性能
;;https://github.com/hugoduncan/criterium
(defproject foo "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.6.0"]]
;;just enables some JVM JIT optimizations
:jvm-opts ^:replace []
:profiles {:dev {:dependencies [[criterium "0.4.3"]]}})
(use 'criterium.core)
(defn length-1 [x] (.length x))
(bench (length-1 "hi there!"))
WARNING: Final GC required 1.979001231224729 % of runtime
Evaluation count : 18409560 in 60 samples of 306826 calls.
Execution time mean : 3.113565 µs
Execution time std-deviation : 306.166228 ns
Execution time lower quantile : 2.584964 µs ( 2.5%)
Execution time upper quantile : 3.558237 µs (97.5%)
Overhead used : 2.453292 ns
;;=> nil
(defn length-2 [^String x]
(.length x))
;;=> #'user/length-2
(bench (length-2 "hi there!"))
Evaluation count : 11897945400 in 60 samples of 198299090 calls.
Execution time mean : 2.284492 ns
Execution time std-deviation : 0.496940 ns
Execution time lower quantile : 1.564069 ns ( 2.5%)
Execution time upper quantile : 3.099086 ns (97.5%)
Overhead used : 2.453292 ns
;;=> nil
(bench (.length "hi there!"))
Evaluation count : 6555744840 in 60 samples of 109262414 calls.
Execution time mean : 6.054197 ns
Execution time std-deviation : 1.046173 ns
Execution time lower quantile : 4.534307 ns ( 2.5%)
Execution time upper quantile : 7.556461 ns (97.5%)
Overhead used : 2.453292 ns
;;=> nil
;;可以看到加上type hint之后明显快了一个数量级 因为加上了type hint了之后 jvm就不需要用反射机制去执行方法了
;;但是在clojure代码中何时加上type hint来避免反射非常具有技巧性和经验 clojure中可以通过设置一个dynamic binding来自动提示我们一段代码中是否需要设置type hint
(set! *warn-on-reflection* true)
;;=> true
(defn length-1 [x] (.length x))
;;Reflection warning, /private/var/folders/yy/lm5sds992rn767l2js6vv8lr0000gn/T/form-init2270832937079303145.clj:1:20 - reference to field length can't be resolved.
;;=> #'user/length-1
;;Clojure High Performance Programming一书中介绍了许多clojure代码优化方法
(defn sum [xs]
(reduce + xs))
(def collection (range 10))
(bench (sum collection))
Evaluation count : 109085340 in 60 samples of 1818089 calls.
Execution time mean : 567.392161 ns
Execution time std-deviation : 42.773842 ns
Execution time lower quantile : 486.908340 ns ( 2.5%)
Execution time upper quantile : 632.978747 ns (97.5%)
Overhead used : 2.453292 ns
Found 2 outliers in 60 samples (3.3333 %)
low-severe 1 (1.6667 %)
low-mild 1 (1.6667 %)
Variance from outliers : 56.7741 % Variance is severely inflated by outliers
;;=> nil
(defn array-sum [^ints xs]
(loop [index 0
acc 0]
(if (< index (alength xs))
(recur (unchecked-inc index) (+ acc (aget xs index)))
acc)))
(def array (into-array Integer/TYPE (range 100)))
(bench (array-sum array))
Evaluation count : 262590300 in 60 samples of 4376505 calls.
Execution time mean : 235.638416 ns
Execution time std-deviation : 13.807141 ns
Execution time lower quantile : 205.759026 ns ( 2.5%)
Execution time upper quantile : 258.648432 ns (97.5%)
Overhead used : 2.453292 ns
Found 1 outliers in 60 samples (1.6667 %)
low-severe 1 (1.6667 %)
Variance from outliers : 43.4640 % Variance is moderately inflated by outliers
;;=> nil
;;加上type hint并且用类似命令式的写法来进行优化的确可以在性能上得到一定的提升 但是也导致了代码变得丑陋 clojure.core中提供了areduce方法
(defn array-sum [^ints xs]
(areduce xs index ret 0 (+ ret (aget xs index))))
(bench (array-sum array))
Evaluation count : 296017800 in 60 samples of 4933630 calls.
Execution time mean : 231.004291 ns
Execution time std-deviation : 22.946323 ns
Execution time lower quantile : 183.933405 ns ( 2.5%)
Execution time upper quantile : 261.804172 ns (97.5%)
Overhead used : 2.453292 ns
Found 1 outliers in 60 samples (1.6667 %)
low-severe 1 (1.6667 %)
Variance from outliers : 68.7068 % Variance is severely inflated by outliers
;;=> nil
;;areduce的内部实现
(defmacro areduce
[a idx ret init expr]
`(let [a# ~a]
(loop [~idx 0 ~ret ~init]
(if (< ~idx (alength a#))
(recur (unchecked-inc ~idx) ~expr)
~ret))))
(macroexpand '(areduce xs index ret 0 (+ ret (aget xs index))))
=> (let* [a__3504__auto__ xs] (clojure.core/loop [index 0 ret 0] (if (clojure.core/< index (clojure.core/alength a__3504__auto__)) (recur (clojure.core/unchecked-inc index) (+ ret (aget xs index))) ret)))
(macroexpand-1 '(areduce xs index ret 0 (+ ret (aget xs index))))
=> (clojure.core/let [a__3504__auto__ xs] (clojure.core/loop [index 0 ret 0] (if (clojure.core/< index (clojure.core/alength a__3504__auto__)) (recur (clojure.core/unchecked-inc index) (+ ret (aget xs index))) ret)))
;;这里再一次看到了宏的威力 所有传给宏的东西都可以认为是symbol 是数据 不需要有特殊的含义 但是在macro内部进行一系列的操作之后 扔回来的list表达式一定是可以求值的 是可以被执行的代码了 这用纯fn实现类似的功能会需要对传入的内容做特殊的限制 并且会变得非常不简洁
;;https://github.com/Prismatic/hiphip
;;去看这个库的java部分其实是对几种数据类型分别写了一套操作 将这些操作都提到编译期 从而牺牲抽象性 提升速度
(require 'hiphip.int)
(use 'criterium.core)
(def array (into-array Integer/TYPE (range 100)))
(bench (hiphip.int/asum array))
WARNING: Final GC required 2.409662123626667 % of runtime
Evaluation count : 189557820 in 60 samples of 3159297 calls.
Execution time mean : 334.323963 ns
Execution time std-deviation : 31.128169 ns
Execution time lower quantile : 284.060824 ns ( 2.5%)
Execution time upper quantile : 381.895240 ns (97.5%)
Overhead used : 2.974551 ns
;;=> nil
;;其实用hiphip也没比之前快多少 估计是clojure自己也有优化 反正都是在纳秒级别的 一个数量级
(defn array-sum-of-squares [^ints xs]
(areduce xs index ret 0 (+ ret (let [x (aget xs index)] (* x x)))))
(bench (array-sum-of-squares array))
Evaluation count : 37936500 in 60 samples of 632275 calls.
Execution time mean : 1.703424 µs
Execution time std-deviation : 130.997109 ns
Execution time lower quantile : 1.473775 µs ( 2.5%)
Execution time upper quantile : 1.920034 µs (97.5%)
Overhead used : 2.974551 ns
;;=> nil
(bench (hiphip.int/asum [n array] (* n n)))
Evaluation count : 36492240 in 60 samples of 608204 calls.
Execution time mean : 1.749422 µs
Execution time std-deviation : 123.411889 ns
Execution time lower quantile : 1.452996 µs ( 2.5%)
Execution time upper quantile : 1.911485 µs (97.5%)
Overhead used : 2.974551 ns
Found 1 outliers in 60 samples (1.6667 %)
low-severe 1 (1.6667 %)
Variance from outliers : 53.4170 % Variance is severely inflated by outliers
;;=> nil
(bench (reduce #(+ (* %2 %2) %1) 0 (range 100)))
Evaluation count : 5867280 in 60 samples of 97788 calls.
Execution time mean : 11.882008 µs
Execution time std-deviation : 1.358105 µs
Execution time lower quantile : 9.696508 µs ( 2.5%)
Execution time upper quantile : 15.287655 µs (97.5%)
Overhead used : 2.974551 ns
Found 3 outliers in 60 samples (5.0000 %)
low-severe 3 (5.0000 %)
Variance from outliers : 75.4950 % Variance is severely inflated by outliers
;;=> nil
;;是用hiphip相对于直接用areduce并没有多少性能上的提升 但是在代码可读性和简洁性上确实好了不少 有时候在performance与concise之间确实是一直在做trade off
;; from hiphip/type_impl.clj
(defmacro asum
([array]
`(asum [a# ~array] a#))
([bindings form]
`(areduce ~bindings sum# ~(impl/value-cast +type+ 0) (+ sum# ~form))))
The areduce here is hiphip’s version that provides fancy bindings, not the areduce from clojure.core. impl/value-cast is what gives asum its ability to act on different data types based on the value of +type+ at compile time.
hiphip/type_impl.clj, where this macro is defined, is loaded directly from namespaces like hiphip.int via (load "type_impl"), instead of relying on the usual Clojure :require mechanism. Furthermore, hiphip/type_impl.clj doesn’t actually define a namespace, so when it’s loaded in hiphip.int, for example, it defines the asum macro in that namespace. This explains why +type+ can vary for the different types instead of just being a single unchanging value. This loading mechanism is a bit of a hack to avoid a macro-defining macro, which often means multiple levels of syntax-quoting. I can’t fault anyone for wanting to avoid macro-defining macros—they’re hard to write and even harder to understand later.
;;hiphip应该是用了macro的macro这种黑科技来做到直接通过namespace来控制代码中+type+的值从而确定要用int long float还是double类型 从而做到meta的
;;用宏我们就可以把一些性能优化的细节封装在内部 从而使外部代码看上去更加lisp 更加简洁
;;另一个利用宏来加速性能的方法就是将复杂的一些操作的执行移至编译器 尽可能减少runtime代码的执行 runtime干的事情越多 性能相对越低
;;但是有一些操作是没有办法macro化的 比如
(defn calculate-estimate [project-id]
(let [{:keys [optimistic realistic pessimistic]}
(fetch-esitimates-from-web-service project-id)
weighted-mean (/ (+ optimistic (* realistic 4) pessimistic) 6)
std-dev (/ (- pessimistic optimistic) 6)]
(double (+ weighted-mean (* 2 std-dev)))))
;;因为有内容需要在runtime时才能获取 而如果将上面的函数改为macro那么所有东西都需要在编译期知道
(defmacro calculate-estimate [project-id]
(let [{:keys [optimistic realistic pessimistic]}
(fetch-esitimates-from-web-service project-id)
weighted-mean (/ (+ optimistic (* realistic 4) pessimistic) 6)
std-dev (/ (- pessimistic optimistic) 6)]
(double (+ weighted-mean (* 2 std-dev)))))
;;因为如果没有`或者‘ macro会在编译器就全部展开 而函数的body求值(invoke)是在runtime所以可以接受runtime才确定的值
(defn calculate-estimate [{:keys [optimistic realistic pessimistic]}]
(let [weighted-mean (/ (+ optimistic (* realistic 4) pessimistic) 6)
std-dev (/ (- pessimistic optimistic) 6)]
(double (+ weighted-mean (* 2 std-dev)))))
(calculate-estimate {:optimistic 3 :realistic 5 :pessimistic 8})
;;=> 6.833333333333333
(bench (calculate-estimate {:optimistic 3 :realistic 5 :pessimistic 8}))
Evaluation count : 25892580 in 60 samples of 431543 calls.
Execution time mean : 2.245390 µs
Execution time std-deviation : 154.873472 ns
Execution time lower quantile : 1.937481 µs ( 2.5%)
Execution time upper quantile : 2.466717 µs (97.5%)
Overhead used : 2.974551 ns
;;=> nil
(defmacro calculate-estimate-macro [estimates]
`(let [estimates# ~estimates
optimistic# (:optimistic estimates#)
realistic# (:realistic estimates#)
pessimistic# (:pessimistic estimates#)
weighted-mean# (/ (+ optimistic# (* realistic# 4) pessimistic#) 6)
std-dev# (/ (- pessimistic# optimistic#) 6)]
(double (+ weighted-mean# (* 2 std-dev#)))))
(calculate-estimate-macro {:optimistic 3 :realistic 5 :pessimistic 8})
;;=> 6.833333333333333
(bench (calculate-estimate-macro {:optimistic 3 :realistic 5 :pessimistic 8}))
Evaluation count : 32418120 in 60 samples of 540302 calls.
Execution time mean : 2.136734 µs
Execution time std-deviation : 140.504698 ns
Execution time lower quantile : 1.861233 µs ( 2.5%)
Execution time upper quantile : 2.322053 µs (97.5%)
Overhead used : 2.974551 ns
;;=> nil
(defmacro calculate-estimate-macro-fast [{:keys [optimistic realistic pessimistic]}]
(let [weighted-mean (/ (+ optimistic (* realistic 4) pessimistic) 6)
std-dev (/ (- pessimistic optimistic) 6)]
(double (+ weighted-mean (* 2 std-dev)))))
(calculate-estimate-macro-fast {:optimistic 3 :realistic 5 :pessimistic 8})
;;=> 6.833333333333333
(bench (calculate-estimate-macro-fast {:optimistic 3 :realistic 5 :pessimistic 8}))
Evaluation count : 6501050940 in 60 samples of 108350849 calls.
Execution time mean : 6.141474 ns
Execution time std-deviation : 0.970315 ns
Execution time lower quantile : 5.074135 ns ( 2.5%)
Execution time upper quantile : 7.987635 ns (97.5%)
Overhead used : 2.974551 ns
;;=> nil
;;可以看到calculate-estimate-macro-fast这个宏妈的快的和狗一样 快了一个数量级了
;;macro展开是编译期,函数invoke是运行时,实际上并没有变快,而是benchmarking变快了,因为你的benchmarking被传参的时候宏就已经展开了 或者说,整个求值过程发生在了benchmarking开始计时之前,但是对于一些应用来说性能是提高了的因为有些需要在runtime做的费时操作移至了编译期 从而可以快很多 这个性能优化有点类似C++的inline function 用空间换时间
;;这里macro快的原因之前也说了 因为这个macro中没有任何quote的东西 所以直接在编译期完全展开了 然后扔出macro的就是一个值 在runtime当然快成狗 因为只要扔回一个返回值就行了 但是如果macro中又quote比如` 那么编译期求值就展开到你引用的部分为止了 展开为一个list结构的form扔出macro 然后在runtime求值 整体求值还是在运行时 所以速度和函数没差多少 这就是calculate-estimate-macro这个宏为啥会慢的原因
;;但是calculate-estimate-macro-fast这个宏有一个蛋疼的地方 就是拿不到运行时数据 只能用来求值你直接传进去的明文数据
;;比如
(let [test-data {:optimistic 3 :realistic 5 :pessimistic 8}]
(calculate-estimate-macro-fast test-data))
;;CompilerException java.lang.NullPointerException
;;写死的数据也不一定行,一定是直接明文传给宏的数据才可以,因为宏是在编译期求值啊,还没到运行时,而在编译期的时候test-data只是一个symbol,不是hashmap,但是如果直接明文传 {:optimistic 3 :realistic 5 :pessimistic 8} 那就是hashmap了,这也是宏难用的一个原因,在和宏打交道的时候,你面向的是代码本身的数据结构.而非代码所操作的数据结构,所以一定要记住,写宏一定只能针对语法,绝对不能在宏里面写任何可能涉及语义的东西,就是不要试图从宏的内部拿到语义
(defmacro calculate-estimate-macro-fast-quote [{:keys [optimistic realistic pessimistic]}]
`(let [weighted-mean# (/ (+ ~optimistic (* ~realistic 4) ~pessimistic) 6)
std-dev# (/ (- ~pessimistic ~optimistic) 6)]
(double (+ weighted-mean# (* 2 std-dev#)))))
(let [test-data {:optimistic 3 :realistic 5 :pessimistic 8}]
(calculate-estimate-macro-fast-quote test-data))
;;NullPointerException [trace missing]
;;这里解构出错是因为,解构代码在`() 外面,所以解构发生在编译期而此时test-data还不是一个hashmap只是一个symbol 所以无法解构
(let [test-data {:optimistic 3 :realistic 5 :pessimistic 8}]
(calculate-estimate-macro test-data))
;;=> 6.833333333333333
;;因为calculate-estimate-macro这个宏里取test-data里的内容是在runtime 而在运行时的上下文中test-data这个symbol代表的是{:optimistic 3 :realistic 5 :pessimistic 8}这个hashmap所以在runtime求值可以正常运行,因为let也是一个宏 所以let中的执行也是在runtime 编译期只是在expand一个宏也就是macroexpand看到的结果 这个expand的结果用来在runtime被求值执行
(require '[clojure.walk :as cw])
(cw/macroexpand-all '(let [test-data {:optimistic 3 :realistic 5 :pessimistic 8}]
(calculate-estimate-macro test-data)))
;;=> (let* [test-data {:optimistic 3, :realistic 5, :pessimistic 8}] (let* [estimates__5385__auto__ test-data optimistic__5386__auto__ (:optimistic estimates__5385__auto__) realistic__5387__auto__ (:realistic estimates__5385__auto__) pessimistic__5388__auto__ (:pessimistic estimates__5385__auto__) weighted-mean__5389__auto__ (clojure.core// (clojure.core/+ optimistic__5386__auto__ (clojure.core/* realistic__5387__auto__ 4) pessimistic__5388__auto__) 6) std-dev__5390__auto__ (clojure.core// (clojure.core/- pessimistic__5388__auto__ optimistic__5386__auto__) 6)] (clojure.core/double (clojure.core/+ weighted-mean__5389__auto__ (clojure.core/* 2 std-dev__5390__auto__)))))
虽然此时test-data在macro内部也是一个symbol 但是并没有在编译期需要得到这个symbol的值 编译期只是将macro展开为这个结构而已 test-data只要乖乖地做它的symbol就好了 二当runtime要求值的时候 自然在let的上下文中test-data这个symbol的值是一个hashmap 一切就像执行函数一样正常自然
;;When using macros for API purposes, remember that macros are icing, not the whole cake. If we choose to provide a macro API layer, providing access to the underlying machinery via functions is a great decoupling strategy. This makes our code more convenient for consumers who are likely to have varying requirements and aesthetic considerations, but also for us as we read, test, and maintain our code. All other things being equal, it’s far easier to wrap our minds around functions and macros that do one thing than to understand ones that do many things.
;;Decoupling Macros from Functions
;;做到宏与函数解耦 都有一套可以独立供外部调用的api体系
;;用macro来构造api的售后最好是对一层已有的函数构建的api的更优雅的封装 这样可以方便调试 也方便其他人员重新利用函数api这些底层接口 自己组合一套新的api 增加系统的灵活性
;;Because functions evaluate their arguments before their bodies have a chance to execute, macros are the place to go when you want to create new control flow constructs.
;;说烂的话题了 因为函数会在执行body之前先去求值传入的参数 如果一定要用fn做一些更加meta的事情 就需要将传入的参数quote住或者是包在一个惰性上下文中 比如说一个匿名函数中
(defmacro while
[test & body]
`(loop []
(when ~test
~@body
(recur))))
(while (< @counter 3)
(println @counter)
(swap! counter inc))
;;0
;;1
;;2
;;=> nil
;;如果不使用macro来封装一个控制结构 那代码会变得非常啰嗦 并且缺乏语义性
(loop []
(when (< @counter 3)
(println @counter)
(swap! counter inc)
(recur)))
;;0
;;1
;;2
;;=> nil
;;用macro封装一个控制流一般都是爽到不要不要的
(defmacro do-while
[test & body]
`(loop []
~@body
(when ~test (recur))))
(defn play-game
[secret]
(let [guess (atom nil)]
(do-while (not= (str secret) (str @guess))
(print "Guess the secret I'm thinking:")
(flush)
(reset! guess (read-line)))
(println "You got it!")))
(play-game "zebra")
;;Guess the secret I'm thinking:lion
;;Guess the secret I'm thinking:cleantha
;;Guess the secret I'm thinking:zebra
;;You got it!
;;=> nil
;;In every programming language I’ve ever used, I occasionally yearn for a feature from some other language. When I’m in Java I want first-class functions like the ones in Scheme. In Ruby I want explicit dependencies like the ones in Java. No matter how good your language is, there’s enough great work going on in other languages that chances are you’ll eventually see a feature from another language that intrigues you. Thankfully, in Clojure, we can implement many of these missing features ourselves.
;;在clojure或者说是lisp中 可以用macro实现很多语言特性
;;在defmacro中 所有在quote或者syntax quote之前的事情内容都是发生在编译期 而一旦进入了syntax quote部分那就是运行时行为了 所以才可以用~和~@来反引用求值出参数在运行时值
;;macro的抽象能力还在于可以利用一套类似模板的东西来统一定义一系列功能相近的函数或者是数据结构供运行期使用 极大程度简化重复代码
(defmacro add [x] `(+ ~x ~x)
(add 3)
;;=> 6
(macroexpand '(add 3))
;;=> (clojure.core/+ 3 3)
(defmacro add [x] `(+ x x))
(add 3)
;;CompilerException java.lang.RuntimeException: No such var: user/x
(macroexpand '(add 3))
;;=> (clojure.core/+ user/x user/x)
;;记住一个原则就是macroexpand出来的内容是可以被eval的是可以被求值为一个结果的
;;why we need macro
;;有时候我们希望先执行一些代码 然后再基于这个上下文去执行传入的代码 这个时候就需要将传入的部分quote住 或者包在一个惰性上下文中比如(fn [] ...) 而开放出来的api又不可能 说你调用的时候要传入一个quote住的表达式 或者你要把表达式包在一个匿名函数里 不然多别扭 所以这个时候就用到macro了 macro默认quote传入的表达式 而且会在runtime求值macro最终生成的list form 而如果不用macro就需要用(eval ...)去手动求值 这样也是反人类的 所以对于一些meta的需求 用macro代码会更加自然
;;macro的作用就是封装复杂的细节 保证外部代码的简洁优雅
;;在defmacro中 syntax-quote 之外的部分都是编译时行为 并绑定不了运行时的值 只能是将source code中传给macro的form直接当做数据来操作 而在syntax-quote之中就是运行时的内容了可以用~ ~@ '~ ~' `~'等各种黑魔法来调整运行时的执行结构
;;sytax-quote的作用其实就是可以很方便的获取到运行时的值 而不需要手动显式的构造list从macro中扔回来
(defmacro cleantha
[type & body]
(car body))
(cleantha "123" (+ 1 2))
((lambda [search]
(cleantha "123" (= 1 search))) 1)
(defmacro clea
[op & body]
(prn op)
(prn (str body))
(let [newbody (read-string (clojure.string/replace (str body) #"\+" "-"))]
`(let [a# ~@newbody]
(prn a#))))
(clea - (+ 1 2))
(macroexpand '(clea - (+ 1 2)))
(defmacro create-kw-map
"(let [a 1 b 2 c 3] (create-kw-map a b c)) => {:a 1 :b 2 :c 3}"
[& syms]
`(zipmap (map keyword '~syms) (list ~@syms)))
(u/create-kw-map email name)
(defmacro get-featurenames-by-predicate
"get featurenames by predicate"
[features uid & predicates]
`(->> (let [~'uid ~uid]
(filter (fn [{:keys [~'whitelist ~'blacklist]}]
(and ~@predicates)) ~features))
(u/maps :name)))
;;先取出只有白名单的新功能取出id在白名单上的新功能标识
;;再取出只有黑名单的新功能取出id不在黑名单上的新功能标识
;;取出剩下既没有黑名单也没有白名单的的新功能标识
;;统一过滤掉新功能发布时间在当前用户注册时间之前的
;;然后去掉已经被用户标记过已读的
;;最后就是要提示用户未读的新功能标识
(defnp get-unread-features-by-uid
"get unread features by uid"
[uid email created_ts]
(let [uid (u/->int uid)
features (get-by-predicate {:release_ts ['> created_ts]})
names-white (-> features
(get-featurenames-by-predicate
uid
(not (empty? whitelist))
(empty? blacklist)
(contains? (set whitelist) uid)))
names-black (-> features
(get-featurenames-by-predicate
uid
(empty? whitelist)
(not (empty? blacklist))
(not (contains? (set blacklist) uid))))
names-rest (-> features
(get-featurenames-by-predicate
uid
(empty? whitelist)
(empty? blacklist)))
names-read (->> (select user-feature-read
(where {:email email}))
(u/maps :name))]
(clojure.set/difference (clojure.set/union names-white names-black names-rest) names-read)))
;;http://stackoverflow.com/questions/2320348/symbols-in-clojure
(defmacro symbol?? [x]
(if (symbol? x)
true
false))
(def s 1)
(symbol? s)
; => false
(symbol?? s)
; => true
(symbol? 's)
; => true
(symbol?? 's)
; => false
;;乍一看最后一个表达式的结果会有一些奇怪
(defmacro hehe [x]
(println x))
(hehe 's)
;;(quote s)
;;=> nil
可以看到一个symbol放入一个macro之后会变成'(quote s)这一个list了 不再是一个symbol了
(symbol? '(quote s))
;;=> false
(symbol? (eval '(quote s)))
;;=> true
;;http://stackoverflow.com/questions/11959107/what-is-the-purpose-of-or-in-clojure
;;macro写多了~' 和 '~这两个东西是不会不用的 非常有用
~'symbol is used to produce an unqualified symbol. Clojure's macros capture namespace by default, so a symbol in a macro would normally be resolved to (your-namespace/symbol). The unquote-quote idiom directly results in the simple, unqualified symbol name - (symbol) - by evaluating to a quoted symbol. From The Joy Of Clojure:
(defmacro awhen [expr & body]
`(let [~'it ~expr] ; refer to the expression as "it" inside the body
(when ~'it
(do ~@body))))
(awhen [:a :b :c] (second it)) ; :b
'~symbol is likely used to insert a name in the macro or something similar. Here, symbol will be bound to a value - let [symbol 'my-symbol]. This value is then inserted into the code the macro produces by evaluating symbol.
(defmacro def-symbol-print [sym]
`(defn ~(symbol (str "print-" sym)) []
(println '~sym))) ; print the symbol name passed to the macro
(def-symbol-print foo)
(print-foo) ; foo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment