Lisp-Stat 翻譯 —— 第三章 Lisp編程

第三章 Lisp編程

    上一章咱們使用了一些內建的Lisp函數和Lisp-Stat函數來運行一些有趣的運算。咱們構建的表達式中的一些仍是至關複雜的。當你發覺本身屢次鍵入相同的表達式的時候(固然你使用的數據可能略微有些不一樣),你天然就想爲這個表達式引入一些速記符,也就是說你想要定義本身的函數了。函數定義是Lisp編程的基礎操做。第2.7節已經對這個主題給出了一個簡單的介紹,如今是深刻探索的時候了。在對如何定義一個Lisp函數進行一個簡略的複習以後,咱們將檢驗一些須要的技術以開發功能更強大的函數:狀態求值、遞歸和迭代、局部變量、函數式數據、映射和賦值。 前端

    除了介紹定義函數的工具,本章也展現了一些Lisp編程經常使用編程技術和原則。尤爲地,對於本章的大多數狀況我都會使用函數式風格直至本章結束,目的是避免使用賦值方式改變變量的值(注:這裏提到的原則也是函數式的目的之一,即函數可使用外部變量,但在函數的整個執行過程當中不對變量進行寫操做,不破壞外部變量,這樣的函數要非破壞性函數,反之叫破壞性函數,在《Practical Common Lisp》和《On Lisp》裏你將接觸大量的非破壞性函數和他們的破壞性版本,都有其各自的書寫約定)。本章的開發嚴重依賴Abelson和Sussman的《Scheme:計算機程序結構與解釋》的前兩章。爲了容許咱們集中精力到編程過程自己,本章只使用咱們見過的基本類型數據:數值型、字符串型和列表型。Lisp和Lisp-Stat提供了若干額外的數據類型和用來操做這些數據類型的函數,這些能夠在自定義函數裏當作組件。下一章我將介紹一些這樣的函數。 算法

3.1 編寫簡單的函數

    Lisp函數須要使用特殊形式defun來定義。defun的基礎語法是(defun <name> <parameters> <body>),這裏的<name>是用來引用該函數的符號(即函數名),<parameters>是用來命名函數形參的符號列表,<body>即由一個或多個表達式組成。當調用函數的時候,<body>裏的表達式將按次序求值,最後一個表達式的求值結果將做爲函數的返回值。當前咱們不會使用函數體裏超過一個表達式的函數。defun的參數都不須要使用引號,由於defun不對參數求值。 數據庫

    舉個例子,這裏有個函數,功能是計算一個數據集的平方和,咱們能夠對一個數值列表使用這個函數,就像第二章裏使用的那些函數同樣: express

> (defun sum-of-squares (x)
    (sum (* x x)))
SUM-OF-SQUARES
> (sum-of-squares (list 1 2 3))
14
該定義使用了Lisp-Stat提供的矢量運算機制。函數sum將計算參數的每個項的和。

    也能夠定義一個帶多個參數的函數,例如,計算內積的函數能夠定義成這樣: 編程

> (defun my-inner-product (x y) (sum (* x y)))
MY-INNER-PRODUCT
我將將用my-inner-product函數,而不調用inner-product函數,目的就是不失去Lisp-Stat內部對inner-product函數的定義,Lisp-Stat對函數的定義比咱們定義的函數更縝密一些。

    使用defun定義的函數也能夠用來定義其它函數,例如,使用sum-of-squares函數,咱們能夠定義一個計算兩個數據列表歐幾里德距離的函數: 數組

> (defun distance (x y) 
    (sqrt (sum-of-squres (- x y))))
DISTANCE
    使用defun,咱們能夠開發函數做爲複雜表達式的速記符號。然而,咱們仍然被侷限在咱們能使用的表達式的能力裏。假設咱們想要將如下給定的絕對值表達式的定義轉換爲Lisp表達式。

爲了編寫這個函數,咱們須要一些謂詞來比較數字和0,還須要一個條件求值結構來對不一樣的表達式求值,這些表達式是根據謂詞範圍的結果獲得的。 閉包

3.2 謂詞和邏輯表達式

    謂詞就是函數用來肯定一個條件是真是假的詞項,謂詞對假條件返回nil,有時也返回non-nil,對於真條件,通常返回t。爲了比較數值,咱們可使用謂詞'<'、'>'、'='和其它比較謂詞。這些謂詞都要帶兩個貨更多參數。謂詞'<'的參數以升序排列,則返回真: app

> (< 1 2)
T
> (< 2 1)
NIL
> (< 1 2 3)
T
> (< 1 3 2)
NIL
謂詞'>'與'<'類似。謂詞'='當全部參數都相等時返回真:
> (= 1 2)
NIL
> (= 1 1)
T
> (= 1 1 1)
T
在Lisp-Stat裏 比較謂詞是矢量化的。'>'和'<'謂詞的非組合參數必須是實數;對於'='謂詞的其非組合參數必須是實數或者複數,其它類型的值,都會引起錯誤。

    使用特殊形式and和or,還有函數not,能夠在這些簡單的謂詞基礎上構建更復雜的謂詞。例如,咱們能夠定義一個函數來測試一個數字是否在區間(3, 5]內: 編程語言

> (defun in-range (x)
    (and (< 3 x) (<= x 5)))
IN-RANGE
特殊形式and帶兩個或更多參數,而後每次對一個參數求值,直到全部參數都求值過並返回真,或者其中一個爲假。一旦某一個參數爲假,就不須要對其它參數進行求值了。

    爲了測試一個參數是否再也不區間(3, 5]內,咱們可使用特殊形式or: 函數

> (defun not-in-range (x) (or (>= 3 x) (> x 5)))
NOT-IN-RANGE
與and類似,特殊形式or也帶兩個或更多參數,or每次對一個參數求值,直到一個參數爲真,或者全部參數都爲假是中止求值。

    定義not-in-range的另外一種方式是使用in-range函數與not函數的聯合:

> (defun not-in-range (x) (or (>= 3 x) (> x 5)))
NOT-IN-RANGE
練習 3.1

略。

3.3 條件求值

    最基本的Lisp條件求值結構是cond。使用cond和比較謂詞剛纔已經介紹過了,咱們能夠定義一個函數來計算一個數值的絕對值:

> (defun my-abs (x)
    (cond ((> x 0) x)
          ((= x 0) 0)
          ((< x 0) (- x))))
MY-ABS
條件表達式的通常形式以下:

(cond (<p 1> <e 1>)

         (<p 2> <e 2>)

         ...

         (<p n> <e n>))

相似列表(<p 1> <e 1>)的形式叫作條件語句。每一個條件語句的第一個表達式,好比<p 1>,... ,<p n>是謂詞表達式,當真的時候求值爲非nil,當假的時候求值爲nil。cond一次只運行一條語句,它求值表達式直到一個表達式爲真。若是其中的一個謂詞爲真,好比說就是<p i>,那麼它對應的序列表達式<e i>將被求值,它的結果做爲cond表達式的結果。若是沒有一個謂詞爲真,則返回nil。

    在my-abs函數的定義裏,我爲3個條件語句使用了完整的謂詞表達式。由於最後一個條件語句是一個默認值,我可使用符號t做爲謂詞表達式。my-abs函數應該看起來是這樣的:

> (defun my-abs (x)
    (cond ((> x 0) x)
      ((= x 0) 0)
      (t (-x)))) MY-ABS
由於最後一條語句的謂詞總爲真,因此上邊的語句沒有使用就用這條語句。

    條件語句可能有好幾條結果表達式,這些表達式按順序執行,最終的求值結果將被返回。最終的那個表達式以前的表達式只對它們產生的反作用是有用的。例如,咱們可使用print函數和terpri函數來修改my-abs函數的定義,這樣能夠打印一條消息以指示哪條條件語句被使用了。print函數發送一個西航給解釋器,該消息後緊跟一個它自身參數表達式的打印體和一個空格;terpri函數向解釋器發送一個新行。新的定義以下:

> (defun my-abs (x)
    (cond ((> x 0) (print 'first-clause) (terpri) x)
      ((= x 0) (print 'second-clause) (terpri) 0)
      ((< x 0) (print 'third-clause) (terpri) (-x))))
MY-ABS
當使用函數的時候,將提供一些關於求值過程的信息。
> (my-abs 3)

FIRST-CLAUSE 
3
> (my-abs -3)

THIRD-CLAUSE 
3

print函數的使用在這個特殊的例子裏固然是愚蠢的,可是在一些複雜的函數裏它可能會成爲一個頗有用的調試工具。

    除了cond,Lisp還提供了一些其它的條件計算結構。它們中最重要的就是if。特殊形式if帶3個表達式,一個謂詞表達式,一個結果項,一個替代項。首先計算謂詞表達式,若是爲真,那麼對結果項求值,結果項返回值做爲if的返回值;不然,對替表明達式求值,並返回它的結果,若是沒有提供這個可選的替表明達式,默認返回nil。使用if咱們能夠這麼定義my-abs函數:

> (defun my-abs (x)
    (if (> x 0)
        x
        (- x)))
MY-ABS
if表達式的通常形式與cond表達式是等價的。

練習 3.2

略。

3.4 迭代和遞歸

    Lisp是一種遞歸語言,其結果,在這一點上咱們已經有足夠的工具編寫函數來踐行數值計算,這些數值計算在其它語言裏是須要特殊的迭代結構的。Lisp確實已經提供了一些迭代工具,下面咱們就會看到一些,可是首先檢測在僅僅使用遞歸的狀況下能在多大程度上完成任務這點上是頗有用的。

    舉個例子,讓咱們看一個計算數字x的平方根的算法。這個算法以對√x的起始猜想值y開始,而後像下式同樣計算一個持續改進的猜想值:

這個過程將重複進行,直到當前參測值的平方與x足夠接近爲止。例如,對於√3,其起始猜想值爲1,咱們將進行如下步驟:


    該算法能夠追溯到公元一世界,是牛頓法解方程根的一個特例。咱們可使用Lisp來表示這個算法,經過定義一個函數sqrt-iter,它帶一個初始猜想值和數值x,而後計算近似平方根值:

> (defun sqrt-iter (guess x)
    (if (good-enough-p guess x)
        guess
        (sqrt-iter (improve guess x) x)))
SQRT-ITER
若是當前猜想值是一個滿意值,返回它。不然,計算過程將使用一個改進的猜想值重複運行。

    這個函數須要兩個額外的函數,計算新猜想值的improve函數,還有測試當前值是否足夠接近的good-enough-p函數。改進的函數能夠簡單地對上邊給出的步驟編碼,並定義爲下邊的樣子:

> (defun improve (guess x)
    (mean (list guess (/ x guess))))
IMPROVE
收斂測試須要一個收斂準則的選擇和一個臨界值,簡單定義以下:
> (defun good-enough-p (guess x)
    (< (abs (- (* guess guess) x)) 0.001))
GOOD-ENOUGH-P
將0.001做爲臨界值的選擇固然是隨意的。

    使用sqrt-iter,咱們能夠定義一個平方根函數:

> (defun my-sqrt (x) 
    (sqrt-iter 1 x))
MY-SQRT
測試一下:
> (my-sqrt 9)
3.00009155413138
函數sqrt-iter的定義是遞歸的,也就是說,它使用本身自己定義本身。可是由該函數產生的計算過程,與接下來的計算一個正整數的factorial數列的遞歸函數產生的過程是有些不一樣的:
> (defun factorial (n)
    (if (= n 1)
        1
        (* n (factorial (- n 1)))))
FACTORIAL
這裏factorial函數的值,當n爲1的時候是1;當n>1時,按n(n-1)!計算。當這個factorial函數使用一個參數的時候,它創建一系列的遞延操做直到它最後達到那個基礎條件(本例就是n=1)爲止,而後計算它們之間的相乘。爲了持續跟蹤這些遞延的計算,須要的存儲空間容量與計算過程當中的步驟數量是呈現線性關係的。這樣的計算過程叫線性遞歸。

    與之相反,sqrt-iter函數產生的計算過程不須要持續追蹤任何遞延計算。全部的過程的狀態信息都包含在一個單獨的狀態變量裏,即當前的猜想值這一變量。能夠用肯定數目的狀態變量來描述的計算過程叫作可迭代過程。

    定義factorial函數的一個可迭代版本也是可能的。爲了達到這一目的,咱們能使用一個counter變量和一個product變量做爲狀態變量。每一步驟product變量都會與counter變量相乘,counter變量遞增,直到counter變量大於n爲止:

> (defun fact-iter (product counter n)
    (if (> counter n)
        product
        (fact-iter (* counter product) (+ counter 1) n)))
FACT-ITER
如今咱們能使用fact-iter函數,其參數pruduct和counter都爲1,來定義factorial函數:
> (defun factorial (n) 
    (fact-iter 1 1 n))
FACTORIAL
    sqrt-iter和fact-iter這兩個輔助函數在結構上很類似。由於定義一個可以產生迭代運算過程的函數,一般須要定義一個帶這種結構的輔助函數,Lisp提供了一個能夠簡化這個過程的 特殊形式,同時避免必定要爲這個輔助函數想一個名字。這個 特殊形式就是do。一個do表達式語法的簡化版本以下:

(do ((<name 1> <initial 1> <update 1>)

     ...

      (<name n> <initial n> <update n>))

     (<test> <result>))

有兩個參數:一個三元列表,後邊還跟着一個列表,這個列表由一個終止測試表達式和一個結果表達式組成。三元表達式的每一項都由三部分組成,一個狀態變量名,一個初始化表達式和一個更新表達式。do從對一個初始表達式求值開始,而後將表達式的值與狀態變量綁定;接下來使用迭代變量對終止表達式求值,若是終止表達式的結果爲真,結果對錶達式求值並做爲結果返回,不然,計算更新表達式,變量將與其新值綁定,計算終止測試,等等。

    使用do咱們能夠如此編寫my-sqrt函數:

> (defun my-sqrt (x)
    (do ((guess 1 (improve guess x)))
        ((good-enough-p guess x) guess)))
MY-SQRT
如此,factorial函數變成:
> (defun factorial (n)
    (do ((counter 1 (+ 1 counter))
         (product 1 (* counter product)))
        ((> counter n) product)))
FACTORIAL
將這裏的定義與sqrt-iter和fact-iter函數的原始定義進行比較,代表原始定義的每一塊代碼都將一個槽放進了do的結構裏。對函數fact-iter初始化調用的參數表達式變成了初始化表達式,遞歸調用fact-iter函數時的參數表達式變成了更新表達式,函數fact-iter裏的if表達式的test變成了do表達式的test表達式,if裏的結果表達式變成了do的結果表達式。

    do結構是很是強大的,可是它也是有一點恐怖的。對於使用sqrt-iter或者fact-iter函數的輔助函數來講,do能夠很容易地想出一個簡單的替代函數。重要的一點是,這裏的初始化表達式和更新表達式是並行計算的,不是串行的。結果,三元結構元素的順序如何指定是沒有關係的。這與給一個沒有調用的輔助函數傳遞參數的方式是相似的,函數調用以前,它的參數順序是無所謂的。

練習 3.3

略。

練習 3.4

略。

3.5 環境

    使用def和defun定義的變量和函數是全局變量和全局函數,一旦有變量或函數被定義,它們將持續存在直到它們被從新定義、取消定義,或者你結束了與解釋器的會話。咱們已經見過變量其它類型了:一個函數的參數在函數體裏被用做局部變量。Lisp提供了一些其它的方法來構造局部變量,也容許你構造本身的局部函數。爲了可以有效地使用這些方法,咱們須要介紹一些新的想法和術語。

3.5.1 一些術語

    讓咱們從一個很簡單的例子開始。假設咱們有一個全局變量是這麼定義的:

> (def x 1)
X
而後再定義一個f函數:
> (defun f (x)
    (+ x 1))
F
當咱們使用一個參數的時候,好比是2,當f被調用的時候,f函數體裏的變量x將引用這個參數:
> (f 2)
3
    一個變量和一個值得配對叫作綁定。在這個例子裏,變量x與數值1有一個全局的綁定。當使用參數2調用函數的時候,建立了x的局部綁定,函數體將使用該局部綁定進行求值,這個局部綁定將屏蔽全局綁定。在特定時間綁定有效的那個變量集合叫作環境。一個特定綁定應用的表達式集合叫作這個綁定的做用域。在這個例子裏,f函數體在這樣的環境裏求值,該環境由x與參數值的局部綁定組成,這個局部綁定的做用域是f的函數體。

     下載,讓咱們作一點兒改變。定義另外一個全局變量a:

> (def a 10)
而後重定義函數f:
> (defun f (x) (+ x a))
f的函數體如今引用了兩個變量,x和a。變量x與函數的參數綁定,所以叫綁定變量。變量a沒有與任何參數綁定,所以叫自由變量,函數f使用參數2時的會發生什麼,該預測不會太難。
> (f 2)
12
自由變量a的值能夠來自全局環境或者null環境。

    可是假設函數f從函數內部調用,例如,定義函數g:

> (defun g (a) (f 2))
表達式(g 20)的值是多少呢?爲了回答這個問題,咱們須要知道Lisp用來肯定自由變量的值的規則。這裏有幾種狀況。一個就是在函數被調用的環境裏查找自由變量的值,這個規則叫動態做用域。在咱們的例子裏,當f被調用時,變量a和數值20進行了綁定,表達式(g 20)的結果將是22。另外一個方法是在函數原始定義裏查找自由變量的值,這個叫靜態做用域和詞法做用域。由於函數f是在局部環境裏定義的,該方法將在局部環境裏查找a的值,在那裏a是與10綁定的,表達式(g 20)的返回值是12。

    Common Lisp使用靜態做用域規則,因此:

> (g 20)
12
當一個Common Lisp函數做用到一個參數集合上的時候,一個新的環境創建起來了,函數和它的參數 在這個環境裏創建起來。而後,函數體裏的表達式在這個環境裏求值。

練習 3.5

略。

3.5.2 局部變量

    定義函數是設置局部環境的一個方法,該環境用來計算一個表達式集合,也就是函數體。另外一個方法就是使用特殊形式let,這在上邊的第2.7節簡單介紹過。let表達式的通常語法是:

(let ((<var 1> <e 1>)

      ...

      (<var n> <e n>))

    <body>)

當let表達式求值的時候,值表達式<e 1>, ..., <e n>首先求值。而後一個let環境被設置成功,該環境包圍了let表達式的環境組成,變量<var 1>, ..., <var n>與表達式的結果<e 1>, ..., <e n>是綁定的。最後,<body>裏的表達式在這個新環境裏被求值,舊的環境被保存起來。let表達式返回的結果就是<body>裏最後一個表達式的求值結果。

    特殊形式let在設置局部變量來簡化表達式方面是很是重要的。舉個簡單的例子,假設咱們想要定義一個函數,它帶兩個參數,呈現爲實向量形式,而後返回其中一個向量在另外一個向量上的投影。若是<x, y>表示表明x和y的內積,那麼y在x上的投影能夠如此給定:<x, y>/<x, x> *x。爲了計算這個投影,咱們能夠這樣定義投影函數:

> (defun project (y x)
    (* (/ (sum (* x y)) (sum (* x x))) x))
PROJECT
如今,咱們能夠找到向量(1 3)在向量(1 1)上的投影:

> (project '(1 3) '(1 1))
(2 2)
    project函數的函數體不算太複雜,可是咱們能夠經過將兩個內積寫成一個局部變量進一步簡化表達式。

> (defun project (y x)
    (let ((ip-xy (sum (* x y)))
          (ip-xx (sum (* x x))))
      (* (/ ip-xy ip-xx) x))) PROJECT
設置局部變量ip-xy和ip-xx來表示<x, y>和<x, x>的內積,在let表達式體裏用來計算投影值。這個表達式與上邊給出的數學表達式很接近,很容易檢查。

    關於let的一個重點是,它構造的綁定是並行的。新的局部變量的值相對的表達式在包圍他們的環境中被計算,而後再設置綁定,爲了說明這一點,讓咱們看一個簡單的,人造的例子:

> (defun f (x)
    (let ((x (/ x 2))
          (y (+ x 1)))
      (* x y)))
F
使用參數4來調用這個表達式的結果是:
> (f 4)
10
首先,這個結果可能讓人吃驚,你可能認爲結果是6.可是若是你使用這裏給定的求值規則該函數就會有意義。變量x在(/ x 2)和(+ x 1)兩個表達式裏均被引用,x是在let環境裏的,該變量對應f函數的參數。當對錶達式(f 4)求值時,該變量的值是4。一旦這兩個表達式求值時,一個新的環境就創建了,其中x與2綁定,y與5綁定。這個對x的新的綁定覆蓋了來自周圍環境的綁定,該變量的舊值在離開let表達式體的時候仍然是有效的。let表達式的值就是在這種綁定條件下的(* x y)的值,所以該值是10。

    在序列化地設置局部變量的時候有時是有用的,即首先定義一個變量,而後在第一個變量後定義第二個變量。在project函數的定義裏,咱們可能想要定義一個變量來表示x的係數,好比之內積變量的論。因爲let綁定的並行分配,這些可使用一個單獨的let表達式完成,可是這可能須要使用連個小括號:

> (defun project (y x)
    (let ((ip-xy (sum (* x y)))
          (ip-xx (sum (* x x))))
      (let ((coef (/ ip-xy ip-xx)))
        (* coef x))))
這是個共性問題,Lisp提供了一個簡單地方法。特殊相識let*與let的工做方式相似,除了它每次都會向所在的環境進行綁定,而後計算在一個環境裏對錶達式求值,包括目前構造的全部的綁定。使用let*,咱們能夠講函數寫成:
> (defun project (y x)
    (let* ((ip-xy (sum (* x y)))
           (ip-xx (sum (* x x)))
           (coef (/ ip-xy ip-xx)))
      (* coef x)))
PROJECT
    在上邊那我的工的案例裏,若是咱們使用let*來代替let,那麼:
> (defun f (x)
    (let* ((x (/ x 2))
           (y (+ x 1)))
      (* x y)))
F
而後,結果應該就是6:
> (f 4)
6

3.5.3 局部函數

    除了變量綁定以外,環境也包含函數綁定。目前爲止,咱們已經經過使用defun來定義全局函數綁定。特殊形式flet能夠用來創建局部函數定義,該定義僅在flet表達式內部是可見的。它容許你以輔助函數的形式定義一個函數,而不須要擔憂與其它全局函數的命名衝突。

    flet表達式的通常形式以下:

(flet ((<name 1> <parameters 1> <body 1>)

       ...

       (<name n> <parameters n> <body n>))

    <body>)

符號<name 1>, ..., <name n>是局部函數的名字,列表<parameters 1>, ..., <parameters n>是參數列表,<body 1>, ..., <body n>是組成函數體的表達式或表達式序列。

    舉個例子,咱們可使用另外一種方式編寫咱們的project函數。不使用局部變量表示內積,而使用一個叫ip的局部內積函數來表示:

> (defun project (y x)
    (flet ((ip (x y) (sum (* x y))))
      (* (/ (ip x y) (ip x x)) x)))
PROJECT
    與let相似,flet在包圍它的環境裏並行地構建綁定,這表示使用一個具體的flet定義的函數都不能互相引用,也不能自引用。爲了定義第二個局部函數coef,在ip函數裏咱們不得不使用第二個flet:
> (defun project (y x)
    (flet ((ip (x y) (sum (* x y))))
      (flet ((coef (x y) (/ (ip x y) (ip x x))))
        (* (coef x y) x))))
PROJECT
Lisp還提供了一個簡單的替換符——特殊形式labels。就像let*同樣,labels順序地定義它的綁定,容許每個函數引用前邊定義的函數,或者引用其自身。那麼labels容許你定義一個遞歸的局部函數。對於咱們的project函數咱們能夠這樣使用labels:
> (defun project (y x)
    (labels ((ip (x y) (sum (* x y)))
             (coef (x y) (/ (ip x y) (ip x x))))
      (* (coef x y) x)))
PROJECT

3.6 做爲數據的函數和表達式

Lisp最強大的能力就是能將函數做爲數據,來構建新的自定義函數。本節介紹一些利用這項能力的函數和技術。

3.6.1 匿名函數

在第2.7節,咱們繪製了函數f(x)=2x+在區間[-2, 3]上的圖形,咱們首先定義了函數f:

> (defun f (x) (+ (* 2 x) (^ x 2)))
F
而後計算表達式(plot-function #'f -2 3)。

    一旦咱們繪製出圖形,咱們就再也不使用函數f了。若是咱們能避免正式地定義函數和必須爲它想個名字的話,彷佛是件極好的事兒。相同的問題在數學裏也存在:爲了描述我要繪製的函數曲線,我引入了"函數f(x)=2x+"。爲了解決這個問題,邏輯學家開發了lambda演算,容許你使用以下表達式: λ(x)(2x+x²)來引用「對於參數x的返回值爲2x+x²的函數」這一表述。Lisp也採用了這一方法,容許函數描述成lambda表達式,一個由符號lambda組成的列表,一個參數列表,和組成函數體的一個或多個表達式。咱們這個函數的lambda表達式形式以下:

> (lambda (x) (+ (* 2 x) (^ x 2)))
#<Closure: #142d008>
由於使用λ表達式描述的函數沒有名字,它們有時叫作匿名函數。

    lambda表達式能夠用來替換符號,做爲傳給解釋器的表達式的第一個元素:

> ((lambda (x) (+ (* 2 x) (^ x 2))) 1)
3
lambda表達式能夠做爲參數傳遞給相似plot-function這樣的函數。這個匿名函數須要經過首先與當前環境的組合來完成,而後用來肯定自由變量的值。這個組合叫作函數閉包,或者簡稱閉包。閉包將使用特殊形式function來構造,或者它的縮寫形式#'。那麼,爲了繪製咱們的函數,咱們應該使用以下表達式:
> (plot-function #'(lambda (x) (+ (* 2 x) (^ x 2))) -2 3)
#<Object: 1429688, prototype = SCATTERPLOT-PROTO>
鏈接函數閉包裏的環境和函數定義的能力,是一項極其強大的編程工具。然而,爲了可以充分利用該想法的優點,咱們須要檢測如何定義一個接受函數做爲參數的函數。

3.6.2 使用函數參數

假設咱們想要近似一個積分值:

一個方法就是使用第3.4節介紹的do結構,來定義一個積分函數:

> (defun integral (a b h)
    (do ((itegral 0 (+ integral (* h (f x))))
         (x (+ a (/ h 2)) (+ x h)))
        ((> x b) integral)))
INTEGRAL
這個定義假設有一個全局定義的函數f,它將計算被積分函數。例如,爲了計算x²在區間[0, 1]上的積分,咱們能夠這樣定義函數f:
> (defun f (x) (^ x 2))
F
而後使用integral函數計算積分:
> (integral 0 1 .01)
0.333325
    最好設計一個接受被積分函數做爲參數。爲了可以作到這一點,咱們須要知道如何使用這個函數參數。首先,咱們能夠這樣定位integral函數:
> (defun integral (f a b h)
    (do ((integral 0 (+ integral (* h (f x))))
         (x (+ a (/ h 2)) (+ x h)))
        ((> x b) integral)))
INTEGRAL
而後使用以下表達式進行積分:
> (integral #'(lambda (x) (^ x 2)) 0 1 .01)
0.3333250000000004
不幸的是,該表達式不起做用。緣由是局部變量f將咱們的函數做爲它的值,而不是做爲函數定義。(注:我在Lisp-Stat上運行是沒有問題的,不知做者爲何這麼說!!!)

    相反地,咱們可使用funcall函數,這個函數帶一個函數參數,還有不少該函數參數須要的參數,並將該函數參數做用到這些參數上。這裏有個例子:

> (funcall #'(lambda (x) (^ x 2)) 2)
4
> (funcall #'+ 1 2)
3
使用funcall函數,咱們能夠這樣定義integral函數:
> (defun integral (f a b h)
    (do ((integral 0 (+ integral (* h (funcall f x))))
         (x (+ a (/ h 2)) (+ x h)))
        ((> x b) integral)))
INTEGRAL
該定義將按咱們想要的方式運行:
> (integral #'(lambda (x) (^ x 2)) 0 1 0.01)
0.3333250000000004
    當你提早知道函數參數須要帶幾個參數的時候,函數funcall是頗有用的。若是你不知道,或者若是函數帶可變數量的參數的時候,那麼你可使用函數apply。這個函數帶一個函數參數和這個函數參數須要的參數列表,使用這個函數,返回結果。這裏有幾個例子:
> (apply #'+ (list 1 2))
3
> (apply #'+ (list 1 2 3))
6
    在函數參數和列表之間插入必定數量的額外測參數是可能的。這些參數將按給定的按順序傳遞給函數,要比列表裏的參數提早。例如:
> (apply #'+ 1 2 (list 3 4 5))
15
在下一章裏,給技術將被證實是有用的。

    使用函數funcall和apply有一些限制。它們僅能經過傳遞函數參數來使用,而不能經過特殊形式和宏。此外,大多數Lisp系統設置了能夠傳遞給函數的參數的數量上限。儘管在一些系統裏這個限值可能很大,Common Lisp規範要求這個限值至少是50。這意味着這不是一個好主意,好比說,定義一個計算列表裏元素的和的函數:

> (defun my-sum (x) (apply #'+ x))
這個定義對小列表起做用,但對大列表沒做用。apply和funcall函數也能接收符號做爲他們的參數。這些符號的函數定義將在全局環境裏肯定。

練習 3.6

略。

3.6.3 做爲結果的返回函數

如今咱們已經見過如何使用函數參數,咱們返回到函數閉包的使用上。在3.5節咱們定義了一個函數project來計算y在x上的投影。思考數學投影問題的另外一個方法是以投影操做符的形式思考,該操做符接受任意矢量y並將其放到關於x的投影上去。咱們將用Px表示這個操做符,或者簡寫爲P,並將y在x上的投影表示爲Py。該操做符被視爲一個單參數函數。被投影到的那個空間,被x跨越的空間,以某種方式「內建」到P裏。

    在Lisp裏咱們可使用函數閉包構建這個投影操做符的模型。首先,我麼構建一個函數make-projection,它帶一個表示數學矢量的列表x,並返回一個函數閉包,該閉包計算參數在x上的投影:

>  (defun make-projection (x)
     (flet ((ip (x y) (sum (* x y))))
       #'(lambda (y) (* x (/ (ip x y) (ip x x))))))
MAKE-PROJECTION
lambda表達式用來構建一個包含自由變量x的結果。lambda表達式所在的環境轉換成一個閉包。x引用了 在調用make-projection函數的時候那個參數,那麼make-projection函數返回的函數閉包「記住」了要投影的那個矢量。舉個例子,有一個4維的能夠被投影的常矢量,而後定義一個投影操做符投影其上:
> (def p (make-projection '(1 1 1 1)))
P
該投影操做符是符號p的值,因此咱們不得不使用該操做符做爲apply和funcall函數的參數:
> (funcall p '(1 2 3 4))
(2.5 2.5 2.5 2.5)
> (funcall p '(-1 1 1 -1))
(0 0 0 0)
    代替使用一個lambda表達式來構建咱們的結果,咱們也可使用一個局部函數。爲了使這個局部函數容許使用內積函數ip,咱們不得不也使用一個帶小括號的flet表達式或者一個labels表達式,就像這樣:
> (defun make-projection (x)
    (labels ((ip (x y) (sum (* x y)))
             (proj (y) (* x (/ (ip x y) (ip x x)))))
      #'proj))
MAKE-PROJECTION
練習 3.7

略。

練習 3.8

略。

3.6.4 做爲數據的表達式

代替使用函數參數,有時使用表達式更加方便。一個做何的Lisp表達式只不過是一個Lisp列表。它的元素可使用select函數或者函數first、second, ..., tenth來提取:

> (def expr '(+ 2 3))
EXPR
> (first expr)
+
> (second expr)
2
另外一個檢測表達式的有用的函數是rest,這個函數帶一個列表做爲參數,而且返回一個除了第一個元素的列表:
> (rest expr)
(2 3)
函數eval能夠對一個表達式求值:
> (eval expr)
5
函數eval在全局環境裏進行求值,那麼若是你的表達式包含任何變量,這些變量的全局值將被使用:
> (def x 3)
X
> (let ((x 5)) (eval 'x))
3
若是你想在一個表達式求值以前,用一個特定值替換掉表達式裏的變量,你可使用函數subst:
> (def expr2 '(+ x 3))
EXPR2
> (eval (subst 2 'x expr2))
5
事實上,subst函數將使用第一個參數代替第二個參數的全部資源指引,不顧及語法問題。這可能會致使一些無心義的表達式:
> (subst 2 'x '(let ((x 3)) x))
(LET ((2 3)) 2)
一個替表明達式就是構建一個包圍你的表達式的let表達式,而後將它傳遞給eval:
> (list 'let '((x 2)) expr2)
(LET ((X 2)) (+ X 3))
> (eval (list 'let '((x 2)) expr2))
5
建立一個列表模板,該模板裏只有不多表達式被引用,咱們來向模板裏填入內容,這一過程是至關廣泛的。Lisp又一次提供了一個簡寫,一個置於列表以前的反引號將致使列表裏的全部元素被引用,除了那些前面加了逗號的元素:
> `(let ((x 2)) ,expr2)
(LET ((X 2)) (+ X 3))
> (eval `(let ((x 2)) ,expr2))
5
都好不能出如今加反引號的那個列表的頂層,可是能夠包含在其子列表裏:
> `(let ((x ,(- 3 1))) ,expr2)
(LET ((X 2)) (+ X 3))
    舉個例子,咱們能夠構建一個簡單的函數,用來繪製一個矢量表達式相對於一個變量值的圖形。若是咱們調用函數plot-expr,而後(plot-expr '(+ (* 2 x) (^ x 2)) 'x -2 3)表達式應該產生一個2x+x²在區間[-2, 3]上的圖形。使用反引號機制,咱們能夠這樣定義這個函數:
> (defun plot-expr (expr var low high)
    (flet ((f (x) (eval `(let ((,var ,x)) ,expr))))
      (plot-function #'f low high)))
PLOT-EXPR
使用表達式參數而不是一個函數的優點是plot-expr函數已經得到了表達式和變量的名稱,它們能夠被用來爲圖形構建有意義的座標標記。

3.7 映射

可以一個函數對一個列表按元素進行操做常常是很是有用的。這個處理過程叫作映射,mapcar函數能夠帶一個函數和一個參數列表,而且返回結果列表,該結果是函數做用到每一個元素時產生的:

> (def x (mapcar #'normal-rand '(2 3 2)))
X
> x
((0.374662664815698 2.2129702160457247) (-0.6790406077067712 -1.5090307911933598 -0.7422588556111767) (0.2212920384039958 0.5462770527223718))
> (mapcar #'mean x)
(1.2938164404307113 -0.9767767515037692 0.3837845455631838)
mapcar參數能夠帶一些類表做爲參數,被映射的函數必須帶相同數量的參數。第一個元素將被傳遞以調用函數,而後是第二個參數,等等:
> (mapcar #'+ '(1 2 3) '(4 5 6))
(5 7 9)
若是列表參數之間長度不等,那麼求值過程將在最短的那個列表用盡時中止。

    另外一個在Lisp-Stat裏可用的映射函數是map-elements。這個函數將使用組合數據和簡單數據的Lisp-Stat區別。組合數據是列表、矢量、數組和組合數據對象(這個一下子介紹)。不是組合數據的數據項將被看作是簡單數據。預測函數compound-data-p能夠測試一個數據是不是組合數據。

    函數map-elements容許你在參數上映射一個函數,該參數多是簡單數據和組合數據的組合。若是任何一個參數是組合數據,那麼任何簡單數據都將被做爲合適大小的常量看待。例如:

> (map-elements #'+ 1 '(1 2 3) '(4 5 6))
(6 8 10)
    Lisp-Stat裏的全部矢量化算術函數都隱含使用一個隊map-elements函數的遞歸調用。+函數的定義與下式較類似:
> (defun vec+ (x y)
    (if (or (compound-data-p x) (compound-data-p y))
        (map-elements #'vec+ x y)
        (+ x y)))
VEC+
一些例子以下:
> (vec+ 1 2)
3
> (vec+ 1 '(2 3))
(3 4)
> (vec+ '(1 2) '(3 4))
(4 6)
> (vec+ '(1 2) '(3 (4 5)))
(4 (6 7))
    與mapcar函數不一樣,函數map-elements但願它的全部組合參數有相同數量的元素數目:
> (vec+ '(1 2) '(3 4 5))
Error: arguments not all the same length
Happened in: #<Subr-MAP-ELEMENTS: #13ef154>
事實上,組合參數應該是相同形狀的,那麼數組應該有相同的維度。

    你不須要常用map-elements函數來定義你本身的矢量化函數,除非你須要條件計算。例如,假設你想要定義一個這樣的矢量化版本:

Lisp-Stat提供了一個if-else函數,這樣調用它:(if-else <x> <y> <z>),若是<x>, <y>和<z>是相同長度的列表,if-else函數返回一個同長度的列表。結果的第i個元素,或者是<y>的第i個元素,或者是<z>的第i個元素,這取決於<x>的第i個原始是non-nil仍是nil。編寫咱們函數的簡單的方法就是使用if-else:

> (defun f (x) (if-else (> x 0) (log x) 0))
F
這個不起做用,由於if-else是一個函數,它全部的參數都在函數調用以前被調用了。所以在if-else改變去測試比較的的結果以前,試圖用0去調用log函數將會產生一個錯誤。
> (defun f ()
    (if (compound-data-p x)
        (map-elements #'f x)
        (if (> x 0) (log x)0)))
F
練習 3.9

略。

3.8 賦值和破壞性修改

    在像FORTRAN和C這類編程語言裏,賦值語句是編程的最基本元素。例如,在下邊的factorial函數的C定義裏,賦值語句用來更新局部變量prod,它是累計結果的關鍵一步:

int factorial(n)
{
    int count;
    int prod = 1;

    for (count=0; count<n; count=count +1)
        prod=prod * (coung + 1);
    return prod;
}

這裏咱們不能對這段程序翻譯成Lisp。咱們能夠設置局部變量,像prod,來給它賦一個新值,可是咱們沒有辦法改變它們的值。也許更重要的是,咱們沒有賦值機制的須要。

    目前爲止,本章中我所完成的每一件事都是用函數式的或者可用的編程風格來完成的。更復雜的函數已經

以簡單函數組合的形式構建出來了的。局部變量僅僅被定義用來簡化表達式。當咱們替換了在let表達式裏建立的局部變量的時候,咱們的函數的意思不該該改變,例如,即便函數定義所以變模糊了,其意義也不該改變。相對地,針對factorial的C程序使用了局部變量prod做爲存儲位置。使用它的初始值1代替每個位置的prod變量都會產生廢話。

    使用局部變量來定義,而不經過賦值的方式改變它的值,這種編程風格叫作引用透明。事實上,不管何時可能,使用這種風格都有很好的理由。簡而言之,一個程序使用賦值符是正確的,不使用就是不正確的,這句話很難經過構建一個數學證實來驗證。Abelson和Sussman詳盡地討論了這個問題。

    然而,賦值操做確實有一些重要的應用。例如,咱們可能想在計算機屏幕上構建一個窗體的軟件表示。窗體可能又各類各樣的屬性,像它的尺寸和位置,這些能夠記錄成本地狀態變量。做爲一個帶窗體的用戶接口,它能夠移動和調整尺寸,爲了保持一直是最新的表示法,咱們須要可以改變這些狀態變量,給他們賦新值。

    在Lisp裏,基本的賦值工具是特殊形式setf。setf能夠改變全局變量和局部變量的值。例如,

> (setf x 3)
3
> x
3
> (let ((x 1))
    (setf x 2)
    x)
2
傳給setf的第一個參數不會被求值,因此符號x不須要被引用。不像setf,def隻影響全局綁定。

    一個問題的例子是隨機數字生成器,在這個問題裏咱們須要可以修改一個狀態變量。一個線性同餘生成器須要由一個種子X0,一個乘法器A和一個係數M來制定,這些都是正整數。它根據規則:來計算僞隨機整數序列X1, X2,...。一個近似正態的僞隨機序列U1, U2, ...,這樣產生:Ui=Xi/M。

    經過使用包含乘法器、係數和當前X值的函數閉包,咱們可以實現這樣一個生成器。X的當前值是狀態變量,每次它都會更新成得到的新的數字。函數make-generator構建了這樣一個閉包:

> (defun make-generator (a m x0)
    (let ((x x0))
      #'(lambda ()
          (setf x (rem (* a x) m))
          (/ x m))))
MAKE-GENERATOR

函數rem計算第一個參數被第二個參數除後的餘數。該定義中lambda表達式體包含兩個表達式,第一個是個賦值表達式,用來產生局部變量x的變化的值得反作用(注:這裏的反作用是針對函數式風格說的!!!)。第二個表達式在生成器被使用時返回結果。這個lambda表達式沒有帶參數。

    經過使用A=7的五次方,係數M=2的31次方-1,種子爲12345,咱們可以構造和嘗試一個特定的生成器:

(def g (make-generator (^ 7.0 5) (- (^ 2.0 31) 1) 12345))
> (funcall g)
0.09661652850760917
> (funcall g)
0.8339946273872604
> (funcall g)
0.9477024976851895

由於由make-generator返回的函數不須要任何參數,funcall函數除了生成器g以外不須要其它參數。環境裏的局部狀態變量x,閉包g就是在那個環境裏建立的,每當g被調用的時候x更新。結果,每次對g的調用都會返回不一樣的值。賦值操做能夠用來以程序的風格編寫程序。例如,如今咱們能夠將本節開頭的C版本的factorial函數,翻譯成Lisp版本:

> (defun factorial (n)
    (let ((prod 1))
      (dotimes (count n)
               (setf prod (* prod (+ count 1))))
      prod))
FACTORIAL
dotimes結構在第2.5.6節就有簡單介紹了,隨着count值從0增加到n-1,dotimes的結構體重複執行n次。

    像咱們在2.4.5節看到的,setf能夠經過改變列表裏元素的值來破壞性地修改一個列表。例如,若是x被構建成這樣:

> (setf x (list 1 2 3))
咱們可使用下邊的表達式來改變其第二個元素,該元素的索引(即下標)爲1:
(setf (select x 1) 'a)
setf表達式裏的(select x 1)叫作位置形式(注:此處翻譯可能不許確,原文是a place form,意思應該是選擇列表的某一位置的元素的操做),或者叫廣義變量。可用的位置形式還有不少,而且定義新的位置形式或者setf方法也是可能的。這個在下一章裏將進一步討論。

    值得咱們再次關注的是,Lisp變量僅僅爲數據項的名字,破壞性的修改可能帶來不可預期的反作用。尤爲地,若是x這樣構建:(setf x '(1 2 3)),y這樣定義:(setf y x),那麼修改x的值將修改y的值,由於這些符號只不過是它們引用的相同的Lisp數據項的兩個不一樣的名字而已。

> (setf (select x 1) 'a)
A
> x
(1 A 3)
> (setf y x)
(1 A 3)
> y
(1 A 3)
    在修改以前,你可使用copy-list函數對x作一份拷貝。

3.9 等價

當兩個Lisp數據項被視爲相同的時候,不少函數須要來肯定這個判斷。這就發生了一件微妙的事情,尤爲在介紹了賦值以後。兩個事物是否等價,依賴於使用它們完成了什麼。字符串「fred」和「Fred」可能被看作是不一樣的,由於它們包含不一樣的字符串。它們可能被視爲是相同的,由於它們都能拼出Fred這個名字。當你把一個符號的名字鍵入到Lisp解釋器的時候,字母的大小寫是被忽略的。對於這個目的,上邊的兩個字符串是相同的。若是使用這些字符的ASCII碼來計算字符串的編碼,你會對這兩個字符串計算出兩個不一樣的結果。對於這個目的,這兩個字符串是不一樣的。

    另外一個例子,假設你有兩個列表,都打印成這樣: (A B),它們是等價的嗎?若是你僅僅是想提取它們的元素,那麼這兩個字符串後返回相同的結果,所以它們可能被看作相同。換句話說,隨着setf的引入,咱們能夠物理地修改一個列表的內容。假設咱們這麼作了,那另外一個列表也會受影響嗎?這取決於它們是否處於相同的計算機內存位置。

    爲了解決這種狀況,Lisp提供了4個不一樣嚴格等級的等價謂詞。最嚴格的的是eq。當且僅當它們處於內存的相同位置的時候,它們纔是eq的。這是咱們想要的測試,用來確保在修改它們中的一個以前咱們的兩個列表是真的不一樣。

    當解釋器將一個字符串翻譯成一個符號時,能夠保證的是相同名字的兩個符號是eq的。

    一個密切相關的謂詞是eql。eq與eql之間惟一的不一樣是eql將考慮如下幾項指標,相同類型和值的數量,有相同值和大小寫狀況的字符,在相同的大小寫狀況下的有相同字符的字符串。依據Lisp實現,它們多是eq的也可能不是eq的。如此,對一些Lisp實現,(eq 1 1)可能返回t,而對另外一些則返回nil,可是(eql 1 1)始終返回t,換句話說,

> (eq (list 'a 'b) (list 'a 'b))
NIL
> (eql (list 'a 'b) (list 'a 'b))
NIL
> (eq 1 1.0)
NIL
> (eql 1 1.0)
NIL
對list的兩次調用返回了不一樣的列表,整數1和浮點數1.0是不一樣的數據類型。

    謂詞equal和equalp用來肯定兩個數據項是否看起來很類似。若是(eql x y)返回t,或者x與y的程度相等全部元素也對應相等,那麼表達式(equal x y)返回t。equalp謂詞詞性比equal略弱,若是兩個數的數值是相等的,那麼equalp就認爲他們是equalp相等的,不管類型是否相同。若是兩個字符串有相同的字符,不管大小寫是否相同,它們是equalp相等的。那麼:

> (equal (list 'a 'b) (list 'a 'b))
T
> (equalp (list 'a 'b) (list 'a 'b))
T
> (equalp 1 1.0)
T
> (equal 1 1.0)
NIL
> (equal "fred" "Fred")
NIL
> (equalp "fred" "Fred")
T
    一些函數須要測試列表之間對應元素的等價性,好比3.6.4節裏介紹的subst函數。默認狀況下這些函數使用eql測試。可使用一個關鍵字參數來覆蓋這個默認值(見4.4節)。

3.10 一些例子

本章介紹了大量的新想法和技術。在繼續深刻以前,看一些更普遍的例子是頗有幫助的,這些例子動用了這裏表達的一些技術。第一個例子使用牛頓法求解一個函數的變量的平方根。第二個例子展現了一個構建表徵區別的方法。

3.10.1 牛頓法求根

牛頓法求解可微函數f的根,該法帶一個猜想值y,並這樣計算一個改進的猜想值:y-f(y)/Df(y),這裏的Df表示f的導數。經過對以下形式的遞歸定義,基本的迭代能夠沿着3.4節裏的平方根的方法開發:

> (defun newton-search (f df guess)
    (if (good-enough-p guess f df)
        guess
        (newton-search f df (improve guess f df))))
NEWTON-SEARCH
或者經過使用do結構:
> (defun newton-search (f df guess)
    (do ((guess-guess (improve guess f df)))
        ((good-enough-p guess f df) guess)))
NEWTON-SEARCH
    函數improve和good-enough-p可定義成這樣:
> (defun improve (guess f df)
    (- guess (/ (funcall f guess) (funcall df guess))))
IMPROVE
> (defun good-enoungh-p (guess f df)
    (< (abc (funcall f guess)) 0.001))
GOOD-ENOUNGH-P
做爲檢核,咱們可使用newton-search,來求sin(x)在3附近的值時π的值:
> (newton-search #'sin #'cos 3)
3.14255
newton-search函數的定義有一些缺陷,尤爲地,improve和good-enough-p的定義可能會干擾到其它定義,就像爲了求解平方根問題咱們所作的設置那樣。爲了不這些困難,咱們可使用flet創建一個塊結構:
> (defun newton-search (f df guess)
    (flet ((improve (guess f df)
                    (- guess
                       (/ (funcall f guess)
                          (funcall df guess))))
           (good-enough-p (guess f df)
                          (< (abs (funcall f guess)) .0.001)))
(do ((guess guess (improve guess f df)))
    ((good-enough-p guess f df) guess))))
NEWTON-SEARCH
函數improve和good-enough-p僅在newton-search體裏是可見的。

    經過將improve和good-enough-p函數移動到newton-search函數裏,咱們能夠進行一點簡化。由於f和df在improve哈good-enough-p函數定義的那個環境裏是可用的,因此咱們不須要將它們做爲參數傳遞:

> (defun newton-search (f df guess)
    (flet ((improve (guess)
                    (- guess
                       (/ (funcall f guess)
                          (funcall df guess))))
           (good-enough-p (guess)
                          (< (abs (funcall f guess)) .001)))
      (do ((guess guess (improve guess)))
          ((good-enough-p guess) guess))))
NEWTON-SEARCH
    正確地找出導數一般是個問題。由於對於牛頓法來講,數值的導數一般足夠精確了。咱們能夠重寫newton-search函數來使用數值導數進行計算。可是這也意味着當咱們確實有可用的導數的時候,咱們就不能利用這個精確的導數了。一個替代物就是構建一個能夠產生數值導數的函數:
> (defun make-derivative (f h)
    #'(lambda (x)
        (let ((fx+ (funcall f (+ x h)))
              (fx- (funcall f (- x h)))
              (2h (* 2 h)))
          (/ (- fx+ fx-) 2h))))
MAKE-DERIVATION
make-derivation函數返回的結果是一個函數閉包,該閉包用來記憶在計算數值導數中用到的函數f和步長h。當這個函數使用一個參數x定義的時候,他將使用函數f在x的對稱差商來逼近其導數。如今咱們可使用make-derivative函數來爲函數newton-search提供導數這個參數:
> (newton-search #'sin (make-derivative #'sin .001) 3)
3.1425465668320545
    咱們也可使用newton-search和make-derivate函數,經過查找其一階導數的根的方法,來定位函數的最大值。舉個例子,假設咱們想要找到γ-分佈裏的指數的極大似然估計,該分佈的尺度函數參數是1。咱們能夠從指數α=4.5的γ-分佈裏生成一組數據樣本,而後把它賦給變量x:
> (def x (gamma-rand 30 4.5))
X
其log似然估計能夠寫成這樣:(α-1)s - n log Γ(α),其中s=ΣlogXi,是充分的統計量,n是樣本大小。

    估計這個log似然度的函數能夠這樣構建:

> (def f (let ((s (sum (log x)))
               (n (length x)))
           #'(lambda (a)
               (- (* (- a 1) s) (* n (log-gamma a))))))
F
這個函數是變量f的值,包圍lambda表達式的let語句用來建立一個環境,在這個環境裏變量s和n表示足夠的統計樣本和樣本大小,對咱們的樣原本說他們綁定到合適的值上。生成這個函數閉包的過程這個數學處理過程是相似的,該數學處理過程發生在一旦獲取樣本數據時,抑制對數據的對數似然的依賴性。

    爲了使用newton-search函數找到α的最大似然估計,咱們須要對數似然函數的一階導數和二階導數。這些能夠這樣獲取:

> (def df (make-derivative f .001))
DF
> (def ddf (make-derivative df .001))
DDF
對極大似然估計量咱們還須要一個初始猜想,由於γ-分佈的尺度參數是1,α的矩估計方法就是樣本均值:
> (mean x)
4.683152258151905
如今咱們能夠找到最大似然估計量了:
> (newton-search df ddf (mean x))
4.747512880561235
練習 3.10

略。

3.10.2 符號微分

符號微分化使用微分計算的規則來表達和產生表示導數的表達式。起初它看起來多是使人吃驚的,但事實上自動地使用這些規則來開發程序是至關簡單的。最難的那部分就是肯定如何經過這樣一個程序來表達使用的數據:被微分的表達式和表示導數的表達式。

    爲了保持簡潔,讓咱們以微積分類可能的開始方式,經過考慮僅針對微分常量、變量、和還有乘積的規則,開始咱們的討論。

抽象表達式

微分函數使用的數據就是表達式。有不少不一樣的方式,這些方式不能用計算機來表達,可是表達式的細節與微分式要表現的基本任務幾乎沒有關係。爲了反映這個事實,簡要地想想用來開發微分函數所須要的表達式的精確特徵,還有用來捕獲這些特徵的函數集合,想清楚以上兩點是很是重要的。這個函數集合叫作抽象數據表示法,它能夠用來編寫咱們的微分函數。過會兒,咱們可以用開發這個數據表示的不一樣的方式來進行試驗。可是,經過將表達式的使用從他們的表示的內部細節中分離出來,咱們將得到一個系統,該系統比數據表示與數據使用交織在一塊兒的方法更容易理解和修改。這個編程策略叫作數據抽象。

    在咱們的函數式表達方法中,咱們須要表示的表達式的基本特徵是什麼呢?首先,有四種數據類型:常量、變量、加法和乘積。咱們須要可以識別一個特定的數據項是否是這四種數據類型中的一個。讓咱們作個假設:爲這個目的咱們能夠定義四個謂詞:

(constantp <e>)
(varialbep <e>)
(sump <e>)
(productp <e>)
咱們還須要另外一個謂詞,用來識別兩個變量是否相同,
(same-variable-p <v1> <v2>)

    常量和變量時原子表達式,它們不能被分解或者由其它表達式構建。相反,加法和乘積是組合表達啥。所以咱們須要「訪問函數」來提取它們的各部分構件,須要構造函數來構建新的加法和乘積。加法的各部分構件是加數和被加數。假設咱們能夠用如下表達式獲取:

(addend <e>)
(augend <e>)
而後咱們能夠構建一個新的加法:
(make-sum <a1> <a2>)
乘積的各部分構件是被乘數和乘數,它們能夠這樣得到:
(multiplicand <e>)
(multiplier <e>)
一個新的乘積能夠這樣構建:
(make-product <m1> <m2>)
    這些是全部的咱們用來描述微分處理的函數:
> (defun deriv (exp var)
    (cond
      ((constantp exp) 0)
      ((variablep exp) (if (same-variable-p exp var) 1 0))
      ((sump exp)
       (make-sum (deriv (addend exp) var)
                 (deriv (augend exp) var)))
      ((productp exp)
       (make-sum (make-product (multiplier exp)
                               (deriv (multiplicand exp) var))
                 (make-product (deriv (multiplier exp) var)
                               (multiplicand exp))))
      (t (error "Can't differentiate this expression"))))
error函數用來若是沒有可用的微分規則時發出一個錯誤信號,那個字符串參數就是錯誤信息。

    函數deriv表示微積分課程裏涵蓋的前幾個微分法則的簡單的Lisp編碼。例如,加法法則的Lisp編碼爲:

表達式如何實現的細節是不重要的,只要他們符合這裏使用的函數式抽象的接口就能夠了。在咱們能使用這個函數以前,咱們確實須要開發至少一個表示法用來表達。

一個用來表達的表示法

表示表達式有不少方法,可是最容易的方法就是使用標準Lisp語法,常量由數字表示,變量由符號表示。

    讓咱們從定義加法規則和乘積規則的「讀取函數」和「構造函數」。對於加法規則咱們定義:

> (defun addend (e) (second e))
ADDEND
> (defun augend (e) (third e))
AUGEND
> (defun make-sum (a1 a2) (list '+ a1 a2))
MAKE-SUM

對於乘積法則:

> (defun multiplier (e) (second e))
MULTIPLIER
> (defun multiplicand (e) (third e))
MULTIPLICAND
> (defun make-product (m1 m2) (list '* m1 m2))
MAKE-PRODUCT
    測試一個數據項是常量仍是變量的謂詞至關簡單:
> (defun constantp (e) (numberp e))
CONSTANTP
> (defun variablep (e) (symbolp e))
VARIABLEP
測試兩個變量是否相同的謂詞也很簡單:
> (defun same-variable-p (v1 v2)
    (and (variablep v1) (variablep v2) (eq v1 v2)))
SAME-VARIABLE-P
咱們使用eq謂詞是由於當且僅當兩個符號eq等價時,它們纔是相同的。

    對加法規則和乘積規則進行測試的謂詞就稍微複雜一些了。咱們須要檢查一個數據項是不是三元素的列表,它們的開始元素是+或者*。那麼對於加法規則有:

> (defun sump (e)
    (and (listp e) (= (length e) 3) (eq (first e) '+)))
SUMP

對於乘積規則有:

> (defun productp (e)
    (and (listp e) (= (length e) 3) (eq (first e) '*)))
PRODUCTP

如今咱們能夠用一些例子測試一下咱們的導數函數:

> (deriv '(+ x 3) 'x)
(+ 1 0)
> (deriv '(* x y) 'x)
(+ (* X 0) (* 1 Y))
> (deriv '(* (* x y) (+ x 3)) 'x)
(+ (* (* X Y) (+ 1 0)) (* (+ (* X 0) (* 1 Y)) (+ X 3)))
    結果是正確的,可是與須要相比它們仍是太複雜了。不幸的是,簡化一個表達式是一項比微分計算還要困難的任務,首要的是由於不太容易陳述一個表達式比另外一個簡單了究竟意味着什麼。然而,經過編寫函數將加法規則和乘積規則更加智能一點,咱們能略微改善咱們的微分算子。在make-sum函數的定義裏,咱們將檢查若是全部的參數都是數字,那麼返回它們的和。若是其中一個參數是0,咱們能夠只返回其它參數:
> (defun make-sum (a1 a2)
    (cond
      ((and (numberp a1) (numberp a2)) (+ a1 a2))
      ((numberp a1) (if (= a1 0) a2 (list '+ a1 a2)))
      ((numberp a2) (if (= a2 0) a1 (list '+ a1 a2)))
      (t (list '+ a1 a2))))
MAKE-SUM

類似地,對於make-product函數,若是兩個參數都是數字就返回他們的乘積,若是其中一個爲零就返回零,若是其中一個爲1,則返回另外一個參數的數值:

> (defun make-product (m1 m2)
    (cond
      ((and (numberp m1) (numberp m2)) (* m1 m2))
      ((numberp m1)
       (cond ((= m1 0) 0)
         ((= m1 1) m2)
         (t (list '* m1 m2))))
      ((numberp m2)
       (cond ((= m2 0) 0)
         ((= m2 1) m1)
         (t (list '* m1 m2))))
      (t (list '* m1 m2))))
MAKE-PRODUCT

如今結果更合理一些,儘管還不完美:

> (deriv '(+ x 3) 'x)
1
> (deriv '(* x y) 'x)
Y
> (deriv '(* (* x y) (+ x 3)) 'x)
(+ (* X Y) (* Y (+ X 3)))
練習 3.11 3.12 3.13

略。

加入一元函數

假設咱們想要加入exp函數,不是爲這個函數增長一個強制的規則來求得導數,而是最好增長一個表示鏈式規則的規則,而後在數據表示裏,將這個特定的函數處理成微分形式。爲了加入鏈式規則,咱們須要一個謂詞用以識別一個一元函數調用:

(unary-p <e>)

咱們也須要「讀取函數」來肯定函數名和函數的參數:

(unary-function <e>)
(unary-argument <e>)
咱們須要可以構建一個表達式在函數的參數上計算導數:
(make-unary-deriv <f> <x>)
而後咱們能夠將deriv函數修改爲這樣:
> (defun deriv (exp var)
    (cond
      ((constantp exp) 0)
      ((variablep exp) (if (same-variable-p exp var) 1 0))
      ((sump exp)
       (make-sum (deriv (addend exp) var)
                 (deriv (augend exp) var)))
      ((productp exp)
       (make-sum (make-product (multiplier exp)
                               (deriv (multiplicand exp) var))
                 (make-product (deriv (multiplier exp) var)
                               (multiplicand exp))))
      ((unary-p exp)
       (make-product (make-unary-deriv (unary-function exp)
                                       (unary-argument exp))
                     (deriv (unary-argument exp) var)))
      (t (error "Can't differentiate this expression"))))
DERIV
    函數make-unary-deriv也能夠咱們的表達式的抽象表示法的方式定義。一個辦法就是使用case結構:
> (defun make-unary-deriv (fcn ar)
    (case fcn
      (exp (make-unary 'exp arg))
      (sin (make-unary 'cos arg))
      (cos (make-product -1 (make-unary 'sin arg)))
      (t (error "Can't differentiate this expression"))))
MAKE-UNARY-DERIV

這裏的case後帶一個表達式,該表達是將計算出一個符號,叫作「case選擇器」,它後邊緊跟真一系列的case語句。每一個case語句都以一個符號或符號列表開始,而後case將按流程處理每條語句直到選擇器與其中一條語句裏的符號匹配爲止。當發現一個匹配以後,被匹配的語句的剩餘的表達式將被求值,而後其最後一個表達式的結果將被返回。若是沒有發現可匹配的符號,將返回nil。符號t比較特殊:它能夠匹配任何選擇器。

    make-unary-deriv函數須要一個額外的構造器(make-unary <f> <x>)來構造一個一元函數構造表達式。

    對於咱們的表達式來講,謂詞和讀取函數都是比較容易定義的:

> (defun unary-p (e)
    (and (listp e) (= (length e) 2)))
UNARY-P
> (defun unary-function (e) (first e))
UNARY-FUNCTION
> (defun unary-argument (e) (second e))
UNARY-ARGUMENT
謂詞unary-p的定義不是很完美,可是對目前的應用來講已經足夠了。構造函數make-unary也很簡單:
> (defun make-unary (fcn arg) (list fcn arg))
MAKE-UNARY
    爲了保證這個新的規則能夠正常工做,讓咱們測試一些例子:
> (deriv '(exp (* 3 x)) 'x)
(* (EXP (* 3 X)) 3)
> (deriv '(sin (* 3 x)) 'x)
(* (COS (* 3 X)) 3)
使用規則庫

這個方法的一個方面是不太使人滿意的。就像你在用deriv函數同樣,增長一些函數多是你想要作的事情。如今,須要編輯make-unary-deriv函數。一個能夠替代的辦法就是設置一個導數數據庫。讓咱們作一個假設,咱們有一個包含處理一元函數的規則的數據庫,這些規則可使用函數get-unary-rule來查詢獲得。函數apply-unary-rule用來將一個規則做爲參數,去產生一個經過對參數求值獲得的導數表達式。咱們能夠這樣編寫make-unary-deriv函數:

> (defun make-unary-deriv (fcn arg)
    (apply-unary-rule (get-unary-rule fcn) arg))
MAKE-UNARY-DERIV
    爲了實現咱們的數據庫,咱們可使用一個「關聯列表」。關聯列表就是列表的列表。每個子列表以一個符號開始,做用是左右一個鍵。函數assoc帶一個鍵和一個關聯列表,而後返回第一個匹配鍵的子列表,當無匹配鍵時返回nil。一個簡單的例子以下:
> (def *mylist* '((x 1) (y "hello") (abc a w (1 2 3))))
*MYLIST*
> (assoc 'x *mylist*)
(X 1)
> (assoc 'y *mylist*)
(Y "hello")
> (assoc 'abc *mylist*)
(ABC A W (1 2 3))
> (assoc 'z *mylist*)
NIL
    讓咱們使用一個全局變量*derivatives*來處理咱們的導數數據庫。初始狀況下咱們的數據庫是空的,因此咱們將它設置爲nil:
> (def *derivatives* nil)
*DERIVATIVES*
爲了能向數據庫加入數據,咱們可使用函數cons將一個元素加入到一個列表的前端,例如:
> (cons 'a '(b c))
(A B C)
使用cons函數,咱們可以定義函數add-unary-rule來向數據庫加入規則:
> (defun add-unary-rule (f rule)
    (setf *derivatives* (cons (list f rule) *derivatives*)))
ADD-UNARY-RULE
取回函數能夠這樣編寫:
> (defun get-unary-rule (f)
    (let ((rule (assoc f *derivatives*)))
      (if rule
          rule
          (error "Can't differentiate this expression"))))
GET-UNARY-RULE
    如今咱們能夠肯定對於規則什麼是可使用的。一個簡單的選擇就是一個帶單參數的函數,導數參數,而後返回導數表達式。那麼apply-unary-rule函數就很簡單了:
> (defun apply-unary-rule (entry arg)
    (funcall (second entry) arg))
咱們可使用如下方式向咱們的數據庫增長一些規則:
> (add-unary-rule 'exp #'(lambda (x) (make-unary 'exp x)))
((EXP #<Closure: #13b0844>))
> (add-unary-rule 'sin #'(lambda (x) (make-unary 'cos x)))
((SIN #<Closure: #13b02a4>) (EXP #<Closure: #13b0844>))
如今咱們能夠看一些例子:
> (deriv '(exp x) 'x)
(EXP X)
> (deriv '(exp (* -1 (* x x))) 'x)
(* (EXP (* -1 (* X X))) (* -1 (+ X X)))
> (deriv '(sin (* 3 x)) 'x)
(* (COS (* 3 X)) 3)
> (deriv '(* (cos (* 3 x))) 'x)
Error: Can't differentiate this expression
Happened in: #<FSubr-IF: #1353650>
咱們的系統不能處理最後一個表達式,由於它不知道如何對餘弦進行微分,不是一旦咱們添加對餘弦函數的求導規則,它就能對餘弦進行微分了:
> (add-unary-rule 'cos
                  #'(lambda (x)
                      (make-product -1 (make-unary 'sin x))))
((COS #<Closure: #13ba8b0>) (SIN #<Closure: #13aff74>) (SIN #<Closure: #13b02a4>) (EXP #<Closure: #13b0844>))
> (deriv (deriv '(sin (* 3 x)) 'x) 'x)
(* (* (* -1 (SIN (* 3 X))) 3) 3)
    一個行爲由數據來決定,而後使用帶有合適行爲的行爲數據庫叫作數據導向編程。對一小段數據的行爲進行選擇的過程叫作調度(dispatching)。

練習 3.14

略。

相關文章
相關標籤/搜索