中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

由淺入深學(xué)習(xí) Lisp 宏之實(shí)戰(zhàn)篇

2018-07-20    來源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢(shì)上線!快速搭建集群,上萬Linux鏡像隨意使用

在上一篇文章中,介紹了宏(macro)的本質(zhì): 在編譯時(shí)期運(yùn)行的函數(shù) 。宏相對(duì)于普通函數(shù),還有如下兩條特點(diǎn):

  1. 宏的參數(shù)不會(huì)求值(eval),是 symbol 字面量
  2. 宏的返回值是 code(在運(yùn)行期執(zhí)行),不是一般的數(shù)據(jù)。

這兩條特點(diǎn)也決定了是需要用普通函數(shù)還是宏來解決問題,這里面也蘊(yùn)含著 code as data 的思想,也被稱為同像性(homoiconicity,來自希臘語單詞 homo,意為與符號(hào)含義表示相同)。同像性使得在 Lisp 中去操作語法樹(AST)顯得十分自然,而這在非 Lisp 語言只能由編譯器(Compiler)去操作。

這篇文章側(cè)重于實(shí)戰(zhàn),用具體示例介紹寫宏的技巧與注意事項(xiàng),希望讀者能把本文的 Clojure 代碼自己手動(dòng)敲到 REPL 里面去運(yùn)行、調(diào)試,直到完全理解。

Code as data

首先看一個(gè)簡(jiǎn)單的程序片段

(defn hello-world []
  (println "hello world"))

上面的代碼首先是一個(gè)大的 list,里面依次包含了2 個(gè) symbol,1 個(gè) vector,1 個(gè) list,這個(gè)嵌套的 list 又包含了 1 個(gè) symbol,1 個(gè) string?梢钥吹,這些都是 Clojure 里面的基本數(shù)據(jù)類型,這就給我們提供了一個(gè)很好的寫宏基礎(chǔ)。Clojure 里面很多控制結(jié)構(gòu)都是用宏來實(shí)現(xiàn),比如 when :

(defmacro when [test & body]
  (list 'if test (cons 'do body)))

' 代表 quote,作用是阻止后面的表達(dá)式求值,如果不使用 ' 的話,在進(jìn)行 (list 'if test ...) 求值時(shí)會(huì)報(bào)錯(cuò),因?yàn)闆]發(fā)對(duì) special form 單獨(dú)進(jìn)行求值,這里需要的僅僅是 if 字面量,list 函數(shù)執(zhí)行后的結(jié)果(是一個(gè) list)作為 code 插入到調(diào)用 when 的地方去執(zhí)行。

(when (even? (rand-int 100))
  (println "good luck!")
  (println "lisp rocks!"))

;; when 展開后的形式

(if (even? (rand-int 100))
  (do (println "good luck!") (println "lisp rocks!")))

syntax-quote & unquote

對(duì)于一些簡(jiǎn)單的宏,可以采用像 when 那樣的方式,使用 list 函數(shù)來形成要返回的 code,但對(duì)于復(fù)雜的宏,使用 list 函數(shù)來表示,會(huì)顯得十分麻煩,看下 when-let 的實(shí)現(xiàn):

(defmacro when-let [bindings & body]
  (let [form (bindings 0) tst (bindings 1)]
    `(let [temp# ~tst]
       (when temp#
         (let [~form temp#]
           [email protected])))))

這里返回的 list 使用 ` (backtick)進(jìn)行了修飾,這是 syntax-quote,它與 quote ' 類似,只不過在阻止表達(dá)式求值的同時(shí),支持以下兩個(gè)功能:

  1. 表達(dá)式里的所有 symbol 會(huì)在當(dāng)前 namespace 中進(jìn)行 resolve,返回 fully-qualified symbol
  2. 允許通過 ~ (unquote) 或 [email protected] (slicing-unquote) 阻止部分表達(dá)式的 quote,以達(dá)到對(duì)它們求值的效果

可以通過下面一個(gè)例子來了解它們之間的區(qū)別:

(let [x '(* 2 3) y x]
  (println `y)
  (println ``y)
  (println ``~y)
  (println ``~~y)
  (println (eval ``~~y))
  (println `[[email protected]]))

;; 依次輸出

user/y
(quote user/y)
user/y
(* 2 3)
6
[* 2 3]

這里尤其要注意理解嵌套 syntax-quote 的情況,為了得到正確的值,需要 unquote 相應(yīng)的次數(shù)(上例中的第四個(gè)println),這在 macro-writing macro 中十分有用,后面會(huì)介紹的。

最后需要注意一點(diǎn),在整個(gè) Clojure 程序生命周期中, (syntax-)quote , (slicing-)unquote 是 Reader 來解析的,詳見編譯器工作流程?梢酝ㄟ^ read-string 來驗(yàn)證:

user> (read-string "`y")
(quote user/y)
user> (read-string "``y")
(clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) 
                                       (clojure.core/list (quote user/y))))
user> (read-string "``~y")
(quote user/y)
user> (read-string "``~~y")
y

Macro Rules of Thumb

在正式實(shí)戰(zhàn)前,這里摘抄 JoyOfClojure 一書中關(guān)于寫宏的一般準(zhǔn)則:

  1. 如果函數(shù)能完成相應(yīng)功能,不要寫宏。在需要構(gòu)造語法抽象(比如 when )或新的binding 時(shí)再去用宏
  2. 寫一個(gè)宏使用的 demo,并手動(dòng)展開
  3. 使用 macroexpand , macroexpand-1 與 clojure.walk/macroexpand-all 去驗(yàn)證宏是如何工作的
  4. 在 REPL 中測(cè)試
  5. 如果一個(gè)宏比較復(fù)雜,盡可能拆分成多個(gè)函數(shù)

In Action

宏的一大應(yīng)用場(chǎng)景是流程控制,比如上面介紹的 when、when-let,以及各種 do 的衍生品 dotimes、doseq,我們的實(shí)戰(zhàn)也從這里入手,構(gòu)造一系列 do-primes,由淺入深介紹寫宏的技巧與注意事項(xiàng)。

(do-primes [n start end]
  body)

它會(huì)遍歷 [start, end) 范圍內(nèi)的素?cái)?shù),對(duì)于具體素?cái)?shù) n,執(zhí)行 body 里面的內(nèi)容。

