《計算機程序的構造和解釋》學習筆記

學習計劃:總22節,每節1天,預計22天學習時間+4天練習時間,2013年4月6日~2013年5月2日

20130406==============================

Emacs中使用mit-scheme

安裝好mit-scheme之後,雖然自帶的edwin(類emacs編輯器)也不錯了,但是缺少了語法高亮多少還是有點不方便。這裏主要是講如何在emacs裏面使用mit-scheme的方法。

首先在~/.emacs裏面加入如下的語句:

;;; Always do syntax highlighting  
(global-font-lock-mode 1)  
;;; Also highlight parens  
(setq show-paren-delay 0  
      show-paren-style 'parenthesis)  
(show-paren-mode 1)  
;;; This is the binary name of my scheme implementation  
(setq scheme-program-name "scm")

注意上面的最後一行裏面的scm要修改成對應的scheme解釋器的程序名。

然後在新啓動的emacs裏面如果打開後綴爲.scm或者.ss的文件,那麼默認是當成scheme的文件,並且開啓了語法高亮。

如果需要在編輯的源代碼裏面調用scheme解釋器的話,可以按以下的步驟來進行:

  1. C-x 2   ;;這個是用來新打開一個水平分割的窗口。
  2. C-x o   ;;跳轉到這個新打開的窗口。
  3. M-x run-scheme  ;;在新打開的窗口裏面運行scheme解釋器。

現在你就可以像用edwin一樣來使用嵌入了scheme的emacs了。下面兩個key可以用來馬上執行文件的語句:

  1. C-x C-e   ;;將光標之前的最後一個語句交給scheme解釋並執行。
  2. C-x h C-c C-r  ;;將整個buffer的內容都交給scheme解釋執行。


一、構造過程抽象

心智的活動,除了盡力產生各種簡單的認識之外,主要表現在如下三個方面:

1)將若干簡單認識組合爲一個複合認識,由此產生出各種複雜的認識。

2)將兩個認識放在一起對照,不管它們如何簡單或複雜。在這樣做時並不將它們合而爲一,由此得到有關它們的相互關係的認識。

3)將有關認識與那些在實際中和它們同在的所有其他認識隔離開,這就是抽象,所有具有普遍性的認識都是這樣得到的。

我們準備學習的是有關計算過程的知識


Lisp並不是一種主流語言,爲什麼要用它作爲程序設計的基礎呢?

因爲它具有許多獨立特徵,這些特徵使它成爲研究重要程序的設計、構造,以及各種數據結構,並將其關聯於支持它們的語言特徵的一種極佳媒介。

比如最重要的特徵:計算過程的Lisp描述本身又可以作爲Lisp的數據來表示和操作。現存許多威力強大的程序設計技術,都依賴於填平在“被動的”數據和“主動的”過程之間的傳統劃分。然而Lisp具有可以將過程作爲數據進行處理的靈活性,使它成爲探索這些技術的最方便的現存語言之一。

1.1程序設計的基本元素

一種強有力的程序設計語言,不僅是一種指揮計算機執行任務的方式,還應該成爲一種框架,使我們能夠在其中組織自己有關計算過程的思想。這就需要具備最少以下三種機制:

1)基本表達形式,用於表示語言所關心的最簡單的個體。

2)組合的方法,通過它們可以從較簡單的東西出發構造出複合的元素。

3)抽象的方法,通過它們可以爲複合對象命名,並將它們當作單元去操作。

1.1.1表達式

(+ 11 22)是一個表達式,其結果爲33。

注意對於多重嵌套運算,應用格式良好的形式書寫,這樣有助於閱讀。

()稱爲組合式,+是運算符,其餘項11和22是運算對象

表達式是定義一個數據操作過程的最小單位,通常由運算符+運算對象(這兩者在書中也被稱爲子表達式)組成的表達式稱爲組合式。

1.1.2命名和環境

(define size 2)定義了一個變量size=2。

