Scheme语言深入
本文轉(zhuǎn)自:http://www.ibm.com/developerworks/cn/linux/l-schm/part3/
一、關(guān)于符號(hào)類型
符號(hào)類型又稱引用類型,在概要一文中本人介紹得非常的模糊,使很多初學(xué)者不理解。符號(hào)類型在Scheme語(yǔ)言中是最基礎(chǔ)也是最重要的一種類型,這是因?yàn)镾cheme語(yǔ)言的祖先Lisp語(yǔ)言的最初目的就是符號(hào)處理,在Scheme語(yǔ)言中幾乎所有的東西都可以看做是符號(hào)或做為符號(hào)列表來(lái)處理,這也是我們把符號(hào)類型做為第一個(gè)問(wèn)題研究的原因。
與符號(hào)類型相關(guān)的關(guān)鍵字有四個(gè),分別是:quote, quasiquote, unquote和unquote-splicing,如下所示:
規(guī)范用法:(quote obj)
簡(jiǎn)化用法:'obj (注意,'為右單引號(hào),"雙引號(hào)下面的那個(gè)符號(hào)。)
意義:符號(hào)類型的定義,(quote obj)本身就是一個(gè)值,雖然它不如數(shù)字123這樣直觀。
規(guī)范用法:(quasiquote obj)
簡(jiǎn)化用法:`obj (注意,`為左單引號(hào),~波浪號(hào)下面的那個(gè)符號(hào)。)
意義:"類似符號(hào)"類型的定義,最好稱之為逆符號(hào)類型,它可以將符號(hào)類型轉(zhuǎn)換為具有實(shí)際意義的東西。
規(guī)范用法:(unquote obj)
簡(jiǎn)化用法:,obj (注意,,逗號(hào),<小于號(hào)下面的那個(gè)符號(hào)。)
意義:"非符號(hào)"類型的定義,非符號(hào)類型出現(xiàn)在符號(hào)類型或逆符號(hào)類型定義中間,它不直接做為符號(hào)類型使用,而是將運(yùn)算結(jié)果做為符號(hào)類型的一部分。
規(guī)范用法:(unquote-splicing obj)
簡(jiǎn)化用法:,@obj
意義:非符號(hào)類型的拼接,注意:,@ 兩個(gè)符號(hào)做為一個(gè)操作符來(lái)使用)。當(dāng)非符號(hào)類型是一些復(fù)雜算法時(shí),需要用它來(lái)做一下拼接,以達(dá)到符號(hào)類型的目的。
上面所說(shuō)的所有規(guī)范用法和簡(jiǎn)化用法的功能都是相同的。
符號(hào)類型的意義在于,一個(gè)說(shuō)明,英文單詞zebra指的是活生生的斑馬,而'zebra或(quote zebra)指的是由字母z、e、b、r、a構(gòu)成的這串符號(hào)(不是字符串),就象我們定義變量(define x 100),這時(shí)x指的就是100這個(gè)數(shù)值,而'x或(quote x)則代表字母x構(gòu)成的這個(gè)符號(hào)。
首先看一段代碼:
| guile> (define s '(good morning)) guile> s (good morning) guile> (symbol? s) #f guile> (list? s) #t guile> (symbol? (list-ref s 1)) #t |
從此示例中可以看出,用quote定義的列表的類型仍是列表,而列表中的某一值的類型則是符號(hào)類型。還可以看出有點(diǎn)類似于如下:
| (+ 1 (+ 2 (+ 3 (+ 4 5)))) ==> (+ 1 2 3 4 5) (list 'a 'b 'c 'd 'e) ==> '(a b c d e) |
兩者有異曲同工之妙,減少了多余的操作符,使表達(dá)式更直觀,更容易理解。
從 '(1 2 3 4 5) ==> (1 2 3 4 5) 可以看出,由符號(hào)類型的定義來(lái)形成列表,這是Scheme語(yǔ)言繼承自LISP語(yǔ)言的傳統(tǒng)。
下面是在guile中的用法示例:
| guile> `(1 ,(+ 1 1) 3) (1 2 3) guile> (quasiquote (1 (unquote (+ 1 1)) 3)) (1 2 3) ;;;第一個(gè)是簡(jiǎn)化用法,第二個(gè)是標(biāo)準(zhǔn)用法。 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) ;;;第一個(gè)是簡(jiǎn)化用法,第二個(gè)是標(biāo)準(zhǔn)用法(注意:,@ 兩個(gè)符號(hào)做為一個(gè)操作符來(lái)使用)。 |
從示例中我們可以看出,這些應(yīng)用多數(shù)與列表有關(guān),而處理列表是Scheme語(yǔ)言的關(guān)鍵所在。符號(hào)類型的用法對(duì)深入理解Scheme語(yǔ)言也非常關(guān)鍵,因?yàn)镾cheme語(yǔ)言本身就可以理解為是這種符號(hào)類型的列表,處理符號(hào)類型就是處理Scheme語(yǔ)言本身。
回頁(yè)首
二、關(guān)于尾遞歸
數(shù)列問(wèn)題是研究遞歸的非常好的范例,在王垠的主頁(yè)中有關(guān)于用菲波那契數(shù)列來(lái)說(shuō)明非尾遞歸與尾遞歸之間區(qū)別和尾遞歸的好處的一個(gè)例子,見 http://learn.tsinghua.edu.cn/homepage/2001315450/wiki/TailRecursion.html 。我們這里用更簡(jiǎn)單一點(diǎn)的問(wèn)題,求累計(jì)的問(wèn)題來(lái)說(shuō)明,即求自然數(shù)1+2+3+4+ ... +n的和。事實(shí)上就是設(shè)計(jì)一個(gè)過(guò)程,給它一個(gè)參數(shù)n,求1+2+3+ ... +n的和,我們首設(shè)計(jì)一個(gè)suma過(guò)程,代碼如下:
| #! /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) 運(yùn)行這段代碼,會(huì)出現(xiàn)如下結(jié)果: (suma 100) ==> 5050 (suma 818) ==> 334971 |
說(shuō)明:
以(suma 5)為例,表達(dá)式展開后:
| (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的話則會(huì)最終形成(+ 1000 999 ... 3 2 1)這樣長(zhǎng)度驚人的表達(dá)式,結(jié)果直接導(dǎo)致guile的崩潰。
為什么會(huì)是818呢?因?yàn)?19是會(huì)溢出的,出錯(cuò),得不到結(jié)果,這可能大出乎我們意料之外,因?yàn)槿绻肅來(lái)寫這樣功能的程序代碼,可能會(huì)求到6位數(shù)而不出問(wèn)題。這一過(guò)程是用非尾遞歸來(lái)完成的,它的擴(kuò)張呈指數(shù)級(jí)增長(zhǎng)。代碼的迅速膨脹,使guile沒(méi)有處理到1000就崩潰了。
我們?cè)賮?lái)看看采用尾遞歸的情況,代碼如下:
| #! /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) 運(yùn)行結(jié)果如下: (sumb 100) ==> 5050 (sumb 1000) ==> 500500 |
還是以n為5的情況來(lái)說(shuō)明:
| (sumb 5) (f 5 1) (f 4 6) (f 3 10) (f 2 13) (f 1 15) ==> 15 |
這樣的話,始終是依次計(jì)算,不會(huì)出現(xiàn)列表膨脹,所以n為1000時(shí)也不會(huì)出錯(cuò),計(jì)算速度也很快。
此結(jié)果大超出了非尾遞歸的818限制,參數(shù)是10000也沒(méi)問(wèn)題,因它采用尾遞歸,代碼根本沒(méi)有膨脹,而是依次計(jì)算。首先是在過(guò)程內(nèi)部綁定了一個(gè)過(guò)程f,它有兩個(gè)參數(shù),一個(gè)i的值來(lái)自sum過(guò)程的參數(shù)n,另一個(gè)參數(shù)a定義值為1,當(dāng)i值不等于時(shí),仍調(diào)用f,第一個(gè)參數(shù)(也就是i)減1,第二個(gè)參數(shù)(也就是a)的值在原來(lái)的基礎(chǔ)上加上i,當(dāng)i的值為1是返回a,也就此sum過(guò)程的結(jié)果。理解這些后,你會(huì)發(fā)現(xiàn)事實(shí)上尾遞歸是在過(guò)程的綁定和過(guò)程的參數(shù)上做文章,用參數(shù)來(lái)保存運(yùn)算結(jié)果,遞歸調(diào)用綁定的過(guò)程,最終達(dá)到運(yùn)算目的。
回頁(yè)首
三、關(guān)于過(guò)程參數(shù)的問(wèn)題
過(guò)程的多參數(shù)問(wèn)題對(duì)初學(xué)者不太好理解,一般情況下我們處理過(guò)程時(shí),過(guò)程參數(shù)的數(shù)量是固定的,當(dāng)過(guò)程的參數(shù)數(shù)量不固定時(shí)怎么辦呢?對(duì)了,時(shí)刻記住列表,把過(guò)程的參數(shù)做為一個(gè)列表來(lái)處理,如求和過(guò)程:(sum arg1 arg2 ...),(初學(xué)者可能對(duì)如何實(shí)現(xiàn)定義這樣的過(guò)程無(wú)從下手不知所措),我們?nèi)绾蝸?lái)求這些參數(shù)的和呢?看下面的代碼:
| guile> (define sum (lambda args (apply + args))) guile> sum #<procedure sum args> guile> (sum 1 2 3 4 5) 15 |
從中可以看出,lambda的格式有所變化,由原來(lái)的((lambda arg1 arg2 ...) body ...)變成了(lambda args body ...),也就是將原來(lái)的多個(gè)項(xiàng)組成的列表,改成了用一個(gè)名稱args來(lái)標(biāo)識(shí)的列表,其本質(zhì)并沒(méi)有變,變的是我們的方法,由原來(lái)的單項(xiàng)處理變成了統(tǒng)一處理的列表。
這里還用到了for-each過(guò)程,通過(guò)下面代碼來(lái)看一下for-each過(guò)程的一般用法:
| guile> (define newdisplay (lambda (x) (begin (display x)(newline)))) guile> newdisplay #<procedure newdisplay (x)> guile> (define tt (lambda args (for-each newdisplay args))) guile> tt #<procedure tt args> guile> (tt 'abc 'efg 'tomson) abc efg tomson |
for-each過(guò)程的一般用法是(for-each 過(guò)程 列表),此中的過(guò)程可以是我們自定義的,也可以是系統(tǒng)提供的,還可以是lambda 表達(dá)式。
棧結(jié)構(gòu)是一種簡(jiǎn)單而又有意義的數(shù)據(jù)結(jié)構(gòu),我們可以用列表來(lái)模擬一個(gè)簡(jiǎn)單的棧,下面是代碼:
| #! /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) |
結(jié)果如下:
| (7 8 9) 7 (8 9) |
這里面我們定義了一個(gè)變量stack來(lái)表示棧,定義一個(gè)過(guò)程push!向棧內(nèi)壓數(shù)據(jù),同時(shí)還定義了一個(gè)過(guò)程pop!來(lái)從棧內(nèi)彈出數(shù)據(jù),完成了基本的棧功能。這段代碼的缺點(diǎn)是要定義一個(gè)外部的變量來(lái)表示棧,同時(shí)還有兩個(gè)過(guò)程,如果創(chuàng)建多個(gè)棧的話就需要更多的過(guò)程和變量了,這在某些情況下是不可想象的,如果程序中要用100個(gè)棧,我們就不得不100次復(fù)制和更改上面的代碼。如何解決這一問(wèn)題呢?看下面的代碼:
| #! /usr/local/bin/guile -s !# (define make-stack(lambda ()(let ((st '()))(lambda (process arg)(case process((push!) (begin(set! st (cons arg st))st))((pop!) (let ((temp (car st)))(set! st (cdr st))temp))((view) (display st))(else "error!")))))) (define s (make-stack)) (display (s 'push! 9)) (newline) (display (s 'push! 8)) (newline) (display (s 'push! 7)) (newline) (display (s 'pop! 0)) (newline) (s 'view 0) (newline) |
結(jié)果如下:
| (9) (8 9) (7 8 9) 7 (8 9) |
在上面代碼中定義的make-stack過(guò)程,它的形式是一種特殊的情況,在lambda表達(dá)式里面又嵌有l(wèi)ambda表達(dá)式,在使用這類過(guò)程時(shí),先要調(diào)用這一過(guò)程定義一個(gè)變量(這個(gè)變量其實(shí)就是第二個(gè)lambda表達(dá)式),然后將這個(gè)變量再做為一個(gè)過(guò)程來(lái)直接調(diào)用(事實(shí)上也就是生成了一個(gè)過(guò)程),就像代碼中的(s 'push 9) 。
我們首先綁定了一個(gè)變量st為空值做為棧的基礎(chǔ),與棧有關(guān)的操作都圍繞它展開,這樣的話前面提到代碼重復(fù)問(wèn)題就不會(huì)出現(xiàn)了,你可以定義任意多個(gè)棧。這段代碼里還用到了case結(jié)構(gòu),它有點(diǎn)像C語(yǔ)言中的switch語(yǔ)句,用它判斷第二個(gè)lambda表達(dá)式的第一個(gè)參數(shù),也就是要對(duì)棧的操作,在調(diào)用時(shí)要用符號(hào)變量來(lái)使用,否則會(huì)出錯(cuò),因?yàn)?#39;push結(jié)果就是push,所以在過(guò)程定義中直接使用push,而調(diào)用時(shí)用'push。從這段代碼中你會(huì)意識(shí)到變量和符號(hào)的重要性了。
這段代碼中我們?nèi)杂蒙厦娲a的形式,用列表來(lái)模擬棧,因?yàn)檫@更能體現(xiàn)棧的原理和列表這一Scheme語(yǔ)言的基礎(chǔ)數(shù)據(jù)類型的做用。細(xì)心的讀者朋友會(huì)發(fā)現(xiàn)我們對(duì)棧進(jìn)行pop操作時(shí)的調(diào)用是(s 'pop! 0),而正確的操作應(yīng)該是(s 'pop!),'view也同樣;這是因?yàn)槲覀兊诙€(gè)lambda表達(dá)式是lambda (process arg),為了不出錯(cuò),不得不用這樣的調(diào)用,如果將第二個(gè)lambda表達(dá)式改為lambda (process . arg)就可以避免這種尷尬情況了,但結(jié)果可能并不是我們想要的,棧會(huì)變成((7) (8) (9))這種情況。
如何更好的實(shí)現(xiàn)一個(gè)棧呢?對(duì)了,改變現(xiàn)有的形式,使用list(不用原始的cons)或vector來(lái)模擬,將 lambda (process arg) 改成 (process . arg) 或 (process . args) (注意,這兩者可不一樣啊!),這就要看你對(duì)棧結(jié)構(gòu)的理解和編碼水平了,相信參照這一代碼你會(huì)模擬出更實(shí)用更快捷的棧結(jié)構(gòu)來(lái)的。(本文代碼中有一個(gè)用list來(lái)模擬棧結(jié)構(gòu)的稍完整的代碼)這里我們還會(huì)發(fā)現(xiàn)一個(gè)小小的慣例,如果過(guò)程要更改變量的值,那么它的過(guò)程名后一定要加一個(gè)!;而如果過(guò)程是一個(gè)判斷,結(jié)果為邏輯值時(shí),它的過(guò)程名后一定要加一個(gè)?,這會(huì)使別人很快理解你的代碼。
回頁(yè)首
四、關(guān)于continuation
Scheme語(yǔ)言相對(duì)Lisp語(yǔ)言的重要特征是提出并實(shí)現(xiàn)了continuation,這是讓初學(xué)者最難理解,也是最讓程序員心動(dòng)的特征。那么Continuation到底是什么呢?在運(yùn)算Scheme表達(dá)式過(guò)程中,我們必須跟蹤兩個(gè)東西:1、運(yùn)算什么?2、什么與值相關(guān)?看一看下面表達(dá)式的計(jì)算: (if (null? x) (quote ()) (car x)) 。其中首先要運(yùn)算的是(null? x),然后在這個(gè)值的基礎(chǔ)上再運(yùn)算(quote ())或(cdr x),要運(yùn)算的是(null? x)而與值相關(guān)的是(quote ())和(car x);這里把與值相關(guān)的稱為運(yùn)算的continuation。我們看(null? x)表達(dá)式中與值相關(guān)的東西就有三個(gè),1、表達(dá)式本身,結(jié)果為#t或#f;2、過(guò)程null?,它是一個(gè)lambda表達(dá)式;3、變量x,它應(yīng)用當(dāng)有一個(gè)值。那么上面的表達(dá)式里面有幾個(gè)continuation呢?答案是六個(gè),上面的表達(dá)式本身,我們說(shuō)過(guò)的三個(gè),car也是一個(gè)lambda表達(dá)式,還有它后面的x,也和值相關(guān);而(car x)沒(méi)有計(jì)算在內(nèi),因?yàn)樗c上面表達(dá)式結(jié)果之一是相同的。
在任何表達(dá)式中,Continuation可以用一個(gè)叫call-with-current-continuation的過(guò)程來(lái)得到,多數(shù)情況下它被簡(jiǎn)化為call/cc。在guile的1.6.4或以前版本中,簡(jiǎn)化代碼如下所示:
| (define call/cc call-with-current-continuation) |
而在其它的Scheme語(yǔ)言的實(shí)現(xiàn)版本中完全可以不用這樣定義,而直接使用call/cc。
call/cc過(guò)程的唯一個(gè)參數(shù)應(yīng)該是一個(gè)過(guò)程(或lambda表達(dá)式),而且這個(gè)過(guò)程只能有一個(gè)參數(shù),continuation就綁定在這個(gè)參數(shù)上。看下面的代碼:
| guile> (define call/cc call-with-current-continuation) guile> call/cc #<procedure call-with-current-continuation (proc)> guile> (call/cc (lambda (k) 5)) 5 ;;;此時(shí)過(guò)程參數(shù)k未用到,所以取過(guò)程的返回值5做為結(jié)果 guile> (call/cc (lambda (k) (* 5 (k 8)))) 8 ;;;此時(shí)過(guò)程參數(shù)k綁定為8,所以其結(jié)果為8 guile> (+ 2 (call/cc (lambda (k) (* 5 (k 8))))) 10 ;;;此時(shí)結(jié)果在我們意料之中了 |
可以利用call/cc這一特點(diǎn),讓它從一個(gè)循環(huán)中跳出來(lái),這有點(diǎn)像C語(yǔ)言中的break,看下面的代碼:
| guile> (call/cc (lambda (return)(for-each (lambda (x) (if (< x 0) (return x)))'(99 88 77 66 55))#t)) #t |
其結(jié)果為#t,因?yàn)閒or-each運(yùn)算過(guò)程中未出現(xiàn)過(guò)(< x 0)的情況,所以(lambda (return ) ...)的結(jié)果為#t,call/cc取此值為最終結(jié)果。
| guile> (call/cc (lambda (return)(for-each (lambda (x) (if (< x 0) (return x)))'(11 22 33 44 -55 66 77))#t)) -55 |
其結(jié)果為-55,因?yàn)楫?dāng)遇到小于0的數(shù)時(shí),return就綁定x,call/cc就返回此值,即從for-each處理過(guò)程中跳出來(lái),這是call/cc的重要功能之一,也是最基本的功能。
call/cc 還可以這樣操作:
| guile> (define foo #f) guile> (call/cc (lambda (bar) (set! foo bar) 123)) 123 guile> (foo 456) 456 guile> (foo 'abc) abc |
上面的操作中,call/cc綁定了lambda表達(dá)式的參數(shù)bar,而后我們又設(shè)定foo的值為bar,此時(shí)foo即為一個(gè)靈活的continuation;最后我們又讓lambda表達(dá)式的值為123,所以整個(gè)call/cc表達(dá)式的值為123,如果我們不加這個(gè)123而直接結(jié)束這個(gè)表達(dá)式的話,此表達(dá)式就沒(méi)有結(jié)果,不返回任何值。
當(dāng)foo成為一個(gè)continuation后,我們可以象調(diào)用過(guò)程那樣來(lái)調(diào)用它(當(dāng)然它并不是過(guò)程),其后面綁定的值即為此continuation的結(jié)果,如上面的代碼運(yùn)行所示。
這段代碼的前提是有call/cc的定義,如果你直接運(yùn)行g(shù)uile后就輸入上面的代碼是肯定會(huì)出錯(cuò)的。
在趙蔚的文章 http://www.ibm.com/developerworks/cn/linux/l-scheme/part2/index.shtml中提到過(guò)由David Madore先生提出的"陰陽(yáng)迷題",下面我們通過(guò)對(duì)它的研究來(lái)理解一下continuation,代碼如下所示(為解釋方便,稍做改動(dòng)):
| (let* ((yin ((lambda (foo) (display "@") foo) (call/cc (lambda (bar) bar))))(yang ((lambda (foo) (display "*") foo) (call/cc (lambda (bar) bar)))))(yin yang)) |
我們進(jìn)行一些簡(jiǎn)化,將其中的lambda表達(dá)式定義為過(guò)程,使其看起來(lái)更清晰:
| (define bar (lambda (bar) bar)) (define foox (lambda (foo) (display "@") foo)) (define fooy (lambda (foo) (display "*") foo)) |
則上面的繁瑣的表達(dá)式可以變成為:
| (let* ((yin (foox (call/cc bar)))(yang (fooy (call/cc bar))))(yin yang)) |
將let*改變成let,使其進(jìn)一步簡(jiǎn)化為:
| (let ((yin (foox (call/cc bar))))(let ((yang (fooy (call/cc bar))))(yin yang))) |
最后將let去掉,繼而成為:
| ((foox (call/cc bar)) (fooy (call/cc bar))) |
經(jīng)過(guò)這一翻簡(jiǎn)化處理(對(duì)初學(xué)者是有必要的),相信我們對(duì)Scheme語(yǔ)言會(huì)有新的認(rèn)識(shí)。需要說(shuō)明的是每一次簡(jiǎn)化后,都要運(yùn)行一下,保證它的結(jié)果如下所示,否則說(shuō)明我們簡(jiǎn)化過(guò)程中有錯(cuò)誤:
| @*@**@***@****@*****@******@*******@********@*********@********** ...... |
在理解continuation之前,這一結(jié)果是我們無(wú)論如何也想不到的,如果你有足夠奈心的話,你會(huì)發(fā)現(xiàn)它會(huì)按這一規(guī)律無(wú)限的延長(zhǎng),在我的老機(jī)上從11:20一直到15:20仍無(wú)跡象表明要停止。
為什么會(huì)出現(xiàn)這一結(jié)果呢?首先看看我們?yōu)榱撕?jiǎn)化而定義的三個(gè)過(guò)程bar、foox和fooy,bar是Scheme語(yǔ)言中最簡(jiǎn)單的過(guò)程,只有一個(gè)參數(shù),并且返回這個(gè)參數(shù);foox和fooy比它們只多了一步,執(zhí)行完一個(gè)display過(guò)程后再返回這個(gè)參數(shù);這樣簡(jiǎn)單的兩個(gè)過(guò)程確出現(xiàn)如此復(fù)雜的結(jié)果,原因全在call/cc身上。
首先,看(call/cc bar)表達(dá)式,它形成了一個(gè)靈活的continuation,用它來(lái)做foox的參數(shù),表達(dá)式(foox (call/cc bar))則形成一個(gè)新的靈活的continuation,即顯示一個(gè)@字符同時(shí)也是一個(gè)continuation。
再看,表達(dá)式((call/cc bar) (foox bar))的結(jié)果與表達(dá)式((foox bar) (foox bar))的結(jié)果相同,均為顯示兩個(gè)@字符,同時(shí)返回一個(gè)過(guò)程#<procedure bar (bar)>,這就讓我們?cè)谀撤N程度上理解了表達(dá)式(call/cc procedure?)的結(jié)果為#t了,因?yàn)?procedure? procedure?)也為#t;而(call/cc boolean?)則不然,因?yàn)?boolean? boolean?)為#f。
從上面我們可以看出表達(dá)式((call/cc bar) (foox (call/cc bar)))實(shí)際則是與表達(dá)式((foox (call/cc bar)) (foox (call/cc bar))),運(yùn)行一下會(huì)發(fā)現(xiàn),兩者確實(shí)相同,顯示@字符,無(wú)窮無(wú)盡,看來(lái)(call/cc bar)這個(gè)參數(shù)起到了關(guān)鍵作用。那么再看我們的陰陽(yáng)表達(dá)式((foox (call/cc bar)) (fooy (call/cc bar))),前者(foox (call/cc bar))為一個(gè)顯示字符@的continuation,我們表示為*cc;后者(fooy (call/cc bar))為一個(gè)顯示字符*的continuation,我們表示為*cc;則在@cc的調(diào)動(dòng)指揮下,每次*cc再加下一個(gè)(fooy (call/cc bar)),形成**cc,再加后,如此形成我們前面的效果。過(guò)程如下所示:
| @cc *cc @cc **cc @cc ***cc @cc ****cc 一直至無(wú)窮盡 |
前一段時(shí)間,"暈"這個(gè)字總會(huì)出現(xiàn)在網(wǎng)友的聊天中,相信現(xiàn)在不"暈"了。我們給上面的代碼改動(dòng)一下:
| #! /usr/local/bin/guile -s !# (define call/cc call-with-current-continuation) (define n 0) (define bar (lambda (bar) bar)) (define foo (lambda (foo) (display n) (newline) (set! n (+ n 1)) foo)) ((call/cc bar) (foo (call/cc bar))) |
這樣形成了,0 1 2 3 4 ......無(wú)限制的循環(huán),由此我們解決了一個(gè)不用循環(huán)語(yǔ)句來(lái)形成循環(huán)的問(wèn)題。
回頁(yè)首
五、關(guān)于記錄類型
在guile中提供了很多復(fù)雜的復(fù)合類型,如record,struct,hashtable,array等等,record類型是其中較簡(jiǎn)單的一種,我們這里稱之為記錄類型,這種類型有點(diǎn)像C語(yǔ)言中的結(jié)構(gòu),更像C++中的類。通過(guò)它我們可以了解一些面向?qū)ο笏枷朐赟cheme語(yǔ)言中的應(yīng)用。記錄類型包括九個(gè)相關(guān)過(guò)程,以下是簡(jiǎn)單介紹:
record? 記錄類型的判斷過(guò)程
make-record-type 創(chuàng)建記錄類型,兩個(gè)參數(shù),類型的名稱和類型的成員名稱列表
record-constructor 創(chuàng)建記錄類型構(gòu)建過(guò)程,一個(gè)參數(shù),類型
record-predicate 創(chuàng)建記錄類型的判斷過(guò)程,用此過(guò)程某一變量是否為已創(chuàng)建的記錄類型
record-accessor 創(chuàng)建記錄類型的get系列過(guò)程,兩個(gè)參數(shù),類型和表示成員名稱的符號(hào)
record-modifier 創(chuàng)建記錄類型的set系列過(guò)程,同上
record-type-descriptor 一般不用,可忽略
record-type-name 取得記錄類型的名字,返回字符串
record-type-fields 取得記錄類型的成員名字列表
要想知道如何定義一個(gè)記錄類型和上面提到的相關(guān)過(guò)程的用法,具體代碼是必不可少的,下面是一個(gè)簡(jiǎn)單的示例:
| #! /usr/local/bin/guile -s !# (define girl (make-record-type "girl" '(name info))) ;;定義record類型girl,包含兩個(gè)成員name和info,其中name為一字符串,info為一過(guò)程用來(lái)顯示信息 (define girl-init! (record-constructor girl)) ;;定義girl的初始化過(guò)程 (define girl-name-get (record-accessor girl 'name)) ;;定義取得girl類型的name成員的值的過(guò)程 (define girl-name-set! (record-modifier girl 'name)) ;;定義設(shè)定girl類型的name成員的值的過(guò)程 (define girl-info-get (record-accessor girl 'info)) ;;定義取得girl類型的info成員的值的過(guò)程 (define girl-info-set! (record-modifier girl 'info)) ;;定義設(shè)定girl類型的info成員的值的過(guò)程 (define hi(lambda (name)(display "Hi! I'm ")(display name)(display "."))) ;;定義hi過(guò)程,顯示"Hi! I'm " 加字符串 name 加 "." (define g (girl-init! "Lucy" hi)) ;;定義一個(gè)girl類型的變量g,其成員name值為"Lucy",成員info值為上面定義的hi過(guò)程 ((girl-info-get g) (girl-name-get g)) (newline) ;;取得girl類型變量g的info成員,做為過(guò)程來(lái)執(zhí)行它,取得girl類型變量g的name成員做為此過(guò)程的參數(shù) |
這段代碼的運(yùn)行結(jié)果為: Hi! I'm Lucy.
代碼中的注釋相信大家都能看懂,需要說(shuō)的是當(dāng)我們用定義一個(gè)用make-record-type創(chuàng)建的記錄類型后,就可以用record?來(lái)判斷此類型是否為記錄類型了,即 (record? girl) ==> #t 。
還有就是可以用代碼 (define girl? (record-predicate girl)) 來(lái)定義一個(gè)此記錄類型girl的判斷過(guò)程girl?,也就是 (girl? g) ==> #t 。
還有就是下面的結(jié)果也應(yīng)該在我們的想象之中:
| (record-type-name girl) ==> "girl" (record-type-fields girl) ==> (name info) |
從這個(gè)簡(jiǎn)單的例子來(lái)看,記錄類型已經(jīng)具備了面向?qū)ο蟮木幊趟枷胨蟮囊恍┍貍涞臇|西,而且更具有Scheme語(yǔ)言自己的特色。相信在我的這個(gè)例子基礎(chǔ)上你可以創(chuàng)建一個(gè)更優(yōu)秀的girl來(lái)。
回頁(yè)首
六、關(guān)于宏定義
Scheme語(yǔ)言中的宏定義類似于自己定義一個(gè)Scheme語(yǔ)言關(guān)鍵字,可以實(shí)現(xiàn)不同的功能,很多關(guān)鍵字都可以通過(guò)宏定義來(lái)實(shí)現(xiàn),我們?cè)诙鄶?shù)參考資料中都可以看到這樣的例子。
在多數(shù)Scheme語(yǔ)言的實(shí)現(xiàn)中,都提供了不同形式的宏定義功能,在guile中提供了用defmacro或define-macro來(lái)定義宏,defmacro的格式為:
| (defmacro name (args ...) body ...) |
它等同于
| (define-macro (name args ...) body ...) |
我們來(lái)看一個(gè)簡(jiǎn)單的宏定義:
| #! /usr/local/bin/guile -s !# (define-macro (defobj name)`(begin(define ,(string->symbol (string-append "make-" name))(lambda ()(display "make object ok!\n")))(define ,(string->symbol (string-append name "?"))(lambda (obj)(if (eq? obj 'name)#t#f))))) (defobj "foo") (make-foo) (display (foo? 'name)) (newline) 這段程序的運(yùn)行結(jié)果如下: make object ok! #t |
從這段代碼中你可能看到了逆符號(hào)(quasiquote)以及相關(guān)的操作的重要性了,這里我們定義了一個(gè)宏defobj,當(dāng)運(yùn)行完(defobj "boy")這個(gè)宏時(shí),產(chǎn)生了兩個(gè)過(guò)程定義即make-foo和foo?,從這一點(diǎn)上來(lái)看,高性能的宏定義可以大大減輕我們代碼的重復(fù)使用的麻煩。還有就是guile系統(tǒng)中很多宏定義都是按上面的宏定義方式來(lái)進(jìn)行的。
在Scheme語(yǔ)言中,R5RS中的宏定義是通用的標(biāo)準(zhǔn),guile中通過(guò)調(diào)用syncase模塊來(lái)實(shí)現(xiàn)這一功能,你需要在代碼中加入:(use-modules (ice-9 syncase)) 來(lái)調(diào)用ice-9目錄下的syncase模塊。
下面的R5RS格式的宏定義實(shí)現(xiàn)了前面提到的sum功能和一個(gè)列表定義功能,它都有多參數(shù)的特點(diǎn),這在R5RS宏觀定義中很容易實(shí)現(xiàn):
| #! /usr/local/bin/guile -s !# (use-modules (ice-9 syncase)) (define-syntax sum(syntax-rules ()((_ exp1 exp2 ...)(+ exp1 exp2 ...)))) (display (sum 1 2 3 4 5)) (newline) (define-syntax ourlst(syntax-rules ()((_ exp)(cons exp '()))((_ exp1 exp2 ...)(cons exp1 (ourlst exp2 ...))))) (display (ourlst 1 2 3 4 5)) (newline) |
上面代碼的結(jié)果如下:
| 15 (1 2 3 4 5) |
在sum宏定義中,如果附合規(guī)則(_ exp1 exp2 ...)或(sum exp1 exp2 ...)將按(+ exp1 exp2 ...)的方式來(lái)處理,其中exp1、exp2表示Scheme表達(dá)式,而省略號(hào) ... 則表示更多的表達(dá)式。也就是說(shuō) (sum 1 2 3 4 5)將按(+ 1 2 3 4 5)來(lái)處理,其結(jié)果為15。在ourlst宏中則有兩個(gè)規(guī)則,第一是只有一個(gè)參數(shù)的情況,第二是多參數(shù)的情況,在多參數(shù)情況下還用到了遞歸,相信大家都能理解。
這是按R5RS標(biāo)準(zhǔn)來(lái)實(shí)現(xiàn)的最簡(jiǎn)單的兩個(gè)宏(當(dāng)然還有我在概要一文中提到的糟糕的start宏),相信通過(guò)這兩個(gè)宏的定義,您會(huì)理解并能進(jìn)行宏定義了。
回頁(yè)首
七、關(guān)于模塊
上面提到的syncase模塊是如何實(shí)現(xiàn)的呢?多數(shù)Scheme語(yǔ)言的實(shí)現(xiàn)版本都提供了一套模塊系統(tǒng),guile也不例外,看一看下面的簡(jiǎn)單的模塊定義:
| ;;;file : tt.scm , a module test here . (define-module (ice-9 tt):export (newdisplay)) (define newdisplay(lambda (str)(display str)(newline))) |
將其以tt.scm文件名保存。這段代碼中,首先用define-module表示定義一個(gè)模塊,而(ice-9 tt)則指明了模塊的具體位置和名稱,最后:export指出模塊中要導(dǎo)出的過(guò)程等的名稱,這們這里只有一個(gè)newdisplay,可以用列表來(lái)形成多個(gè)導(dǎo)出過(guò)程名。而下面的代碼則和我們普通的過(guò)程定義代碼相同,簡(jiǎn)單的定義了一個(gè)名為newdisplay的過(guò)程,功能是在要顯示的東西后面加一個(gè)換行符。
我們?cè)倬帉懸欢未a來(lái)測(cè)試一下這個(gè)模塊:
| #! /usr/local/bin/guile -s !# (use-modules (ice-9 tt)) (newdisplay "test use tt modules") |
這段代碼中用usemodules來(lái)調(diào)用我們上面定義的tt模塊,以test_tt.scm文件名保存,運(yùn)行后會(huì)出現(xiàn)錯(cuò)誤信息: ERROR: no code for module (ice-9 tt) 。
這是因?yàn)?#xff0c;默認(rèn)情況下,模塊所在目錄為/usr/local/share/guile/1.6/ice-9 或 /usr/share/guile/1.6/ice-9 。執(zhí)行命令: cp tt.scm /usr/local/share/guile/1.6/ice-9/ ,將模塊文件復(fù)制到相應(yīng)目錄,再執(zhí)行test_tt.scm文件,其輸出結(jié)果如下:(輸出字符串自動(dòng)換行了)
| test use tt modules |
這說(shuō)明我們成功的定義了一個(gè)模塊。我們?cè)诤甓x時(shí)用的syncase模塊實(shí)際上就是在/usr/local/share/guile/1.6/ice-9目錄中的syncase.scm文件,研究一下此目錄中的scm文件會(huì)發(fā)現(xiàn)很多定義模塊的技巧。
回頁(yè)首
八、關(guān)于eval
在概要一文中還沒(méi)有提到的是eval這個(gè)過(guò)程的用法,利用它可以實(shí)現(xiàn)用Scheme語(yǔ)言本身來(lái)解釋Scheme表達(dá)式的功能,這對(duì)一個(gè)學(xué)習(xí)編譯系統(tǒng)和Scheme語(yǔ)法功能的實(shí)現(xiàn)非常重要。下面是在guile中運(yùn)行eval過(guò)程來(lái)解釋Scheme表達(dá)式的情況:
| guile> (primitive-eval '(+ 2 3)) 5 guile> (eval '(+ 1 2) (interaction-environment)) 3 |
這里面包含了兩個(gè)版本的eval過(guò)程,首先是原始的guile內(nèi)部使用的primitive-eval,它可以直接解釋運(yùn)行Scheme表達(dá)式;第二個(gè)是正常的eval過(guò)程,它需要兩個(gè)參數(shù),一個(gè)是要解釋運(yùn)行的要Scheme表達(dá)式,第二個(gè)是表達(dá)式運(yùn)行的環(huán)境,可以由interaction-environment過(guò)程來(lái)獲得,如此則eval過(guò)程正常運(yùn)行。
可以想象用C語(yǔ)言來(lái)寫C語(yǔ)言編譯器對(duì)初學(xué)者來(lái)說(shuō)的難度,但掌握Scheme語(yǔ)法和eval的用法后,你會(huì)發(fā)現(xiàn)用Scheme語(yǔ)言來(lái)寫一個(gè)Scheme語(yǔ)言解釋器并不是很難,這可能成為你理解編譯原理的重要一步。
我們?cè)诟杏X到Scheme語(yǔ)言的簡(jiǎn)單易用的同時(shí),還應(yīng)該意識(shí)到它是一門富于挑戰(zhàn)意義的語(yǔ)言,相信現(xiàn)在我們能夠真正理解Scheme挑戰(zhàn)意義的所在了吧。(本文涉及到了Scheme語(yǔ)言中大多數(shù)的稍復(fù)雜一些的內(nèi)容,還請(qǐng)熱愛Scheme語(yǔ)言的朋友們多多指正。)
注:所有代碼在redhat9.0 guile1.6.4下測(cè)試通過(guò)
參考資料
- 《the Scheme Programming Language》 Tahird Edition ,《Scheme編程語(yǔ)言》第三版,在線網(wǎng)址: http://www.scheme.com/tspl3/ 這可能是最權(quán)威而又不失簡(jiǎn)潔的Scheme語(yǔ)言著作了。
- 本文中的部分代碼包在這里下載, scm.tar.gz。
- guile的主頁(yè), http://www.gnu.org/software/guile/ 這里又有新版本的guile要發(fā)布了,還有g(shù)uile的參考文檔,內(nèi)容相當(dāng)豐富。
- http://lonelycactus.com/guilebook/book1.html,一本關(guān)于如何使用guile做擴(kuò)展語(yǔ)言的書,還未完善。
- http://www.schemer.org,Scheme語(yǔ)言愛好者的大本營(yíng)。
總結(jié)
以上是生活随笔為你收集整理的Scheme语言深入的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 用python进行按掩膜提取的批量操作
- 下一篇: 谈谈反垃圾