第 18 章 解構

2018-02-24 15:54 更新

第 18 章 解構

解構(destructuring) 是賦值的一般形式。操作符?setq?和?setf?的賦值對象只是獨立的變量。而解構把賦值和訪問操作合二為一:在這里,我們不再只是把單個變量作為第一個參數(shù),而是給出一個關于變量的模式,在這個模式中,賦給每個變量的值將來自某個結構中對應的位置。

18.1 列表上的解構

從?CLTL2?開始,Common Lisp 包括了一個名為 destructuring-bind 的新宏。這個宏在第 7 章里簡單介紹過。這里將更仔細地了解它。假設 lst 是一個三元素列表,而我們想要綁定?x?到第一個元素,y?到第二個,z?到第三個。在原始?CLTL1?的 Common Lisp 里,只能這樣表達:

(let ((x (first lst))
    (y (second lst))
    (z (third lst)))
  ...)

借助新宏我們只需說:

(destructuring-bind (x y z) lst
    ...)

這樣處理,既短小,又清楚。讀者對于視覺形象的感受力比單純的文字要敏銳很多。使用后一種形式,x?,y?和?z?之間的關系可以一覽無余;而在前一種形式下,我們必須稍作思考才看得出來。

如果這樣簡單的情形都能通過使用解構而變得更清晰,試想一下它在更復雜情況下會帶來什么樣的改觀吧。destrucuring-bind?的第一個參數(shù)可以是任意復雜的一棵樹。想象

(destructuring-bind ((first last) (month day year) . notes)
  birthday
  ...)

如果用?let?和列表訪問函數(shù)來寫將會是什么樣子。這引出了另一個要點:解構使得程序更容易寫也更容易讀。

解構在?CLTL1?的 Common Lisp 里確實也有過。如果上例中的模式看起來眼熟的話,那是因為它們和宏的參數(shù)列表具有相同的形式。事實上,destructuring?就是就是用來處理宏參數(shù)列表的代碼,只不過現(xiàn)在拿出來單賣了。任何可以放進宏參數(shù)列表里的東西,你都可以把它置于這個匹配模式中,不過有個無關緊要的例外(那個?&environment?關鍵字)。

建立各種綁定總的來說是一個很有吸引力的想法。接下來的幾個小節(jié)會介紹這個主題的幾個變化。

18.2 其他結構

沒有理由把解構僅限于列表。解構同樣適用于各種復雜對象。本節(jié)展示如何編寫用于其他類型對象的類似 destructuring-bind 的宏。

下一步,自然是去處理一般性的序列。[示例代碼 18.1] 中定義了一個名為?dbind?的宏,它和destrucuring-bind?類似,不過可以用在任何種類的序列上。第二個參數(shù)可以是列表,向量或者它們的任意組合:


[示例代碼 18.1]:通用序列解構操作符