(*2(+size 3))結果爲10。

定義圓周長運算組合式:

(define pi 3.14159)

(define radius 10)

(define circumference (* 2 pi radius))

一般而言,計算得到的對象完全可以具有非常複雜的結構,如果每次需要使用它們時,都必須記住並重復地寫出它們的細節,那將是極端不方便的事情。

實際上,構造一個複雜的程序,也就是爲了去一步步地創建出越來越複雜的計算性對象。解釋器使這種逐步的程序構造過程變得非常方便。

於是,一個Lisp程序通常總是由一大批相對簡單的過程組成。

命名的意義在於,使我們能通過名字去使用(一些可能過程及其複雜)計算機對象(,而不必再去在新的過程中重新構建它)。
這些名字也可以稱之爲符號,在上例中可見,將值與符號關聯,而後又提取這些值,意味着解釋器必須維護某種存儲能力,以便保持有關的名字-值對偶的軌跡,這種存儲被稱爲環境

1.1.3組合式的求值

本章目標是把與過程性思維有關的各種問題隔離出來。以下是解釋器的工作過程:

1)求值該組合式的各個子表達式。(這一步驟是遞歸方式執行的)

2)將作爲最左子表達式(運算符)的值的那個過程應用於相應的實際參數,所謂實際參數也就是其他子表達式(運算對象)的值。

例如:(* (+ 2(* 4 6))(+ 3 5 7))

我們可以採用一棵樹的形式,用圖形標示這一組合式的求值過程,其中的每個組合式用一個帶分支的結點表示,由它發出的分支對應於組合式裏的運算符和各個運算對象。(這一計算過程稱爲“樹形累計”)

組合式的求值過程可以視作“樹形積累”,每個子表達式(運算符與運算對象)都視爲一個節點。


一般而言,我們應該把遞歸看做一種處理層次性結構的(像樹這樣的對象)極強有力的技術。

處理這些基礎情況的方式如下規定:

1)數的值就是它們所表示的數值。

2)內部運算符的值就是能完成相應操作的機器指令序列。(這一規定可以看作是下一條規定的特殊情況)

3)其他名字(比如之前定義過的size、pi等)的值就是在環境中關聯於這一名字的那個對象。

一般性求值規則的例外稱爲特殊形式,define是我們接觸到第一個特殊形式,它不應用於除它以外的實際參數,而是將一個變量名關聯到一個值。

1.1.4複合過程

Lisp裏的部分元素必然會出現在任何一種強有力的程序設計語言裏,包括:

1)數和算術運算是基本的數據和過程。

2)組合式的嵌套提供了一種組織起多個操作的方法。

3)定義是一種受限的抽象手段,它爲名字關聯相應的值。

複合過程的定義:(define (<name> <formal parameters>) <body>),eg:(define (square x)(* x x))

用複合過程定義另一個複合過程eg:(define (sum-of-squares x y)(+ (square x) (square y)))

複合過程與C語言中的函數定義類似,表達式1中最左側的子表達式可以理解爲是一種自定義過程的運算符,應用於除它以外的子表達式,表達式2中則申明瞭“自定義運算符”的求值規則。

1.1.5過程應用的代換模型

解釋器的工作方式是對組合式中的各個元素(子表達式)求值,而後將得到的那個過程(組合式裏運算符的值)應用於那些實際參數(組合式裏運算對象的值)。

(define (square x)(* x x))
(define (sum-of-squares x y) (+ (square x)(square y)))
(define (f a)(sum-of-squares (+ a 1)(× a 2)))
(f 5)
將a的實際參數5代換f體的形式參數得:(sum-of-squares (+ 5 1)(× 5 2)),問題被歸約爲對另一組合式的求值。
進一步歸約:(+ (square 6)(square 10))。
使用square的定義又可以將它歸約爲:(+ (× 6 6)(× 10 10))。
通過乘法又能將它進一步歸約爲:(+ 36 100),最後得到136。

