<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                ## 第 14 章 指代宏 第 9 章只是把變量捕捉視為一種問題 某種意料之外,并且只會搗亂的負面因素。本章將顯示變量捕捉 也可以被有建設性地使用。如果沒有這個特性,一些有用的宏就無法寫出來。 在 Lisp 程序里,下面這種需求并不鮮見:希望檢查一個表達式的返回值是否為非空,如果是的話,使用這個值做某些事。倘若求值表達式的代價比較大,那么通常必須這樣做: ~~~ (let ((result (big-long-calculation))) (if result (foo result))) ~~~ 難道就不能簡單一些,讓我們像英語里那樣,只要說: ~~~ (if (big-long-calculation) (foo it)) ~~~ 通過利用變量捕捉,我們可以寫一個?`if`,讓它以這種方式工作。 ### 14.1 指代的種種變形 在自然語言里,指代(anaphor) 是一種引用對話中曾提及事物的表達方式。英語中最常用的代詞可能要 算 "it" 了,就像在 "Get the wrench and put it on the table(拿個扳手,然后把它放在桌上)" 里那樣。指代給日常語言帶來了極大的便利 試想一下沒有它會發生什么 但它在編程語言里卻很少見。這在很大程度上是為了語言著想。指代表達式常會產生歧義,而當今的編程語言從設計上就無法處理這種二義性。 盡管如此,在 Lisp 程序中引入一種形式非常有限的代詞,同時避免歧義,還是有可能的。代詞,實際上是一種可捕捉的符號。我們可以通過指定某些符號,讓它們充當代詞,然后再編寫宏有意地捕捉這些符號,用這種方式來使用代詞。 在新版的?`if`?里,符號?`it`?就是那個我們想要捕捉的對象。`Anaphoricif`,簡稱?`aif`?,其定義如下: ~~~ (defmacro aif (test-form then-form &optional else-form) '(let ((it ,test-form)) (if it ,then-form ,else-form))) ~~~ 并如前例中那樣使用它: ~~~ (aif (big-long-calculation) (foo it)) ~~~ 當你使用?`aif`?時,符號?`it`?會被綁定到測試表達式返回的結果。在宏調用中,`it`?看起來是自由的,但事實上,在?`aif`?展開時,表達式?`(foo it)`?會被插入到一個上下文中,而?`it`?的綁定就位于該上下文: ~~~ (let ((it (big-long-calculation))) (if it (foo it) nil)) ~~~ 這樣一個在源代碼中貌似自由的符號就被宏展開綁定了。本章里所有的指代宏都使用了這種技術,并加以變化。 [示例代碼 14.1] 包含了一些 Common Lisp 操作符的指代變形。`aif`?下面是?`awhen`?,很明顯它是`when`?的指代版本: 原書勘誤:(acond (3))將返回 nil 而不是 3。后面的 acond2 也有同樣的問題。 * * * **[示例代碼 14.1] Common Lisp 操作符的指代變形** ~~~ (defmacro aif (test-form then-form &optional else-form) '(let ((it ,test-form)) (if it ,then-form ,else-form))) (defmacro awhen (test-form &body body) '(aif ,test-form (progn ,@body))) (defmacro awhile (expr &body body) '(do ((it ,expr ,expr)) ((not it)) ,@body)) (defmacro aand (&rest args) (cond ((null args) t) ((null (cdr args)) (car args)) (t '(aif ,(car args) (aand ,@(cdr args)))))) (defmacro acond (&rest clauses) (if (null clauses) nil (let ((cl1 (car clauses)) (sym (gensym))) '(let ((,sym ,(car cl1))) (if ,sym (let ((it ,sym)) ,@(cdr cl1)) (acond ,@(cdr clauses))))))) (awhen (big-long-calculation) (foo it) (bar it)) ~~~ * * * `aif`?和?`awhen`?都是經常會用到的,但?`awhile`?可能是這些指代宏中的唯一一個,被用到的機會比它的正常版的同胞兄弟?`while`?(定義于 7.4 節) 更多的宏。一般來說,如果一個程序需要等待(poll) 某個外部數據源的話,類似?`while`?和?`awhile`?這樣的宏就可以派上用場了。而且,如果你在等待一個數據源,除非你想做的僅是靜待它改變狀態,否則你肯定會想用從數據源那里獲得的數據做些什么: ~~~ (awhile (poll *fridge*) (eat it)) ~~~ aand 的定義和前面的幾個宏相比之下更復雜一些。它提供了一個?`and`?的指代版本;每次求值它的實參,it 都將被綁定到前一個參數返回的值上。 在實踐中,`aand`?傾向于在那些做條件查詢的程序中使用,例如這里: ~~~ (aand (owner x) (address it) (town it)) ~~~ 它返回?`x`?的擁有者(如果有的話) 的地址(如果有的話) 所屬的城鎮(如果有的話)。如果不使用?`aand`,該表達式就只能寫成: ~~~ (let ((own (owner x))) (if own (let ((adr (address own))) (if adr (town adr))))) ~~~ 盡管人們喜歡把?`and`?和?`or`?相提并論,但實現指代版本的?`or`?沒有什么意義。一個?`or`?表達式中的實參只有當它前面的實參求值到?`nil`?才會被求值,所以?`aor`?中的代詞將毫無用處。 從?`aand`?的定義可以看出,它的展開式將隨宏調用中的實參的數量而變。如果沒有實參,那么`aand`,將像正常的?`and`?那樣,應該直接返回?`t`?。否則會遞歸地生成展開式,每一步都會在嵌套的`aif`?鏈中產生一層: ~~~ (aif <first argument> <expansion for rest of arguments>) ~~~ `aand`?的展開必須在只剩下一個實參時終止,而不是像大多數遞歸函數那樣繼續展開,直到?`nil`?才停下來。 倘若遞歸過程一直進行下去,直到消去所有的合取式,那么最終的展開式將總是下面的模樣: ~~~ (aif <C> . . . (aif <Cn> t)...) ~~~ 這樣的表達式會一直返回?`t`?或者?`nil`?,因而上面的示例將無法正常工作。 第 10.4 節曾警告過:如果一個宏總是產生包含對其自身調用的展開式,那么展開過程將永不終止。雖然?`aand`?是遞歸的,但是它卻沒有這個問題,因為在基本情形里它的展開式沒有引用?`aand`。 最后一個例子是?`acond`?,它用于?`cond`?子句的其余部分想使用測試表達式的返回值的場合。(這種需求非常普遍,以至于 Scheme 專門提供了一種方式來使用?`cond`?子句中測試表達式的返回值。) 在?`acond`?子句的展開式里,測試結果一開始時將被保存在一個由?`gensym`?生成的變量里,目的是為了讓符號?`it`?的綁定只在子句的其余部分有效。當宏創建這些綁定時,它們應該總是在盡可能小的作用域里完成這些工作。這里,要是我們省掉了這個?`gensym`,同時直接把?`it`?綁定到測試表達式的結果上,就像這樣: ~~~ (defmacro acond (&rest clauses) ; wrong (if (null clauses) nil (let ((cl1 (car clauses))) '(let ((it ,(car cl1))) (if it (progn ,@(cdr cl1)) (acond ,@(cdr clauses))))))) ~~~ 那么it 綁定的作用域也將包括后續的測試表達式。 * * * **[示例代碼 14.2] 更多的指代變形** ~~~ (defmacro alambda (parms &body body) '(labels ((self ,parms ,@body)) #'self)) (defmacro ablock (tag &rest args) '(block ,tag ,(funcall (alambda (args) (case (length args) (0 nil) (1 (car args)) (t '(let ((it ,(car args))) ,(self (cdr args)))))) args))) ~~~ * * * [示例代碼 14.2] 有一些更復雜的指代變形。宏?`alambda`?是用來字面引用遞歸函數的。不過什么時候會需要字面引用遞歸函數呢?我們可以通過帶?`#'`?的 λ表達式來字面引用一個函數: ~~~ #'(lambda (x) (* x 2)) ~~~ 但正如第 2 章里解釋的那樣,你不能直接用λ–表達式來表達遞歸函數。代替的方法是,你必須借助`labels`?定義一個局部函數。下面這個函數(來自 2.8 節) ~~~ (defun count-instances (obj lsts) (labels ((instances-in (lst) (if (consp lst) (+ (if (eq (car lst) obj) 1 0) (instances-in (cdr lst))) 0))) (mapcar #'instances-in lsts))) ~~~ 接受一個對象和列表,并返回一個由列表中每個元素里含有的對象個數所組成的數列: ~~~ > (count-instances 'a '((a b c) (d a r p a) (d a r) (a a))) (1 2 1 2) ~~~ 通過代詞,我們可以將這些代碼變成字面遞歸函數。`alambda`?宏使用?`labels`?來創建函數,例如,這樣就可以用它來表達階乘函數: ~~~ (alambda (x) (if (= x 0) 1 (* x (self (1- x))))) ~~~ 使用?`alambda`?我們可以定義一個等價版本的?`count-instances`?,如下: ~~~ (defun count-instances (obj lists) (mapcar (alambda (list) (if list (+ (if (eq (car list) obj) 1 0) (self (cdr list))) 0)) lists)) ~~~ `alambda`?與 [示例代碼 14.1] 和 14.2 節里的其他宏不一樣,后者捕捉的是 it,而?`alambda`?則捕捉`self`。`alambda`?實例會展開進一個?`labels`?表達式,在這個表達式中,`self`?被綁定到正在定義的函數上。`alambda`?表達式不但更短小,而且看起來很像我們熟悉的?`lambda`?表達式,這讓使用`alambda`?表達式的代碼更容易閱讀。 這個新宏被用了在?`ablock`?的定義里,它是內置的?`block special form`?的一個指代版本。在`block`?里面,參數從左到右求值。在?`ablock`?里也是一樣,只是在這里,每次求值時變量?`it`?都會被綁定到前一個表達式的值上。 這個宏應謹慎使用。盡管很多時候?`ablock`?用起來很方便,但是它很可能會把本可以被寫得優雅漂亮的函數式程序弄成命令式程序的樣子。下面就是一個很不幸的反面教材: ~~~ > (ablock north-pole (princ "ho ") (princ it) (princ it) (return-from north-pole)) ho ho ho NIL ~~~ 如果一個宏,它有意地使用了變量捕捉,那么無論何時這個宏被導出到另一個包的時候,都必須同時導出那些被捕捉了的符號。例如,無論?`aif`?被導出到哪里,`it`?也應該同樣被導出到同樣的地方。否則出現在宏定義里的it 和宏調用里使用的?`it`?將會是不同的符號。 ### 14.2 失敗 在 Common Lisp 中符號?`nil`?身兼三職。它首先是一個空列表,也就是 ~~~ > (cdr '(a)) NIL ~~~ 除了空列表以外,nil 被用來表示邏輯假,例如這里 ~~~ > (= 1 0) NIL ~~~ 最后,函數返回 nil 以示失敗。例如,內置?`find-if`?的任務是返回列表中第一個滿足給定測試條件的元素。 如果沒有發現這樣的元素,find-if 將返回 nil : ~~~ > (find-if #'oddp '(2 4 6)) NIL ~~~ 不幸的是,我們無法分辨出這種情形:即 find-if 成功返回,而成功的原因是它發現了 nil : ~~~ > (find-if #'null '(2 nil 6)) NIL ~~~ 在實踐中,用 nil 來同時表示假和空列表并沒有招致太多的麻煩。事實上,這樣可能相當方便。然而,用nil 來表示失敗卻是一個痛處。因為它意味著一個像 find-if 這樣的函數,其返回的結果可能是有歧義的。 對于所有進行查找操作的函數,都會遇到如何區分失敗和 nil 返回值的問題。為了解決這個問題,Common Lisp 至少提供了三種方案。在多重返回值出現之前,最常用的方法是專門返回一個列表結構。例如,區分 assoc 的失敗就沒有任何麻煩;當執行成功時它返回成對的問題和答案: ~~~ > (setq synonyms '((yes . t) (no . nil))) ((YES . T) (NO)) > (assoc 'no synonyms) (NO) ~~~ 按照這個思路,如果擔心 find-if 帶來的歧義,我們可以用 member-if ,它不單單返回滿足測試的元素,而是返回以該元素開始的整個 cdr: > (member-if #'null '(2 nil 6)) (NIL 6) 自從多重返回值誕生之后,這個問題就有了另一個解決方案:用一個值代表數據,而用第二個值指出成功還是失敗。內置的gethash 就以這種方式工作。它總是返回兩個值,第二個值代表是否找到了什么東西: ~~~ > (setf edible (make-hash-table) (gethash 'olive-oil edible) t (gethash 'motor-oil edible) nil) NIL > (gethash 'motor-oil edible) NIL T ~~~ 如果你想要檢測所有三種可能的情況,可以用類似下面的寫法: ~~~ (defun edible? (x) (multiple-value-bind (val found?) (gethash x edible) (if found? (if val 'yes 'no) 'maybe))) ~~~ 這樣就可以把失敗和邏輯假區分開了: ~~~ > (mapcar #'edible? '(motor-oil olive-oil iguana)) (NO YES MAYBE) ~~~ Common Lisp 還支持第三種表示失敗的方法:讓訪問函數接受一個特殊對象作為參數,一般是用個 gensym,然后在失敗的時候返回這個對象。這種方法被用于 get ,它接受一個可選參數來表示當特定屬性沒有找到時返回的東西: ~~~ > (get 'life 'meaning (gensym)) #:G618 ~~~ 如果可以用多重返回值,那么 gethash 用的方法是最清楚的。我們不愿意像調用 get 那樣,為每個訪問函數都再傳入一個參數。并且和另外兩種替代方法相比,使用多重返回值更通用;可以讓 find-if 返回兩個值,而 gethash 卻不可能在不做 consing 的情況下被重寫成返回無歧義的列表。這樣在編寫新的用于查詢的函數,或者對于其他可能失敗的任務時,通常采用gethash 的方式會更好一些。 * * * **[示例代碼 14.3] 多值指代宏** ~~~ (defmacro aif2 (test &optional then else) (let ((win (gensym))) '(multiple-value-bind (it ,win) ,test (if (or it ,win) ,then ,else)))) (defmacro awhen2 (test &body body) '(aif2 ,test (progn ,@body))) (defmacro awhile2 (test &body body) (let ((flag (gensym))) '(let ((,flag t)) (while ,flag (aif2 ,test (progn ,@body) (setq ,flag nil)))))) (defmacro acond2 (&rest clauses) (if (null clauses) nil (let ((cl1 (car clauses)) (val (gensym)) (win (gensym))) '(multiple-value-bind (,val ,win) ,(car cl1) (if (or ,val ,win) (let ((it ,val)) ,@(cdr cl1)) (acond2 ,@(cdr clauses))))))) ~~~ * * * 在 edible? 里的寫法不過相當于一種記帳的操作,它被宏很好地隱藏了起來。對于類似 gethash 這樣的訪問函數,我們會需要一個新版本的 aif ,它綁定和測試的對象不再是同一個值,而是綁定第一個值,并測試第二個值。這個新版本的 aif ,稱為 aif2 ,由 [示例代碼 14.3] 給出。使用它,我們可以將 edible? 寫成: ~~~ (defun edible? (x) (aif2 (gethash x edible) (if it 'yes 'no) 'maybe)) ~~~ [示例代碼 14.3] 還包含有 awhen ,awhile ,和 acond 的類似替代版本。作為一個使用a cond2 的例子,見 18.4 節上 match 的定義。通過使用這個宏,我們可以用一個 cond 的形式來表達,否則函數將變得更長并且缺少對稱性。 內置的 read 指示錯誤的方式和 get 同出一轍。它接受一個可選參數來說明在遇到eof 時是否報錯,如果不報錯的話,將返回何值。[示例代碼 14.4] 中給出了另一個版本的 read ,它用第二個返回值指示失敗。read2 返回兩個值,分別是輸入表達式和一個標志,如果碰到eof 的話,這個標志就是nil 。它把一個 gensym 傳給 read ,萬一遇到 eof 就返回它,這免去了每次調用 read2 時構造 gensym 的麻煩,這個函數被定義成一個閉包,閉包中帶有一個編譯期生成的 gensym 的私有拷貝。 * * * **[示例代碼 14.4] 文件實用工具** ~~~ (let ((g (gensym))) (defun read2 (&optional (str *standard-input*)) (let ((val (read str nil g))) (unless (equal val g) (values val t))))) (defmacro do-file (filename &body body) (let ((str (gensym))) '(with-open-file (,str ,filename) (awhile2 (read2 ,str) ,@body)))) ~~~ * * * [示例代碼 14.4] 中還有一個宏,它可以方便地遍歷一個文件里的所有表達式,這個宏是用 awhile2 和 read2 寫成的。舉個例子,借助 do-file ,我們可以這樣實現 load : ~~~ (defun our-load (filename) (do-file filename (eval it))) ~~~ ### 14.3 引用透明(Referential Transparency) 有時認為是指代宏破壞了引用透明,Gelernter 和Jagannathan 是這樣定義引用透明的: 一個語言是引用透明的,如果 (a) 任意一個子表達式都可以替換成另一個子表達式,只要后者和前者的值相等,并且 (b) 在給定的上下文中,出現不同地方的同一表達式其取值都相同。 注意到這個標準針對的是語言,而不是程序。沒有一個帶賦值的語言是引用透明的。在下面的表達式中: ~~~ (list x (setq x (not x)) x) ~~~ 第一個和最后一個 x 帶有不同的值,因為被一個 setq 干預了。必須承認,這是丑陋的代碼。這一事實意味著 Lisp 不是引用透明的。 Norvig 提到,倘若把 if 重新定義成下面這樣將會很方便: ~~~ (defmacro if (test then &optional else) '(let ((that ,test)) (if that ,then ,else))) ~~~ 但 Norvig 否定它的理由,也正是因為這個宏破壞了引用透明。 盡管如此,這里的問題在于:上面的宏重定義了內置操作符,而不是因為它使用了代詞。上面定義中的 (b) 條款要求一個表達式 "在給定的上下文中" 必須總是返回相同的值。如果是在這個 let 表達式中就沒問題了, ~~~ (let ((that 'which)) ...) ~~~ 符號 that 表示一個新變量,因為 let 就是被用于創建一個新的上下文。 上面那個宏的錯誤在于,它重定義了 if,而 if 的本意并非是被用來創建新的上下文的。如果我們給指代宏取個自己的名字,問題就迎刃而解。(根據?**CLTL2**,重定義 if 總是非法的。) 由于 aif 定義的一部分就是建立一個新的上下文,并且在這個上下文中,it 是一個新變量,所以這樣一個宏并沒有破壞引用透明。 現在,aif 確實違背了另一個原則,它和引用透明無關:即,不管用什么辦法,新建立的變量都應該在源代碼里能很容易地分辨出來。前面的那個 let 表達式就清楚地表明 that 將指向一個新變量。可能會有反對意見,說:一個 aif 里面的 it 綁定就沒有那么明顯。盡管如此,這里有一個不大站得住腳的理由:aif 只創 建了一個變量,并且創建這個變量是我們使用 aif 的唯一理由。 Common Lisp 自己并沒有把這個原則奉為不可違背的金科玉律。**CLOS**?函數 call-next-method 的綁定依賴上下文的方式和 aif 函數體中符號 it 的綁定方式是一樣的。(關于 call-next-method 應如何實現的一個建議方案,可見 25.2 節上的 defmeth 宏。) 在任何情況下,這類原則的最終目的只有一個:提高程序的可讀性。并且代詞確實讓程序更容易閱讀,正如它們讓英語更容易閱讀那樣。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看