2012-03-21 41 views
16

在Peter Seibel的「Practical Common Lisp」一書中,我們可以一次性找到非常複雜的宏的定義(請參閱頁面底部的http://www.gigamonkeys.com/book/macros-defining-your-own.html)。瞭解如何實現一次只有lisp宏

我在過去3周裏第10次讀這個宏定義,並且無法理解它是如何工作的。 (更糟糕的是,我無法自行開發這個宏,即使我理解它的目的以及如何使用它。

我特別感興趣的是對這個出名的硬宏的系統「推導」,一步一步地! ?幫助

+2

請參閱此處的解釋:https://groups.google.com/forum/?fromgroups#!topic/comp.lang.lisp/F4NVRlOvrX8 – 2012-03-21 17:46:06

回答

24

你看這個:

(defmacro once-only ((&rest names) &body body) 
    (let ((gensyms (loop for n in names collect (gensym)))) 
    `(let (,@(loop for g in gensyms collect `(,g (gensym)))) 
     `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n))) 
     ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g))) 
      ,@body))))) 

這並不複雜,但它確實有一個嵌套的反引號,和多水平,這是彼此相似,導致易混淆,即使是經驗豐富的Lisp編碼器。

This是宏用來編寫擴展的宏:寫宏的部分宏的宏。

在宏本身中有一個普通的let,然後一個反向引用的生成的let它將住在使用once-only的宏體內。最後,在用戶使用宏的代碼站點中會出現一個雙重反引號let,它將出現在宏擴展宏中。

由於once-only本身就是一個宏,所以它必須是衛生的,因爲它本身就是衛生的;所以它在最外面的let中爲它自身產生了一堆gensyms。而且,once-only的目的是爲了簡化另一個衛生宏的編寫。所以它也會爲這個宏產生一個通用。

簡而言之,once-only需要創建一個宏擴展,它需要一些局部變量,其值爲gensyms。這些局部變量將被用於將gensyms插入到另一個宏擴展中以使其更衛生。而且這些局部變量本身就是衛生的,因爲它們是一個宏觀擴展,所以它們也是神經性的。

如果你正在編寫一個簡單的宏,你必須持有與gensyms局部變量,例如:

;; silly example 
(defmacro repeat-times (count-form &body forms) 
    (let ((counter-sym (gensym))) 
    `(loop for ,counter-sym below ,count-form do ,@forms))) 

在寫宏的過程中,你已經發明瞭一種符號,counter-sym。該變量在普通視圖中定義。你,人類,選擇它的方式不會與詞彙範圍內的任何事物發生衝突。有問題的詞彙範圍就是你的宏。我們不必擔心counter-sym意外地捕獲count-formforms中的引用,因爲forms只是插入到一段代碼中的數據,最終會插入到某個遠程詞法作用域(使用宏的站點)中。我們不必擔心counter-sym與我們的宏中的另一個變量混淆。例如,我們不能給我們的本地變量名稱count-form。爲什麼?因爲這個名字是我們的一個函數參數;我們會影響它,造成編程錯誤。

現在,如果您想要一個宏來幫助您編寫該宏,那麼該機器必須與您完成相同的工作。在編寫代碼時,它必須創建一個變量名稱,並且必須注意它的名稱。

但是,與您不同,代碼寫入機器沒有看到周圍的範圍。它不能簡單地查看哪些變量存在,並選擇哪些不會發生衝突。該機器只是一個函數,它需要一些參數(未評估的代碼片段)並生成一段代碼,然後在該機器完成其工作後,將代碼盲目地代入範圍。

因此,機器必須明智地選擇名稱。事實上,爲了完全防彈,它必須是偏執狂,並使用完全獨特的符號:gensyms。

所以繼續這個例子,假設我們有一個機器人爲我們寫這個宏體。這機器人可以是一個宏,repeat-times-writing-robot

(defmacro repeat-times (count-form &body forms) 
    (repeat-times-writing-robot count-form forms)) ;; macro call 

什麼可能的機器人宏觀樣子?

(defmacro repeat-times-writing-robot (count-form forms) 
    (let ((counter-sym-sym (gensym)))  ;; robot's gensym 
    `(let ((,counter-sym-sym (gensym))) ;; the ultimate gensym for the loop 
     `(loop for ,,counter-sym-sym below ,,count-form do ,@,forms)))) 

你可以看到這有一定的once-only的特點:雙嵌套和(gensym)的兩個層次。如果你能理解這一點,那麼到once-only的飛躍很小。當然,如果我們只是想讓一個機器人寫出重複次數,我們可以把它作爲一個函數,然後這個函數就不用擔心發明變量了:它不是一個宏,所以它不會「噸需要衛生:

;; i.e. regular code refactoring: a piece of code is moved into a helper function 
(defun repeat-times-writing-robot (count-form forms) 
    (let ((counter-sym (gensym))) 
    `(loop for ,counter-sym below ,count-form do ,@forms))) 

;; ... and then called: 
(defmacro repeat-times (count-form &body forms) 
    (repeat-times-writing-robot count-form forms)) ;; just a function now 

once-only不能是一個功能,因爲它工作是發明代表了老闆的變量,使用它的宏,函數無法引入變量到它的調用者。

+4

P.S.你可以感謝反引號標記使這種事情變得可行。沒有反引號,「一次只能」會是可怕的。只有在Lisp編程中具有「白癡專家」能力的人才能夠看穿它。反引號是Lisp宏寫的真正主力。 – Kaz 2012-03-21 19:04:05

+2

*嵌套時,反引號語法特別強大。這主要發生在宏定義宏 內;因爲這些代碼主要由嚮導編寫,所以編寫和解釋嵌套的反引號表達式的能力很快就被一定的神祕所包圍。麻省理工學院的Alan Bawden 在Lisp Machine的早期獲得了特別的反引號專家的聲譽。* - 「Lisp的演變」,Gabriel,Steele。 – Kaz 2012-03-21 19:04:39

+3

*反引號和DEFMACRO有很大不同。這種標準形式的 提供了表達能力的飛躍,開始了一個新的語言延伸浪潮,因爲它現在更容易以標準,便攜的方式定義新的語言結構,因此實驗性的方言可以共享 。*「 Lisp的演變「,Gabriel,Steele。 – Kaz 2012-03-21 19:05:16

7

實用公共Lisp的once-only宏的替代方法衍生於Let Over Lambda(參見第三章'僅限一次'一節)。

2

卡茲解釋得很漂亮和廣泛。

不過,如果你不會在乎雙衛生問題多,你可能會發現這一個更容易理解:

(defmacro once-only ((&rest symbols) &body body) 
    ;; copy-symbol may reuse the original symbol name 
    (let ((uninterned-symbols (mapcar 'copy-symbol symbols))) 
    ;; For the final macro expansion: 
    ;; Evaluate the forms in the original bound symbols into fresh bindings 
    ``(let (,,@(mapcar #'(lambda (uninterned-symbol symbol) 
          ``(,',uninterned-symbol ,,symbol)) 
         uninterned-symbols symbols)) 
     ;; For the macro that is using us: 
     ;; Bind the original symbols to the fresh symbols 
     ,(let (,@(mapcar #'(lambda (symbol uninterned-symbol) 
          `(,symbol ',uninterned-symbol)) 
         symbols uninterned-symbols)) 
      ,@body)))) 

第一let是反引號兩次,因爲它會成爲其中的一部分最後的擴張。目的是將原始綁定符號中的表單評估爲新綁定。

第二個let被反引用一次,因爲它將成爲once-only的用戶的一部分。其目的是將原始符號重新塑造成新的符號,因爲它們的形式將在最後的擴展中進行評估和綁定。

如果原始符號的重新綁定在最終的宏展開之前,最終的宏展開將引用未插入符號而不是原始表單。

使用once-onlywith-slots實現是需要雙衛生一個例子:

(defmacro with-slots ((&rest slots) obj &body body) 
    (once-only (obj) 
    `(symbol-macrolet (,@(mapcar #'(lambda (slot) 
            `(,slot (slot-value ,obj ',slot))) 
           slots)) 
     ,@body))) 

;;; Interaction in a REPL  
> (let ((*gensym-counter* 1) 
     (*print-circle* t) 
     (*print-level* 10)) 
    (pprint (macroexpand `(with-slots (a) (make-object-1) 
          ,(macroexpand `(with-slots (b) (make-object-2) 
              body)))))) 

;;; With the double-hygienic once-only 
(let ((#1=#:g2 (make-object-1))) 
    (symbol-macrolet ((a (slot-value #1# 'a))) 
    (let ((#2=#:g1 (make-object-2))) 
     (symbol-macrolet ((b (slot-value #2# 'b))) 
     body)))) 

;;; With this version of once-only 
(let ((#1=#:obj (make-object-1))) 
    (symbol-macrolet ((a (slot-value #1# 'a))) 
    (let ((#1# (make-object-2))) 
     (symbol-macrolet ((b (slot-value #1# 'b))) 
     body)))) 

第二膨脹示出內let被遮蔽結合至outter let的可變#:obj。因此,訪問內部with-slots內的a實際上將訪問第二個對象。

請注意,在此示例中,外部宏擴展獲得名爲g2的gensym和內部g1。在正常的評估或彙編中,情況會相反,因爲表格是從外部走向內部的。