上述這種計算過程稱爲過程應用的代換模型。

代換模型分:應用序求值(先求值參數而後應用) & 正則序求值(完全展開後求值),解釋器實際使用的是應用序。

需要注意:代換的作用只是爲了幫助我們領會過程調用中的情況,而不是對解釋器實際工作方式的具體描述。

1.1.6條件表達式和謂詞

計算x的絕對值,如果x>0取x,如果x=0取0,如果x<0取-x,這種結構稱爲一個分情況分析,Lisp中使用cond(conditional)來處理這一結構。

(define (abs x)(cond ((> x 0) x)((= x 0) 0)((< x 0) (-x))))

條件表達式的一般性形式:(cond (<p1> <e1>)(<p2> <e2>)…(<pn> <en>))

在每個對偶中的第一個表達式(即<p>)是一個謂詞,它的值將被解釋爲真或假。

求值方式:若謂詞p1被解釋爲false,就去進行謂詞p2的求值,若仍爲false,就循環的求p3、p4,直到某一個謂詞得到true,就返回響應子句中的序列表達式<e>的值,視作整個條件表達式的值。若所有<p>都爲false,則cond的值被視作沒有定義。

絕對值表示法2:(define (abs x)(cond ((< x 0)(- x))(else x)))

絕對值表示法3:(define (abs x) (if (< x 0) (- x)  x)),if表達式的一般形式:(if <predicate> <consequent> <alternative>)

除了一批基本謂詞如<、=、>之外,還有一些邏輯複合運算符,利用它們可以構造出各種複合謂詞,常用的如(and <e1> ... <en>)(or<e1> ... <en>)(not <e>)

1.1.7實例:採用牛頓法求平方根

在數學的函數和計算機的過程之間有一個重要差異,那就是,這一過程必須是有效可行的。

比如求平方根可以描述爲:平方根x=那樣的y,使得y>=0而且y的平方=x

用Lisp的形式可以寫爲:(define (sqrt x) (the y (and (>= y 0) (= (square y) x))))

但這只不過是重新提出了原來的問題,函數與過程之間的矛盾,不過是在描述一件事情的特徵,與描述如何去做這件事情之間的普遍性差異的一個具體反映。

因此,計算平方根僅靠描述是不夠的,最常用的是牛頓的逐步逼近方法:如果對x的平方根的值有了一個猜測y,那麼就可以通過執行一個簡單操作去得到一個更好的猜測,只需要求出y和x/y的平均值(它更接近實際的平方根值)。

需要用到之前定義過的abs與square:
(define (square x) (* x x))
(define (abs x) (if (< x 0) (- x)  x))
(define (sqrt-iter guess x) (if (good-enough? guess x)  guess (sqrt-iter (improve guess x) x)))

改進1,求出它與被開方數除以上一個猜測的平均值:
(define (improve guess x) (average guess (/ x guess)))
其中
(define (average x y) (/ (+ x y) 2))

改進2,說明什麼是“足夠好”,這裏採用的方法並不是一個很好的檢測方法,誤差值爲0.001:
(define (good-enough? guess x) (< (abs (- (square guess) x)) 0.001))

改進3,設定一種方式來啓動整個工作,用1.0作爲對任何數的初始猜測值:
(define (sqrt x) (sqrt-iter 1.0 x))

sqrt-iter展示瞭如何不用特殊的迭代結構來實現迭代,其中只需要使用常規的過程調用能力。

函數與過程之間的矛盾是說明性的知識(描述一件事)與行動性的知識(如何完成一件事)之間的差異。

1.1.8過程作爲黑箱抽象

上一小節中的sqrt程序可以看作一族過程,它們直接反應了從原問題到子問題的分解(其中sqrt-iter的定義是遞歸的,1.2節中,將細緻討論)。