do-primes

(defn prime? [n]
  (let [guard (int (Math/ceil (Math/sqrt n)))]
    (loop [i 2]
      (if (zero? (mod n i))
        false
        (if (= i guard)
          true
          (recur (inc i)))))))

(defn next-prime [n]
  (if (prime? n)
    n
    (recur (inc n))))

(defmacro do-primes [[variable start end] & body]
  `(loop [~variable ~start]
     (when (< ~variable ~end)
       (when (prime? ~variable)
         [email protected])
       (recur (next-prime (inc ~variable))))))

上面的實(shí)現(xiàn)比較直接,首先定義了兩個(gè)輔助函數(shù),然后通過返回由 loop 構(gòu)成的 code 來達(dá)到遍歷的效果。簡(jiǎn)單測(cè)試下:

(do-primes [n 2 13]
  (println n))

;; 展開為

(loop [n 2]
  (when (< n 13)
    (when (prime? n) (println n))
    (recur (next-prime (inc n)))))

;; 最終輸出 3 5 7 11

達(dá)到預(yù)期。但是這么實(shí)現(xiàn)會(huì)有些問題,比如傳入的start end 不是固定的數(shù)字,而是一個(gè)函數(shù),我們無法確定這個(gè)函數(shù)有無副作用,這就會(huì)導(dǎo)致重復(fù)執(zhí)行多次 end,這顯然不是我們想要的效果,需要進(jìn)行改造。

也許你會(huì)說,這個(gè)解決也很簡(jiǎn)單,在進(jìn)行 loop 之前,用一個(gè) let 先把 end 的值算出來,這個(gè)確實(shí)能解決多次執(zhí)行的問題,但是又引入另一個(gè)隱患: end 先于 start 執(zhí)行 。這會(huì)不會(huì)產(chǎn)生不良后果,我們同樣無法預(yù)知,我們能做到的就是 盡量不用暴露宏的實(shí)現(xiàn)細(xì)節(jié) ,盡量保證參數(shù)的求值順序。

(defmacro do-primes2 [[variable start end] & body]
  `(let [start# ~start
         end# ~end]
     (loop [~variable start#]
       (when (< ~variable end#)
         (when (prime? ~variable)
           [email protected])
         (recur (next-prime (inc ~variable)))))))

上面使用 gensym 機(jī)制來保證生產(chǎn) symbol 的唯一性,保證宏的“衛(wèi)生”( hygiene )。

(do-primes2 [n 2 (+ 10 (rand-int 30))]
  (println n))
;; 展開為
(let [start__17380__auto__ 2 end__17381__auto__ (+ 10 (rand-int 30))]
  (loop [n start__17380__auto__]
    (when (< n end__17381__auto__)
      (when (prime? n) (println n))
      (recur (next-prime (inc n))))))

only-once

通過上面的例子,我們也很容易的知道,gensym 是一種常用的技巧,所以我們完全有可能再進(jìn)行一次抽象,構(gòu)造 only-once 宏,來保證傳入的參數(shù)按照順序只執(zhí)行一次。

(defmacro only-once [names & body]
  (let [gensyms (repeatedly (count names) gensym)]
    `(let [[email protected](interleave gensyms (repeat '(gensym)))]
       `(let [[email protected](mapcat #(list %1 %2) gensyms names)]
          ~(let [[email protected](mapcat #(list %1 %2) names gensyms)]
             [email protected])))))

(defmacro do-primes3 [[variable start end] & body]
  (only-once [start end]
             `(loop [~variable ~start]
                (when (< ~variable ~end)
                  (when (prime? ~variable)
                    [email protected])
                  (recur (next-prime (inc ~variable)))))))

(do-primes3 [n 2 (+ 10 (rand-int 30))]
  (println n))

;; 展開為

only-once 的核心思想是用 gensym 來替換掉傳入的 symbol(即 names),為了達(dá)到這種效果,它首先定義出一組與參數(shù)數(shù)目相同的 gensyms(分別記為#s1 #s2),然后在第二層 let 為這些 gensyms 做 binding,value 也是用 gensym 生成的(分別記為#s3 #s4),這一層的 let 的返回值將內(nèi)嵌到 do-primes3 內(nèi):

(let [#s1 #s3 #s2 #s4]
  '(let [#s3 start #s3 end]
    (let [start #s1 end #s2]
      [email protected]))

第三層 let 的結(jié)果作為 code 內(nèi)嵌到調(diào)用 do-primes3 處,即最終的展開式:

(let [#s3 2 #s4 (+ 10 (rand-int 30))]
  (loop [n #s3]
    (when (< n #s4)
      (when (prime? n) (println n))
      (recur (next-prime (inc n))))))

根據(jù)上述分析過程,可以看到第四層嵌套的 let 先于第三層嵌套的 let 執(zhí)行,第四層 let 做 binding 時(shí),是把 #s1 對(duì)應(yīng)的 #s3 賦值給 start,#s2 對(duì)應(yīng)的 #s4 賦值給 end,這樣就成功的實(shí)現(xiàn)了 symbol 的替換。

only-once 屬于 macro-writing macro 的范疇,就是說它使用的對(duì)象本身還是個(gè)宏,所以有一定的難度,主要是分清不同表達(dá)式的求值環(huán)境,這一點(diǎn)對(duì)于理解指一類宏非常核心。不過這一類宏大家應(yīng)該很少能見到,更多的時(shí)候是使用輔助函數(shù)來分解復(fù)雜宏。比如我們這里就使用了兩個(gè)輔助函數(shù) prime? next-prime 來簡(jiǎn)化宏的寫法。

def-watched

作為實(shí)戰(zhàn)的最后一個(gè)例子,著重介紹 code 與 data 的聯(lián)系與區(qū)別。

def-watched 它可以定義一個(gè)受監(jiān)控的 var,在 root binding 改變時(shí)打印前后的值

(defmacro def-watched [name & value]
  `(do
     (def ~name [email protected])
     (add-watch (var ~name)
                :re-bind
                (fn [~'key ~'r old# new#]
                  (println '~name old# " -> " new#)))))

(def-watched foo 1)                  
(def foo 2)
;; 這時(shí)打印 foo 1 -> 2

為了簡(jiǎn)化 def-watched,可能會(huì)想把里面的函數(shù)提取出來:

(defn gen-watch-fn [name]
  (fn [k r o n]
    (println name ":" o " -> " n)))

(defmacro def-watched2 [name & value]
  `(do
     (def ~name [email protected])
     (add-watch (var ~name)
                :re-bind (gen-watch-fn '~name))))

(def-watched2 bar 1)                  
;; 展開為
(do (def bar 1) (add-watch #'bar :re-bind (gen-watch-fn 'bar)))

這時(shí)的效果和上面是一樣的,請(qǐng)注意這里是把 gen-watch-fn 實(shí)現(xiàn)為了函數(shù),如果用宏的話,會(huì)有什么效果呢?

;; 將 gen-watch-fn 改為 defmacro,其他均不變 
;; (def-watched2 bar 1) 展開后變成了
(do
  (def bar 1)
  (add-watch
    #'bar
    :re-bind
    #function[user/gen-watch-fn/fn--17288]))

這直接會(huì)報(bào) No matching ctor found for class #function[user/gen-watch-fn/fn–17288],由于 gen-watch-fn 是宏,它返回的是 code,而不是一般的 data,這也就是問題發(fā)生的緣由。

總結(jié)

本文一開始就明確指出 Lisp 中 code as data 的特性,這一點(diǎn)表面看似比較好理解,但是放到具體環(huán)境中時(shí),就十分容易搞錯(cuò)。

實(shí)戰(zhàn)部分給出了一些宏的管用技巧,介紹了相比來說難以理解的 macro-writing marco,理解它有一定難度,但也不是無法入手,理清 quote unquote 的作用機(jī)制,并且在 REPL 中不斷調(diào)試,肯定能有所收獲。

雖說不推薦使用宏解決問題,但是在有些時(shí)候,一個(gè)宏能省掉好幾十行代碼,而且能使邏輯更清晰,這時(shí)候也就不用“吝嗇”了。

最后,希望經(jīng)過這兩篇文章的介紹,大家能對(duì)宏有更深的理解。Happy Lisp!

 

來自:http://liujiacai.net/blog/2017/10/01/macro-in-action/

 

標(biāo)簽: isp 代碼

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請(qǐng)與原作者聯(lián)系。

上一篇:2017年排名前11的iOS應(yīng)用分析工具

下一篇:58 同城 iOS 客戶端搜索模塊組件化實(shí)踐