(defmacro dbind (pat seq &body body)
  (let ((gseq (gensym)))
    '(let ((,gseq ,seq))
      ,(dbind-ex (destruc pat gseq #'atom) body))))

(defun destruc (pat seq &optional (atom? #'atom) (n 0))
  (if (null pat)
    nil
    (let ((rest (cond ((funcall atom? pat) pat)
            ((eq (car pat) '&rest) (cadr pat))
            ((eq (car pat) '&body) (cadr pat))
            (t nil))))
      (if rest
        '((,rest (subseq ,seq ,n)))
        (let ((p (car pat))
            (rec (destruc (cdr pat) seq atom? (1+ n))))
          (if (funcall atom? p)
            (cons '(,p (elt ,seq ,n))
              rec)
            (let ((var (gensym)))
              (cons (cons '(,var (elt ,seq ,n))
                  (destruc p var atom?))
                rec))))))))

(defun dbind-ex (binds body)
  (if (null binds)
    '(progn ,@body)
    '(let ,(mapcar #'(lambda (b)
          (if (consp (car b))
            (car b)
            b))
        binds)
      ,(dbind-ex (mapcan #'(lambda (b)
            (if (consp (car b))
              (cdr b)))
          binds)
        body))))

> (dbind (a b c) #(1 2 3)
  (list a b c))
(1 2 3)
> (dbind (a (b c) d) '(1 #(2 3) 4)
  (list a b c d))
(1 2 3 4)
> (dbind (a (b . c) &rest d) '(1 "fribble" 2 3 4)
  (list a b c d))
(1 #\f "ribble" (2 3 4))

#(?讀取宏用于表示向量,而?#\?則用于表示字符。由于?"abc" = #(#\a #\b #\c),所以 "fribble" 的第一個元素是字符?#f?。為了簡單起見,dbind?只支持?&rest?和?&body?關鍵字。

和迄今為止見過的大多數(shù)宏相比,dbind?儼然是個龐然大物。這個宏的實現(xiàn)之所以值得好好研究一番,原因不僅僅是為了理解它的工作方式,更是為了它能給我們上一課,課的內(nèi)容對于 Lisp 編程是通用的。正如第 3.4 節(jié)提到的,我們在編寫 Lisp 程序時,可以有意識地讓它們更易于測試。在多數(shù)代碼里,我們必須要權衡這一訴求和代碼速度上的需求。幸運的是,如第 7.8 節(jié)所述,速度對于展開器代碼來說不是那么要緊。當編寫用來生成宏展開式的代碼時,我們可以讓自己放輕松一些。dbind的展開式由兩個函數(shù)生成,destruc?和?dbind-ex?。也許它們兩個可以被合并成一個函數(shù),一步到位。但是何苦呢?作為兩個獨立的函數(shù),它們將更容易測試。為什么要犧牲這個優(yōu)勢,換來我們并不需要的速度呢?

第一個函數(shù)是?destruc?,它遍歷匹配模式,將每個變量和運行期對應對象的位置關聯(lián)在一起:

(destruc '(a b c) 'seq #'atom) ((A (ELT SEQ 0)) (B (ELT SEQ 1)) (C (ELT SEQ 2)))

可選的第三個參數(shù)是個謂詞,它用來把模式的結構和模式的內(nèi)容區(qū)分開。

為了使訪問更有效率,一個新的變量(生成符號) 將被綁定到每個子序列上:

> (destruc '(a (b . c) &rest d) 'seq)
((A (ELT SEQ 0))
  ((#:G2 (ELT SEQ 1)) (B (ELT #:G2 0)) (C (SUBSEQ #:G2 1)))
  (D (SUBSEQ SEQ 2)))

destruc?的輸出被發(fā)送給?dbind-ex?,后者被用來生成宏展開代碼。它將?destruc?產(chǎn)生的樹轉(zhuǎn)化成一系列嵌套的?let?:

> (dbind-ex (destruc '(a (b . c) &rest d) 'seq) '(body))
(LET ((A (ELT SEQ 0))
    (#:G4 (ELT SEQ 1))
    (D (SUBSEQ SEQ 2)))
  (LET ((B (ELT #:G4 0))
      (C (SUBSEQ #:G4 1)))
    (PROGN BODY)))

注意到?dbind?,和?destructuring-bind?一樣,假設它將發(fā)現(xiàn)所有它尋找的列表結構。最后剩下的變量并不是簡單地綁定到nil ,就像?multiple-value-bind?那樣。如果運行期給出的序列里沒有包含所有期待的元素,解構操作符將產(chǎn)生一個錯誤:

> (dbind (a b c) (list 1 2))
>>Error: 2 is not a valid index for the sequence (1 2)

其他有內(nèi)部結構的對象該怎么處理呢?通常還有數(shù)組,它和向量的區(qū)別在于其維數(shù)可以大于一。如果我們?yōu)閿?shù)組定義解構宏,我們怎樣表達匹配模式呢?對于兩維數(shù)組,用列表還是比較實際的。[示例代碼 18.2] 含有一個宏【注1】,with-matrix?,用于解構兩維數(shù)組。

> (setq ar (make-array '(3 3)))
#<Simple-Array T (3 3) C2D39E>
> (for (r 0 2)
  (for (c 0 2)
    (setf (aref ar r c) (+ (* r 10) c))))
NIL
> (with-matrix ((a b c)
    (d e f)
    (g h i)) ar
  (list a b c d e f g h i))
(0 1 2 10 11 12 20 21 22)

對于大型數(shù)組,或者維數(shù)是3 或更高的數(shù)組來說,我們就需要另辟奚徑。我們不大可能把一個大數(shù)組里的每一個元素都綁定到變量上。將匹配模式做成數(shù)組的稀疏表達將會更實際一些 只包含用于少數(shù)元素的變量,加上用來標識它們的坐標。[示例代碼 18.2] 中的第二個宏就采用了這個思路。這里我們用它來得到前一個數(shù)組在對角線上的元素:


;; [示例代碼 18.2]:數(shù)組上的解構
(defmacro with-matrix (pats ar &body body)
  (let ((gar (gensym)))
    '(let ((,gar ,ar))
      (let ,(let ((row -1))
          (mapcan
            #'(lambda (pat)
              (incf row)
              (let ((col -1))
                (mapcar #'(lambda (p)
                    '(,p (aref ,gar
                        ,row
                        ,(incf col))))
                  pat)))
            pats))
        ,@body))))

(defmacro with-array (pat ar &body body)
  (let ((gar (gensym)))
    '(let ((,gar ,ar))
      (let ,(mapcar #'(lambda (p)
            '(,(car p) (aref ,gar ,@(cdr p))))
          pat)
        ,@body))))

> (with-array ((a 0 0) (d 1 1) (i 2 2)) ar
  (values a d i))
0
11
22

[示例代碼 18.3]:結構體上的解構

(defmacro with-struct ((name . fields) struct &body body)
  (let ((gs (gensym)))
    '(let ((,gs ,struct))
      (let ,(mapcar #'(lambda (f)
            '(,f (,(symb name f) ,gs)))
          fields)
        ,@body))))

通過這個新宏,我們開始逐漸跳出那些認為元素必須以固定順序出現(xiàn)的思維模式。我們可以做出一個類似形式的宏,用它來綁定變量到?defstruct?所建立的結構體字段上。[示例代碼 18.3] 中就這樣定義一個宏。模式中的第一個參數(shù)被接受為與結構體相關聯(lián)的前綴,其余的都是字段名。為了建立訪問調(diào)用,這個宏使用了?symb?(第 4.7 節(jié))。

> (defstruct visitor name title firm)
VISITOR
> (setq theo (make-visitor :name "Theodebert"
    :title 'king
    :firm 'franks))
#S(VISITOR NAME "Theodebert" TITLE KING FIRM FRANKS)
> (with-struct (visitor- name firm title) theo
  (list name firm title))
("Theodebert" FRANKS KING)

18.3 引用

CLTL?自帶了一個用于解構實例的宏。假設?tree?是一個帶有三個?slot?的類:species、age?和height?,而?my-tree?是一 個?tree?的實例。在

(with-slots (species age height) my-tree
  ...)

的里面我們可以像常規(guī)變量那樣引用?my-tree?的這些?slot。在?with-slots?的主體中,符號height?指向?height slot。height?并不是簡單地綁定到了對應?slot?里的變量,而是直接引用到那個?slot?上。所以,如果我們寫:

(setq height 72)

那么也將給?my-tree?的?height?這個?slot?一個?72?的值。這個宏的工作原理是將?height?定義為一個展開到?slot?引用的符號宏(第 7.11 節(jié))。事實上,symbol-macrolet?就是為了支持像?with-slots?這樣的宏才被加入到 Common Lisp 中的。

無論?with-slots?事實上是不是一個解構宏,它在實際編程中所起的作用和d estructuring-bind 是一樣的。雖然通常的解構都是按值調(diào)用(call-by-value),這種新型解構卻是按名調(diào)用(call-by-name)。無論我們?nèi)绾握{(diào)用它,它對我們都是有用的。還有其他什么宏,我們可以依法炮制呢?


[示例代碼 18.4] 序列上的引用解構

(defmacro with-places (pat seq &body body)
  (let ((gseq (gensym)))
    '(let ((,gseq ,seq))
      ,(wplac-ex (destruc pat gseq #'atom) body))))

(defun wplac-ex (binds body)
  (if (null binds)
    '(progn ,@body)
    '(symbol-macrolet ,(mapcar #'(lambda (b)
          (if (consp (car b))
            (car b)
            b))
        binds)
      ,(wplac-ex (mapcan #'(lambda (b)
            (if (consp (car b))
              (cdr b)))
          binds)
        body))))

我們可以這樣做:將解構宏展開成?symbol-macrolet?而不是?let?,這樣,就可以為任何解構宏創(chuàng)建出與之對應的按名調(diào)用版本。[示例代碼 18.4] 給出了一個被修改成與?with-slots?行為類似的dbind?版本。我們可以像使用 dbind 一樣來使用?with-places?:

> (with-places (a b c) #(1 2 3)
  (list a b c))
(1 2 3)

但這個新宏還給我們?setf?序列位置的選項,就像我們在?with-slots?里所做的那樣:

> (let ((lst '(1 (2 3) 4)))
  (with-places (a (b . c) d) lst
    (setf a 'uno)
    (setf c '(tre)))
  lst)
(UNO (2 TRE) 4)

就像在?with-slots?里那樣,這些變量現(xiàn)在都指向了序列中的對應位置。盡管如此,這里還有一個重要的區(qū)別:你必須使用?setf?而不是?setq?來設置這些偽變量。with-slots?宏必須引入一個code-walker(第 20.3 節(jié)) 來將其體內(nèi)的?setq?轉(zhuǎn)化成?setf。這里,寫一個?code-walker?將需要寫很多代碼,但是帶來的好處卻不大。

如果?with-places?比?dbind?更通用,為什么不干脆只用它呢?dbind?將一個變量關聯(lián)一個值上,而?with-places?卻是將變量關聯(lián)到一組用來找到一個值的指令集合上。每一個引用都需要進行一次查詢。

當?dbind?把?c?綁定到?(elt x 2)?的值上時,with-places?將使?c?成為一個展開成?(elt x 2)的符號宏。

所以如果c 在宏體中被求值了 次,那將會產(chǎn)生 次對elt 的調(diào)用。除非你真的想要 setf 那些由解構創(chuàng)建的變量,否則dbind 將會更快一些。

with-places?的定義和?dbind?([示例代碼 18.1]) 相比僅有輕微的變化。在?wplac-ex?(之前的dbind-ex) 中那些?let?變成了?symbol-macrolet?。通過類似的改動,我們也可以為任何正常的解構宏做出一個按名調(diào)用的版本。

18.4 匹配

正如解構是賦值的泛化,模式匹配是解構的泛化。"模式匹配" 這個術語有許多含義。在這里的語境中,它指的是這樣的操作:比較兩個結構,結構中可能含有變量,判斷是否存在某種給變量賦值的方式使得它們倆相等。例如,如果??x?和??y?是變量,那么這兩個列表

(p ?x ?y c ?x)
(p a b c a)

當??x = a?并且??y = b?時匹配。而列表

(p ?x b ?y a)
(p ?y b c a)

當??x = ?y = c?時匹配。

假設一個程序通過跟外部數(shù)據(jù)源交換信息的方式工作。為了回復一個消息,程序必須首先知道消息的類型,并且還要取出它的特定內(nèi)容。通過一個匹配操作符我們可以將這兩步并成一步。

要寫出這種操作符,必須先想出一種區(qū)分變量的辦法。我們不能直接把所有符號都當成變量,因為需要讓符號在模式中以參數(shù)的形式出現(xiàn)。這里我們規(guī)定:模式變量是以問號開始的符號。如果將來覺得不方便了,只要重定義謂詞var? 就可以改變這個約定。

[示例代碼 18.5] 包含一個模式匹配的函數(shù),它跟一些 Lisp 入門讀物里的匹配函數(shù)樣子差不多。我們傳給?match?兩個列表,如果它們可以匹配,將得到另一個列表,該列表會顯示它們是如何匹配的:

> (match '(p a b c a) '(p ?x ?y c ?x))
((?Y . B) (?X . A))
T
> (match '(p ?x b ?y a) '(p ?y b c a))
((?Y . C) (?X . ?Y))
T
> (match '(a b c) '(a a a))
NIL
NIL

在?match?逐個元素地比較它的參數(shù)時,它建立起來了一系列值和變量之間的賦值關系,這種關系被稱為綁定。這些變量是由參數(shù)?binds?給出的。若匹配成功,match?返回其生成的綁定,否則返回nil?。由于并非所有成功的匹配都能生成綁定,所以和 gethash 一樣,match?用第二個返回值來表示匹配成功與否:

> (match '(p ?x) '(p ?x))
NIL
T

[示例代碼 18.5] 匹配函數(shù)

(defun match (x y &optional binds)
  (acond2
    ((or (eql x y) (eql x '_) (eql y '_)) (values binds t))
    ((binding x binds) (match it y binds))
    ((binding y binds) (match x it binds))
    ((varsym? x) (values (cons (cons x y) binds) t))
    ((varsym? y) (values (cons (cons y x) binds) t))
    ((and (consp x) (consp y) (match (car x) (car y) binds))
      (match (cdr x) (cdr y) it))
    (t (values nil nil))))

(defun varsym? (x)
  (and (symbolp x) (eq (char (symbol-name x) 0) #\?)))

(defun binding (x binds)
  (labels ((recbind (x binds)
        (aif (assoc x binds)
          (or (recbind (cdr it) binds)
            it))))
    (let ((b (recbind x binds)))
      (values (cdr b) b))))

當?match?像上面那樣返回?nil?和?t?時,它表示一個沒有產(chǎn)生任何綁定的成功的匹配。

和?Prolog?一樣,match?也把?_?(下劃線) 用作通配符。它可以匹配任何東西,并且對綁定沒有任何影響:


[示例代碼 18.6]:慢的匹配操作符

> (match '(a ?x b) '(_ 1 _))
((?X . 1))
T

(defmacro if-match (pat seq then &optional else)
  '(aif2 (match ',pat ,seq)
    (let ,(mapcar #'(lambda (v)
          '(,v (binding ',v it)))
        (vars-in then #'atom))
      ,then)
    ,else))

(defun vars-in (expr &optional (atom? #'atom))
  (if (funcall atom? expr)
    (if (var? expr) (list expr))
    (union (vars-in (car expr) atom?)
      (vars-in (cdr expr) atom?))))

(defun var? (x)
  (and (symbolp x) (eq (char (symbol-name x) 0) #\?)))

有了?match?,可以很容易地寫出一個模式匹配版本的?dbind?。[示例代碼 18.6] 中含有一個稱為if-match?的宏。

像?dbind?那樣,它的前兩個參數(shù)是一個模式和一個序列,然后它通過比較模式跟序列來建立綁定。不過,它用另外兩個參數(shù)取代了代碼主體:一個?then?子句,在新綁定下被求值,如果匹配成功的話;以及一個?else?子句在匹配失敗時被求值。這里有一個簡單的使用?if-match?的函數(shù):

(defun abab (seq)
  (if-match (?x ?y ?x ?y) seq
    (values ?x ?y)
    nil))

如果匹配成功了,它將建立??x?和??y?的值,然后返回它們:

> (abab '(hi ho hi ho)
  HI
  HO

函數(shù)?vars-in?返回一個表達式中的所有匹配變量。它調(diào)用?var??來測試是否某個東西是一個變量。目前,var??和用來檢測綁定列表中變量的?varsym??([示例代碼 18.5]) 是相同的,之所以使用獨立的兩個函數(shù)是考慮到我們可能想要給這兩類變量采用不同的表示方法。

像在 [示例代碼 18.6] 里定義的那樣,if-match?很短,但并不是非常高效。它在運行期做的事情太多了。我們在運行期把兩個序列都遍歷了,盡管第一個序列在編譯期就是已知的。更糟糕的是,在進行匹配的過程中,我們構造列表來存放變量綁定。如果充分利用編譯期已知的信息,就能寫出一個既不做任何不必要的比較,也不做任何?cons?的?if-match?版本來。

如果其中一個序列在編譯期已知,并且只有這個序列里含有變量,那么就要另做打算了。在一次對 match 的調(diào)用中,兩個參數(shù)都可能含有變量。通過將變量限制在 if-match 的第一個參數(shù)上,就有可能在編譯期知道哪些變量將會參與匹配。這里,我們不再創(chuàng)建變量綁定的列表,而是將變量的值保存進這些變量本身。


[示例代碼 18.7] 快速匹配操作符

(defmacro if-match (pat seq then &optional else)
  '(let ,(mapcar #'(lambda (v) '(,v ',(gensym)))
      (vars-in pat #'simple?))
    (pat-match ,pat ,seq ,then ,else)))

(defmacro pat-match (pat seq then else)
  (if (simple? pat)
    (match1 '((,pat ,seq)) then else)
    (with-gensyms (gseq gelse)
      '(labels ((,gelse () ,else))
        ,(gen-match (cons (list gseq seq)
            (destruc pat gseq #'simple?))
          then
          '(,gelse))))))

(defun simple? (x) (or (atom x) (eq (car x) 'quote)))

(defun gen-match (refs then else)
  (if (null refs)
    then
    (let ((then (gen-match (cdr refs) then else)))
      (if (simple? (caar refs))
        (match1 refs then else)
        (gen-match (car refs) then else)))))

在 [示例代碼 18.7] 和 18.8 中是?if-match?的新版本。如果能預見到哪部分代碼會在運行期求值,我們不妨就直接在編譯期生成它。這里,我們生成的代碼僅僅完成需要的那些比較操作,而不是展開成對?match?的調(diào)用。

如果我們打算使用變量??x?來包含??x?的綁定的話,怎樣表達一個尚未被匹配過程建立綁定的變量呢?這里,我們將通過將一個模式變量綁定到一個生成符號以表明其未綁定。所以?if-match?一開始會生成代碼將所有模式中的變量綁定到生成符號上。在這種情況下,代替了展開成一個?with-gensyms?,在編譯期做一次符號生成,然后將它們直接插入進展開式是安全的。


[示例代碼 18.8] 快速匹配操作符(續(xù))

(defun match1 (refs then else)
  (dbind ((pat expr) . rest) refs
    (cond ((gensym? pat)
        '(let ((,pat ,expr))
          (if (and (typep ,pat 'sequence)
              ,(length-test pat rest))
            ,then
            ,else)))
      ((eq pat '_) then)
      ((var? pat)
        (let ((ge (gensym)))
          '(let ((,ge ,expr))
            (if (or (gensym? ,pat) (equal ,pat ,ge))
              (let ((,pat ,ge)) ,then)
              ,else))))
      (t '(if (equal ,pat ,expr) ,then ,else)))))

(defun gensym? (s)
  (and (symbolp s) (not (symbol-package s))))

(defun length-test (pat rest)
  (let ((fin (caadar (last rest))))
    (if (or (consp fin) (eq fin 'elt))
      '(= (length ,pat) ,(length rest))
      '(> (length ,pat) ,(- (length rest) 2)))))

其余的展開由?pat-match?完成。這個宏接受和?if-match?相同的參數(shù);唯一的區(qū)別是它不為模式變量建立任何新綁定。在某些情況下這是一個優(yōu)點,第 19 章將把?pat-match?作為一個獨立的操作符來使用。

在新的匹配操作符里,模式內(nèi)容和模式結構之間的差別將用函數(shù)?simple??定義。如果我們想要在模式里使用字面引用,那么解構代碼(以及?vars-in) 必須被告知不要進入那些第一個元素是?quote?的列表。在新的匹配操作符下,我們將可以使用列表作為模式元素,只需簡單地將它們引用起來。

與?dbind?相似,pat-match?調(diào)用?destruc?來得到一個將要在運行期參與其參數(shù)調(diào)用的列表。這個列表被傳給?gen-match?來為嵌套的模式遞歸生成匹配代碼,然后再傳給?match1?,以生成模式樹上每個葉子的匹配代碼。

最后出現(xiàn)在一個?if-match?展開式中的多數(shù)代碼都來自?match1?,如 [示例代碼 18.8], 這個函數(shù)分四種情況處理。如果模式參數(shù)是一個生成符號,那么它是一個由?destruc?創(chuàng)建用于保存子列表的不可見變量,并且所有我們需要在運行期做的就是測試它是否具有正確的長度。如果模式元素是一個通配符 (_),那么不需要生成任何代碼。如果模式元素是一個變量,那么?match1?會生成代碼去匹配,或者將其設置成,運行期給出的序列的對應部分。否則,模式元素被看作一個字面上的值,而 match1 會生成代碼去比較它和序列中的對應部分。

讓我們通過例子來了解一下展開式中的某些部分的生成過程。假設我們從下面的表達式開始

(if-match (?x 'a) seq
  (print ?x)
  nil)

這個模式將被傳給?destruc?,同時帶著一些生成符號(不妨簡稱為?g?) 來代表那個序列:

(destruc '(?x 'a) 'g #'simple?)

得到:

((?x (elt g 0)) ((quote a) (elt g 1)))

在這個列表的開頭我們接上?(g seq)

((g seq) (?x (elt g 0)) ((quote a) (elt g 1)))

然后把結果整個地發(fā)給?gen-match?。就像?length?(第 2.8 節(jié)) 的原生實現(xiàn)那樣,gen-match?首先一路遞歸到列表的結尾,然后在回來的路上構造其返回值。當gen-match 走完所有元素時,它就返回其?then?參數(shù),也就是?(print ?x)【注2】。在遞歸回來的路上,這個返回值將作為?then?參數(shù)傳給?match1??,F(xiàn)在我們將得到一個像這樣的調(diào)用:

(match1 '(((quote a) (elt g 1))) '(print ?x) '<else function>)

得到:

(if (equal (quote a) (elt g 1))
  (print ?x)

<else function>)

然后這些將成為另一個?match1?調(diào)用的?then?參數(shù),得到的值將成為最后的?match1?調(diào)用的?then參數(shù)。這個?if-match?的完整展開式顯示在[示例代碼 18.9] 【注3】中。


[示例代碼 18.9] 一個?if-match?的展開式

(if-match (?x 'a) seq
  (print ?x))

展開成:

(let ((?x '#:g1))
  (labels ((#:g3 nil nil))
    (let ((#:g2 seq))
      (if (and (typep #:g2 'sequence)
          (= (length #:g2) 2))
        (let ((#:g5 (elt #:g2 0)))
          (if (or (gensym? ?x) (equal ?x #:g5))
            (let ((?x #:g5))
              (if (equal 'a (elt #:g2 1))
                (print ?x)
                (#:g3)))
            (#:g3)))
        (#:g3)))))

在這個展開式里有兩個地方用到了?gensym?(生成符號),這兩個地方的用意各不相同。在運行時,一些變量被用來保存樹的一部分,這些變量的名字是用?gensym?生成的,目的是為了避免捕捉。而變量?x?在開始的? 時候被綁定到了一個?gensym,以表明它尚未被匹配操作賦給一個值。

在新的?if-match?中,模式元素現(xiàn)在是被求值而不再是被隱式引用了。這意味著 Lisp 變量可以被用于模式中,和被引用的表達式一樣:

> (let ((n 3))
  (if-match (?x n 'n '(a b)) '(1 3 n (a b))
    ?x))
1

還有兩個進一步的改進,是因為新版本調(diào)用了?destruc?([示例代碼 18.1]) 而出現(xiàn)?,F(xiàn)在模式中可以包含?&rest?或者?&body?關鍵字(match?是不管這一套的)。并且因為?destruc?使用了一般的序列操作符?elt?和?subseq?,

新的?if-match?將工作在任何類型的序列上。如果?abab?采用新版本來定義,它也可以被用于向量和字符串:

> (abab "abab")
#\a
#\b
> (abab #(1 2 1 2))
1
2

事實上,模式可以像 dbind 的模式那樣復雜:

> (if-match (?x (1 . ?y) . ?x) '((a b) #(1 2 3) a b)
  (values ?x ?y))
(A B)
#(2 3)

注意到,在第二個返回值里,向量的元素被顯示出來了。要想使向量以這種方式被輸出,需要將\*print-array\*?設置為?t?。

在本章,我們開始逐步走進一個嶄新的編程領域。以一個簡單的用于解構的宏作開端。在?if-match的最終版本中,我們有了某種看起來更像是它自己的語言的東西。接下來的章節(jié)將要介紹一整類程序,它們秉承的都是相同的理念。

備注:

【注1】譯者注:這里稍微修改了一下原書的代碼,原書中沒有定義?col?變量就直接使用了?(setq col -1),這里仿照?row?的處理方法用?let?建立了一個?col?的局部綁定。

【注2】譯者注:原文中說返回的?then?參數(shù)是??x?,這應該是個筆誤。

【注3】譯者注:原書里有一個筆誤,展開式代碼中的?(gensym? x)?應為?(gensym? ?x)。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號