這一過程分解的意義在於,我們可以將一個大程序看作若干過程模塊的集合,每個過程完成一件可以清楚標明的工作。
例如,當我們基於square定義過程good-enough?之時,就是將square看做一個“黑箱”(即無須關注square這個過程是如何計算出它的結果的)。
因此,如果只看good-enough?過程,與其說square是一個過程,不如說它是一個過程的抽象,即過程抽象

由此可見,一個過程定義應該能隱藏起一些細節。這使過程的使用者可能不必自己去寫這些過程,而是從其他程序員那裏作爲一個黑箱而接受了它。

局部名:過程的形式參數名必須局部於有關的過程體,形式參數的名字稱爲約束變量,而非約束的稱爲自由的
一個完成的過程定義裏的某個約束變量統一換名,並不影響這個過程定義的意義。
比如good-enough?的定義中,guess和x是約束變量,而<、-、abs、square是自由的。

內部定義和塊結構:諸如上述定義(define (square……))(define (sqrt……)),sqrt使用了square等過程,square被定義爲自由的,顯然用戶無法再次定義名爲square的過程,因爲這樣會影響sqrt的執行結果,在許多程序員一起構造大系統的時候,這一問題將會變得非常嚴重。爲了解決這一問題,可以將square定義在sqrt內部:
(define (sqrt x)
  (define (good-enough? guess x) (< (abs (- (square guess) x)) 0.001))
  (define (imporve guess x) (average guess (/ x guess)))
  (define (sqrt-iter guess x) (if (good-enough? guess x)  guess (sqrt-iter (improve guess x) x)))
(sqrt-iter 1.0 x))
這種嵌套的定義(內部定義的方式)稱爲塊結構
因爲x在sqrt的定義中是受約束的,因此我們還能簡化它:
(define (sqrt x)
  (define (good-enough? guess) (< (abs (- (square guess) x)) 0.001))
  (define (improve guess) (average guess (/ x guess)))
  (define (sqrt-iter guess) (if (good-enough? guess)  guess (sqrt-iter (improve guess))))
(sqrt-iter 1.0))
這種方式稱爲詞法作用域。

1.2過程與它們所產生的計算

能夠看清所考慮的動作的後果的能力,對於成爲程序設計專家是至關重要的,就像這種能力在所有綜合性的創造性的活動中的作用一樣。

一個過程也是一種模式,它描述了一個計算過程的局部演化方式,描述了這一計算過程中的每個步驟是怎樣基於前面的步驟建立起來的。在有了一個刻畫計算過程的過程描述之後,我們當然希望能做出一些有關這一計算過程的整體或全局行爲的論斷。一般來說這是非常困難的,但我們至少還是可以試着去描述過程演化的一些典型模式。

這一節,將考察一些簡單過程所產生的計算過程的“形狀”,還將研究這些計算過程消耗各種計算資源(時間和空間)的速率。

20130407==============================

1.2.1線性的遞歸和迭代

乘階函數是大家熟悉的一種展開這一對比的很好的例子,n!=n*(n-1)*(n-2)*(n-3)...3*2*1,分別用方式1(遞歸)和方式2(迭代)來實現它。

方式1:(define (factorial n) (if (= n 1) 1 (* n (factorial (- n 1)))))


方式2:(define (factorial n ) (fact-iter 1 1 n))  (define (fact-iter product counter max-count) (if (> counter max-count) product (fact-iter (* counter product) (+ counter 1) max-count)))

通過應用替換模型(代換模型)的角度看,我們很容易發現兩者“形狀”上的區別。

方式1
形狀:計算過程構造起一個推遲進行的操作所形成的鏈條(在這裏是一個乘法的鏈條),收縮階段表現爲這些運算的實際執行。這種由一個推遲執行的運算鏈條刻畫的計算過程稱爲遞歸計算過程
特點:解釋器需要維護好那些以後將要執行的操作的軌跡。在計算階乘n!時,推遲執行的乘法鏈條的長度也就是爲保存其軌跡需要保存的信息量,這個長度隨着n值而線性增長(正比於n)。這樣的計算過程稱爲線性遞歸過程

