一、關於符號類型 符號類型又稱引用類型,在概要一文中本人介紹得非常的模糊,使很多初學者不理解。符號類型在Scheme語言中是最基礎也是最重要的一種類型,這是因為Scheme語言的祖先Lisp語言的最初目的就是符號處理,在Scheme語言中幾乎所有的東西都可以看做是符號或做為符號列表來處理,這也是我們把符號類型做為第一個問題研究的原因。 與符號類型相關的關鍵字有四個,分別是:quote, quasiquote, unquote和unquote-splicing,如下所示: 規范用法:(quote obj) 簡化用法:'obj (注意,'為右單引號,"雙引號下面的那個符號。) 意義:符號類型的定義,(quote obj)本身就是一個值,雖然它不如數字123這樣直觀。 規范用法:(quasiquote obj) 簡化用法:`obj (注意,`為左單引號,~波浪號下面的那個符號。) 意義:"類似符號"類型的定義,最好稱之為逆符號類型,它可以將符號類型轉換為具有實際意義的東西。 規范用法:(unquote obj) 簡化用法:,obj (注意,,逗號, (define s '(good morning)) guile> s (good morning) guile> (symbol? s) #f guile> (list? s) #t guile> (symbol? (list-ref s 1)) #t 從此示例中可以看出,用quote定義的列表的類型仍是列表,而列表中的某一值的類型則是符號類型。還可以看出有點類似於如下: (+ 1 (+ 2 (+ 3 (+ 4 5)))) ==> (+ 1 2 3 4 5) (list 'a 'b 'c 'd 'e) ==> '(a b c d e) 兩者有異曲同工之妙,減少了多余的操作符,使表達式更直觀,更容易理解。 從 '(1 2 3 4 5) ==> (1 2 3 4 5) 可以看出,由符號類型的定義來形成列表,這是Scheme語言繼承自LISP語言的傳統。 下面是在guile中的用法示例: guile> `(1 ,(+ 1 1) 3) (1 2 3) guile> (quasiquote (1 (unquote (+ 1 1)) 3)) (1 2 3) ;;;第一個是簡化用法,第二個是標准用法。 guile> `(1 ,@(map + '(1 3) '(2 4)) 9) (1 3 7 9) guile> (quasiquote (1 (unquote-splicing (map + (quote (1 3)) (quote (2 4)))) 9)) (1 3 7 9) ;;;第一個是簡化用法,第二個是標准用法(注意:,@ 兩個符號做為一個操作符來使用)。 從示例中我們可以看出,這些應用多數與列表有關,而處理列表是Scheme語言的關鍵所在。符號類型的用法對深入理解Scheme語言也非常關鍵,因為Scheme語言本身就可以理解為是這種符號類型的列表,處理符號類型就是處理Scheme語言本身。
二、關於尾遞歸 數列問題是研究遞歸的非常好的范例,在王垠的主頁中有關於用菲波那契數列來說明非尾遞歸與尾遞歸之間區別和尾遞歸的好處的一個例子,見 http://learn.tsinghua.edu.cn/homepage/2001315450/wiki/TailRecursion.Html 。我們這裡用更簡單一點的問題,求累計的問題來說明,即求自然數1+2+3+4+ ... +n的和。事實上就是設計一個過程,給它一個參數n,求1+2+3+ ... +n的和,我們首設計一個suma過程,代碼如下: #! /usr/local/bin/guile -s !# (define suma (lambda (n) (if (= n 1) 1 (+ n (suma (- n 1)))))) (display "(suma 100) ==> ") (display (suma 100)) (newline) (display "(suma 818) ==> ") (display (suma 818)) (newline) 運行這段代碼,會出現如下結果: (suma 100) ==> 5050 (suma 818) ==> 334971 說明: 以(suma 5)為例,表達式展開後: (suma 5) (+ 5 (suma 4)) (+ 5 4 (suma 3)) (+ 5 4 3 (suma 2)) (+ 5 4 3 2 (suma 1)) (+ 5 4 3 2 1) ==> 15 如果n為1000的話則會最終形成(+ 1000 999 ... 3 2 1)這樣長度驚人的表達式,結果直接導致guile的崩潰。 為什麼會是818呢?因為819是會溢出的,出錯,得不到結果,這可能大出乎我們意料之外,因為如果用C來寫這樣功能的程序代碼,可能會求到6位數而不出問題。這一過程是用非尾遞歸來完成的,它的擴張呈指數級增長。代碼的迅速膨脹,使guile沒有處理到1000就崩潰了。 我們再來看看采用尾遞歸的情況,代碼如下: #! /usr/local/bin/guile -s !# (define sumb (lambda (n) (let f ((i n) (a 1)) (if (= i 1) a (f (- i 1) (+ a i)))))) (display "(sumb 100) ==> ") (display (sumb 100)) (newline) (display "(sumb 1000) ==> ") (display (sumb 1000)) (newline) 運行結果如下: (sumb 100) ==> 5050 (sumb 1000) ==> 500500 還是以n為5的情況來說明: (sumb 5) (f 5 1) (f 4 6) (f 3 10) (f 2 13) (f 1 15) ==> 15 這樣的話,始終是依次計算,不會出現列表膨脹,所以n為1000時也不會出錯,計算速度也很快。 此結果大超出了非尾遞歸的818限制,參數是10000也沒問題,因它采用尾遞歸,代碼根本沒有膨脹,而是依次計算。首先是在過程內部綁定了一個過程f,它有兩個參數,一個i的值來自sum過程的參數n,另一個參數a定義值為1,當i值不等於時,仍調用f,第一個參數(也就是i)減1,第二個參數(也就是a)的值在原來的基礎上加上i,當i的值為1是返回a,也就此sum過程的結果。理解這些後,你會發現事實上尾遞歸是在過程的綁定和過程的參數上做文章,用參數來保存運算結果,遞歸調用綁定的過程,最終達到運算目的。
三、關於過程參數的問題 過程的多參數問題對初學者不太好理解,一般情況下我們處理過程時,過程參數的數量是固定的,當過程的參數數量不固定時怎麼辦呢?對了,時刻記住列表,把過程的參數做為一個列表來處理,如求和過程:(sum arg1 arg2 ...),(初學者可能對如何實現定義這樣的過程無從下手不知所措),我們如何來求這些參數的和呢?看下面的代碼: guile> (define sum (lambda args (apply + args))) guile> sum #
guile> (sum 1 2 3 4 5) 15 從中可以看出,lambda的格式有所變化,由原來的((lambda arg1 arg2 ...) body ...)變成了(lambda args body ...),也就是將原來的多個項組成的列表,改成了用一個名稱args來標識的列表,其本質並沒有變,變的是我們的方法,由原來的單項處理變成了統一處理的列表。 這裡還用到了for-each過程,通過下面代碼來看一下for-each過程的一般用法: guile> (define newdisplay (lambda (x) (begin (display x)(newline)))) guile> newdisplay #
guile> (define tt (lambda args (for-each newdisplay args))) guile> tt #
guile> (tt 'abc 'efg 'tomson) abc efg tomson for-each過程的一般用法是(for-each 過程 列表),此中的過程可以是我們自定義的,也可以是系統提供的,還可以是lambda 表達式。 棧結構是一種簡單而又有意義的數據結構,我們可以用列表來模擬一個簡單的棧,下面是代碼: #! /usr/local/bin/guile -s !# (define stack '()) (define push! (lambda (x) (set! stack (cons x stack)))) (define pop! (lambda () (let ((temp (car stack))) (set! stack (cdr stack)) temp))) (push! 9) (push! 8) (push! 7) (display stack) (newline) (display (pop!)) (newline) (display stack) (newline) 結果如下: (7 8 9) 7 (8 9) 這裡面我們定義了一個變量stack來表示棧,定義一個過程push!向棧內壓數據,同時還定義了一個過程pop!來從棧內彈出數據,完成了基本的棧功能。這段代碼的缺點是要定義一個外部的變量來表示棧,同時還有兩個過程,如果創建多個棧的話就需要更多的過程和變量了,這在某些情況下是不可想象的,如果程序中要用100個棧,我們就不得不100次復制和更改上面的代碼。如何解決這一問題呢?看下面的代碼: #! /usr/local/b