方式2
形狀:計算過程沒有任何增長或收縮。對於任何一個n,在計算過程中的每一步,在我們需要保存的軌跡裏,所有的東西就是變量product、counter和max-count的當前值。這種過程稱爲迭代計算過程
特點:其狀態可以用固定數目的狀態變量描述;存在着一套固定的規則,描述了計算過程在從一個狀態到下一個狀態的轉換時,這些變量的更新方式;還有一個(可能有的)結束檢測。這種計算步驟隨n線性增長的過程稱爲線性迭代過程

從另一角度看,在迭代中,那幾個程序變量提供了有關計算狀態的完整描述,意味着無論在哪一步停下來,只要爲解釋器提供有關這三個變量的值就能喚醒計算。
而遞歸,還存在着另外一些“隱含”信息,它們並未保存在程序變量裏,而是由解釋器維持着,指明瞭過程在所推遲的運算形成的鏈條裏"所處何處"。鏈條越長,需要保存的信息越多。

難點:區分遞歸計算過程的概念和遞歸過程的概念P23

1.2.2樹形遞歸

另一種常見計算模式是樹形遞歸,比如Fibonacci數序列的計算,規則:

將規則翻譯爲一個計算Fibonacci數的遞歸過程:

(define (fib n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (fib (- n 1)) (fib (- n 2))))))

請注意,這裏的每層分裂爲兩個分支(除了最下面),反映出對fib過程的每個調用中兩次遞歸調用自身的事實。

上面的過程作爲典型的樹形遞歸具有教育意義,但它卻是一種很糟的計算Fibonacci數的方法。因此Fib(n)值的增長相對於n是指數的。證明見P25

再用迭代方式計算:

(define (fib n) (fib-iter 1 0 n))  (define (fib-iter a b count) (if (= count 0) b (fib-iter (+ a b) a (- count 1))))

上述兩種方式在資源消耗上差異巨大,樹形遞歸的計算步驟=fib(n+1),而迭代的計算步驟=n。
但當我們考慮的是在層次結構性的數據上操作,而非對數操作時,樹形遞歸就變成了一種自然的、強大的工具。

實例:換零錢方式的統計P26
(define (count-change amount) (cc amount 5))
(define (cc amount kinds-of-coins) (cond ((= amount 0) 1) ((or (< amount 0) (= kinds-of-coins 0)) 0) (else (+ (cc amount (- kinds-of-coins 1)) (cc (- amount (first-denomination kinds-of-coins)) kinds-of-coins)))))
(define (first-denomination kinds-of-coins) (cond((= kinds-of-coins 1) 1) ((= kinds-of-coins 2) 5) ((= kinds-of-coins 3) 10) ((= kinds-of-coins 4) 25) ((= kinds-of-coins 5) 50)))

1.2.3增長的階

描述不同的計算過程在消耗計算資源的速率的差異上可用增長的階的記法,它便於我們理解在輸入變大時,某一計算過程所需資源的粗略度量情況。

20130408==============================

1.2.4求冪

求b的n次方的遞歸定義:
b^n=b*b^(n-1)
b^0=1

直接翻譯爲過程:
(define (expt b n) (if (= n 0) 1 (* b (expt b (- n 1)))))
之前以介紹過這種線性遞歸,需要Θ(n)步和Θ(n)空間。

然後再用迭代的方式重寫:
(define (expt b n) (expt-iter b n 1))
(define (expt-iter b counter product) (if (= counter 0) product (expt-iter b (- counter 1) (* b product))))
線性迭代需要Θ(n)步和Θ(1)空間。

通過連續求平方得到更簡化的版本,比如b^8用三次乘法算出它:
b^2=b*b
b^4=b^2*b^2
b^8=b^4*b^4
總結它的規律得:
b^n=(b^(n/2))^2   若n是偶數
b^n=b*b^(n-1)   若n是奇數
定義過程:
(define (fast-expt b n) (cond ((= n 0) 1) ((even? n) (square (fast-expt b (/ n 2)))) (else (* b (fast-expt b (- n 1))))))
(define (even? n) (= (remainder n 2) 0))
其中square是求平方的過程,remainder用於檢測一個整數是否偶數。
顯然在空間和步數上相對於n都是對數的,比如計算b^2n時,之需要比計算b^n多做一次乘法,每做一次新乘法,能夠計算的指數值(大約)增大一倍。
連續求平方的線性遞歸過程需要Θ(logn)步和Θ(logn)空間。

隨着n變大,Θ(logn)和Θ(n)雙方的增長差異會變得非常明顯,當然這不是最終版,還可以用線性迭代實現連續求平方得到更優的計算過程。

1.2.5最大公約數

兩個整數a和b的最大公約數(GCD)定義爲能除盡這兩個數的那個最大的整數。當研究有理數算術的實現時,會需要GCD。

找出兩個整數的GCD的一種方式是對它們做因數分解,並找出公因子。
但存在一種更高效的算法(歐幾里德算法),基於以下觀察:
GCD(a,b)=GCD(b,r)
如果r是a除以b的餘數,那麼a和b的公約數正好也是b的r的公約數,比如:
GCD(206,40)=GCD(40,6)=GCD(6,4)=GCD(4,2)=GCD(2,0)
將GCD(206,40)歸約到GCD(2,0)最終得到2。可證,從任意兩正整數開始,反覆執行這種歸約,最後產生出一個數對,當其中第二個數是0,另一個數就是GCD。

歐幾里德算法過程化:
(define (gcd a b) (if (= b 0) a (gcd b (remainder a b))))
這將產生一個迭代計算過程,步數依所設計數的對數增長,這一事實與Fibonacci數之間有一種有趣關係:Lame定理P32

1.2.6實例:素數檢測

1.3用高階函數做抽象

通過之前的學習已經看到,在作用上,過程也是一種抽象,它們的描述了一些不依賴於特定數的複合操作。
例如,在定義(define (cube x) (* x x x))時,我們討論的不是某個特定數值的立方,而是對任意數的立方。並且可以通過名字cude調用它。

人們對功能強大的程序設計語言有一個必然要求,就是能爲公共的模式命名,建立抽象,而後直接在抽象的層次上工作。
過程提供了這種能力,這也是爲什麼除最簡單的程序語言外,其他語言都包含定義過程的機制的原因。

然而,若將過程中參數限制爲(只能爲)數,會嚴重影響建立抽象的能力。因此經常有一些同樣的設計模式能用於若干不同的過程。爲了把這種模式描述爲相應的概念,我們就需要構造出這樣的過程,讓它們以過程作爲參數,或者以過程作爲返回值。這類能操作過程的過程稱爲高階過程。

20130409==============================

1.3.1過程作爲參數

觀察下面3個過程的共通性:

計算a到b的個整數之和
(define (sum-integers a b) (if (> a b) 0 (+ a (sum-integers (+ a 1) b))))

計算a到b範圍內的整數的立方之和
(define (sum-cubes a b) (if (> a b) 0 (+ (cube a) (sum-cubes (+ a 1) b))))

計算1/(1*3)+1/(5*7)+1/(9*11)+...序列之和
(define (pi-sum a b) (if (> a b) 0 (+ (/ 1.0 (* a (+ a 2))) (pi-sum (+ a 4) b))))

可以看出這3個過程共享着一種公共的基礎模式:用於從a算出需要加的項的函數,還有用於提供下一個a值的函數。可以總結出如下模板
(define (<name> a b) (if (> a b) 0 (+ (<term> a) (<name> (<next> a) b))))
數學家很早就認識到序列求和中的抽象模式,並提出了專門的“求和記法”,例如:

與此類似,作爲程序模式,我們也希望能寫出一個過程,去表述求和的概念,而不是隻能寫計算特定求和的過程。

1.3.2用lambda構造過程


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章