開始學習Scheme
函數式編程(Functional Programming)是在MIT研究人工智能(Artificial Intelligence)時發明的,其編程語言爲Lisp。確切地說,Lisp是一個語言家族,包括無數的方言如:Scheme、Common Lisp、Haskell……等等。
最後一次學習Scheme已是去年7月份的事情了。原本只是出於興趣,以及拓寬本身思路的目的來學習。不曾想,因爲工做須要,Scheme編程已經成爲一個必備的技能了。其實這裏面也由辦公室政治的緣由,由於我原本是作驅動開發的。如今PO和Boss已經開始對立了,所以出現了PO想讓我作驅動,而Boss更傾向於根據本身的親信的興趣愛好來決定是否要擠掉個人驅動開發崗位。扯得有點遠了,生活就是如此,不能事事如意。仍是那句話,作一天和尚就撞好一天鐘。
出於興趣或者由於工做須要而開始學習一項技能時,學習方法的差別至關的大。出於興趣時,徹底能夠根據本身的喜愛、時間、背景知識等狀況來決定關注點,並能夠充分研究本身所關心的地方。然而,爲了工做而學習時,就須要綜合考慮諸多因素,好比項目的計劃、對技能熟悉程度的要求等來決定學習的重點。這種方式即是所謂"On job training",或者叫作經過實踐來學習。這種方式的好處就是能夠迅速的開始使用某項技能,缺點也很明顯,那就是很難有時間讓你去思考這項技能的本質。市場上充斥着"XXX天學會XXX"的書就不足爲怪了。
說了這麼多閒話,仍是言歸正傳吧。先來看看Scheme的基本概念。
#!r6rs
(import (rnrs base (6))
(rnrs unicode (6))
(rnrs bytevectors (6))
(rnrs lists (6))
(rnrs sorting (6))
(rnrs control (6))
(rnrs records syntactic (6))
(rnrs records procedural (6))
(rnrs records inspection (6))
(rnrs exceptions (6))
(rnrs conditions (6))
(rnrs io ports (6))
(rnrs io simple (6))
(rnrs files (6))
(rnrs programs (6))
(rnrs arithmetic fixnums (6))
(rnrs arithmetic flonums (6))
(rnrs arithmetic bitwise (6))
(rnrs syntax-case (6))
(rnrs hashtables (6))
(rnrs enums (6))
(rnrs eval (6))
(rnrs mutable-pairs (6))
(rnrs mutable-strings (6))
(rnrs r5rs (6)))
(display (find even? '(3 1 4 1 5 9)))
(newline)
(display "Hello\n")
(guard (exn [(equal? exn 5) 'five])
(guard (exn [(equal? exn 6) 'six])
(dynamic-wind
(lambda () (display "in") (newline))
(lambda () (raise 5))
(lambda () (display "out") (newline)))))
第一個,也是最基本的概念:S-expression(Symbolic-expression,符號表達式),最初適用於表示數據的,後來被用做Lisp語法的基礎。它是一個原子,或者一個(s-expr . s-expr)的表達式。後者爲一個pair。所謂list,就是由pair組成的:(x . (y . z))就是一個list,它能夠被簡寫爲(x y z)。原子主要是指數字、字符串和名字。S-expression是Lisp求值器能處理的語法形式。
第二個,則是一等函數(first-class funciton)。它是first-class object的一種,是編程語言裏的一等公民(first-class citizen)。first-class的含義是,當一個對象知足以下條件時:
1. 能夠在運行時構造
2. 能夠當作參數傳遞
3. 能夠被當作返回值返回
4. 能夠賦值給變量
即可以被成爲first-class object。
例如:
- (define (my-cons x y)
- (lambda (f)
- (f x y)
- )
- )
- (define (my-car lst)
- (lst
- (lambda (x y) x)
- )
- )
- (define (my-cdr lst)
- (lst
- (lambda (x y) y)
- )
- )
對其的使用以下:
- > (define pair1 (my-cons 10 11))
- > pair1
- #<procedure>
- > (my-car pair1)
- 10
- > (my-cdr pair1)
- 11
根據上述規則,很顯然,C/C++的函數就不是一等函數,由於他們不知足第一個條件。在函數式編程中,使用另外一個函數做爲參數,或者返回一個函數,或者兩者兼有的函數稱爲高階函數(High-order function)。既然說到高階函數,就不能不說詞法閉包(Lexical Closure,或者簡稱爲閉包closure)。閉包指的是函數自己以及其自由變量(或非本地變量)的引用環境一塊兒構成的結構,其容許函數訪問處於其詞法做用域(lexical scope)以外的變量,例如:
- (define closure-demo
- (let ((y 5))
- (lambda (x)
- (set! y (+ y x))
- y)
- )
- )
這裏須要注意閉包與匿名函數的區別。
第三個基礎概念即是遞歸。其實對於遞歸沒有太多可說的,但必定要注意的是尾遞歸(tail-recursion)。尾遞歸使得用遞歸的形式實現遞推成爲可能。
第四個是詞法做用域。
第五個是lambda算子(lambda calculus)
第六個是塊結構
第七個是一級續延(first-class continuation)
第八個是宏(衛生宏:展開時可以保證不使用意外的標示符)
其中,有些基本概念又能引伸出一些新的概念。後面這些基本概念(4~8),留到之後討論。
另外,在
這裏能夠找到一些業界比較承認的Lisp應用。至於Common Lisp的應用,Paul Graham的Viaweb(後來被Yahoo!收購,成爲Yahoo! Store)是個好例子。最著名的估計是
這個,詳情能夠參考
田春冰河的博客。
線性遞歸以及循環不變式 css
例1:計算x^n(X的n次方)
能夠採用以下算式來計算:
x^0 = 1
x^n = x*x^(n-1) = x*x*x^(n-2) = ……
那麼,很容易獲得該計算過程的遞歸表示:
(define (exp x n)
- (if (= n 0)
- 1
- (* x (exp x (- n 1)))))
很容易看出來,這個計算的時間和空間複雜度均爲O(n)。這即是一個線性遞歸。爲了減小其空間複雜度,可使用線性迭代來代替(使用遞歸實現):html
(define (exp-iter x n)
- (define (iter x n r)
- (if (= n 0)
- r
- (iter x (- n 1) (* r x))))
- (iter x n 1))
計算過程依然有改進空間,那即是能夠下降時間複雜度。根據node
x^n = x^(n/2)*x^(n/2)程序員
可知,計算x^n的時間複雜度能夠下降爲O(logn)。此時,須要一個循環不變式來保證計算結果的正確性。設r初始值爲1,則在計算過程當中,從一個狀態遷移到另外一個狀態(n爲奇數遷移到n爲偶數)時,r*x^n始終保持不變。而此時計算方法爲:web
n爲奇數,則x^n = x*x^(n-1)express
n爲偶數,則x^n = x^(n/2)*x^(n/2) = (x^2)^(n/2)編程
所以,計算過程以下:canvas
(define (fast-exp-iter x n)
- (define (iter x n r)
- (cond ((= n 0) r)
- ((even? n) (iter (* x x) (/ n 2) r))
- (else (iter x (- n 1) (* r x)))))
- (iter x n 1))
例2:a*n能夠寫成a+a*(n-1)的形式。那麼採用加法和遞歸來計算則是:小程序
(define (mul x n)
- (if (= n 0)
- 0
- (+ x (mul x (- n 1)))))
一樣,能夠採用迭代的方式來計算:安全
(define (mul-iter x n)
- (define (iter x n r)
- (if (= n 0)
- 0
- (iter x (- n 1) (+ r x))))
- (iter x n 0))
與例1類似,也能夠將迭代計算的時間複雜度降爲O(logn):
- (define (fast-mul-iter x n)
- (define (iter x n r)
- (cond ((= n 0) r)
- ((even? n) (iter (+ x x) (/ n 2) r))
- (else (iter x (- n 1) (+ r x)))))
- (iter x n 0))
這個計算過程的循環不變式是什麼呢?
在「
學習Scheme」中提到了Scheme,或者說是函數式編程的一些基本概念,這些概念使得Scheme區別於其餘的編程語言,也使得函數式編程FP區別於其餘的編程範式。以前用了四篇博文詳細講述了遞歸以及尾遞歸,並給出了許多實際的範例。尤爲是「
[原]Scheme線性遞歸、線性迭代示例以及循環不變式」,詳細講述瞭如何設計並實現尾遞歸。下面,來看看第三個概念:閉包
「在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這些被引用的自由變量將和這個函數一同存在,即便已經離開了創造它們的環境也不例外。因此,有另外一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。」這是維基百科給出的說明。
Paul Graham在On Lisp一書中對於閉包的定義則爲:函數與一系列變量綁定的組合便是閉包。其實這裏也隱含了一個計算環境的問題,那就是函數定義的計算環境。
Closure的示例以下:
- (define closure-demo
- (let ((y 5))
- (lambda (x)
- (set! y (+ y x))
- y)
- )
- )
這裏使用了set!,所以其封裝了一個狀態,即自由變量y:
- > (closure-demo 5)
- 10
- > (closure-demo 5)
- 15
- > (closure-demo 5)
- 20
「閉包能夠用來在一個函數與一組「私有」變量之間創建關聯關係。在給定函數被屢次調用的過程當中,這些私有變量可以保持其持久性。變量的做用域僅限於包含它們的函數,所以沒法從其它程序代碼部分進行訪問。不過,變量的生存期是能夠很長,在一次函數調用期間所創建所生成的值在下次函數調用時仍然存在。正由於這一特色,閉包能夠用來完成信息隱藏,並進而應用於須要狀態表達的某些編程範型中。」
看到這裏,咱們立刻就能想到一個概念:面向對象。根據對「對象」的經典定義——對象有狀態、行爲以及標識;對象的行爲和結構在通用的類中定義——
能夠獲得,若是使用閉包,很輕鬆即可以定義一個類。另外,因爲向對象發消息須要一個實例,一些參數,並獲得發送消息以後的結果,所以,使用一個dispatcher即可以向對象發送消息。例如:
- (define (make-point-2D x y)
- (define (get-x) x)
- (define (get-y) y)
- (define (set-x! new-x) (set! x new-x))
- (define (set-y! new-y) (set! y new-y))
- (lambda (selector . args) ; a dispatcher
- (case selector
- ((get-x) (apply get-x args))
- ((get-y) (apply get-y args))
- ((set-x! (apply set-x! args))
- ((set-y! (apply set-x! args))
- (else (error "don't understand " selector)))))
在這裏,make-point-2D是一個函數,它接受兩個參數,並返回一個閉包——由lambda定義的一個匿名函數。這個閉包中,引用的自由變量有:get-x,get-y,set-x!, set-y!。這些變量實際上是函數,由於函數是一等公民,所以能夠用變量將其進行傳遞。這就是一個基本的2D point類。該類的使用以下:
- > (define p1 (make-point-2D 10 20))
- > (p1 'get-x)
- 10
- > (p1 'get-y)
- 20
- > (p1 'set-x! 5)
- > (p1 'set- 10)
- > (list (p1 'get-x) (p1 'get-y))
注意,這些自由變量本身自己又是函數,有本身的計算環境,而它們所訪問的變量也是自由變量,所以它們也是閉包,它們的計算環境由lambda定義的匿名函數提供——lambda定義的dispatcher是個大閉包,get-*和set-*都是這個閉包裏的閉包。
利用閉包,還能夠實現繼承,如:
- (define (make-point-3D x y z) ; that is, point-3D _inherits_ from point-2D
- (let ((parent (make-point-2D x y)))
- (define (get-z) z)
- (define (set-z! new-z) (set! z new-z))
- (lambda (selector . args)
- (case selector
- ((get-z) (apply get-z args))
- ((set- (apply set- args)) ; delegate everything else to the parent
- (else (apply parent (cons selector args)))))))
這裏面除了make-point-2D的閉包以外,還增長了get-z、set-z!以及lambda定義的匿名函數三個閉包。
在此基礎上,利用宏對Scheme進行擴展,即可以獲得一個通用的面向對象編程範式框架。固然,不能像在這裏同樣使用quote的串來肯定應該調用哪一個函數。
這裏有個帖子討論爲何Scheme不提供內置OO系統。我贊成Abhijat的觀點。OO主要目的是封裝、模塊化、大規模編程、狀態,區分了數據和操做。Scheme不區分數據和函數,強調無狀態,且函數爲一等公民,所以並不須要OO。但實踐中很難作到無狀態,所以爲了保持最小原則,OO由各實現自行添加。
難學卻重要的Scheme特性
在Scheme中,對宏的處理與C語言相似,也分爲兩步:第一步是宏展開,第二步則是編譯展開以後的代碼。這樣,經過宏和基本的語言構造,能夠對Scheme語言進行擴展——C語言的宏則不具有擴展語言的能力。
Racket對宏的定義以下:
A macro is a syntactic form with an associated transformer that expands the original form into existing forms.
翻譯過來就是說:宏是帶有關聯轉換器的語法形式,該關聯轉換器將原先的形式展開成已有的形式(嫌我翻譯得很差的儘管拍磚)。若是和Racket結合到一塊兒說,應該是:宏是Racket編譯器的一個擴展。
在許多Lisp方言中(固然包括Scheme),宏是基於模式的。這樣,宏將匹配某個模式的代碼展開爲原先語法中所對應的模式。define-syntax和syntax-rules用於定義一個宏,例如,在Scheme中只提供if來執行分支:
(if pred expr1 expr2),對應的命題表達式爲:(pred->expr1, true->expr2)。若是if分支中須要對多個表達式求值,那就須要使用begin,所以能夠編寫以下的宏when來知足需求:
- (define-syntax when
- (syntax-rules ()
- ((when pred exp exps ...)
- (if pred (begin exp exps ...)))))
其中,body裏的when一般使用「_」代替。每次使用when時,就會被展開爲對if的使用。
宏是Scheme的一個很是強大的功能,網上有不少專門針對Scheme宏編程的資源,有興趣的能夠搜索一下。
參考:
- 維基百科
- Racket文檔
- Schemers.org
圖形界面的小應用
在學習了一些Scheme基礎以後,我寫了一個小程序,其功能以下:
- 一個菜單欄上有兩個菜單:File和Help
- File菜單包含Start和Stop兩個菜單項
- Help包含About菜單項
- 點擊Start,程序將畫出三個連在一塊兒的空心小矩形,而後這三個小矩形同時向右移動
- 點擊Stop,中止移動
好吧,我認可,這就是個貪食蛇的雛形。記得當年學習C#時也寫了個最基本的貪食蛇遊戲,如今算是二進宮了,輕車熟路。
在開始以前,須要先大體說明一下Racket的對象系統。
定義一個類:
- (class superclass-expr decl-or-expr ...)
例如:
- (class object%
- (init size) ; initialization argument
-
- (define current-size size) ; field
-
- (super-new) ; superclass initialization
-
- (define/public (get-size)
- current-size)
-
- (define/public (grow amt)
- (set! current-size (+ amt current-size)))
-
- (define/public (eat other-fish)
- (grow (send other-fish get-size))))
這是一個匿名類,其基類爲object%,初始化參數爲size——相似於C++的初始化列表,接下來current-size指的是一個私有成員,其初始值由初始化參數size所指定。再以後是經過(super-new)對父類即object%類調用「構造函數」。以後是三個公有的成員函數。
爲了可以建立這個類對象而不須要每次都把上面這一大段寫到代碼裏,能夠用define把這個匿名類綁定到一個變量上,好比叫作fish%。那麼須要建立一個fish%的對象就很簡單:
須要注意的是,在Racket(也許其餘的Scheme實現也同樣)中,「{}」、「()」、「[]」是相同的,只不過必須匹配,如「{」必須匹配「}」。
爲了調用一個類的函數,須要用如下兩種形式之一:
- (send obj-expr method-id arg ...)
- (send obj-expr method-id arg ... . arg-list-expr)
如:
- (send (new fish% (size 10)) get-size)
看到這裏你也許會感到很奇怪:爲何沒有析構函數?早在Lisp誕生初期,它就包含了垃圾收集功能,所以,根本不須要你釋放new獲得的對象。過了許多年以後,許多包含垃圾收集功能的語言誕生了。
此外,結構體也是頗有用的東西,它與類的區別,跟C++中類與結構體的區別差很少,但Racket結構體提供了不少輔助函數——固然是經過宏和閉包來提供這些函數。結構體是經過struct來定義的。——沒猜錯的話,struct應該也是一個宏——尚未細看Racket的代碼。
- (struct node (x y) #:mutable)
其使用以下所示:
- (node-x n) ; get x from a node n
- (set-node- n 10) ; set x to 10 of a node n
- (node? n) ; predicate, check if n is a node
還有其餘的輔助函數,在此不一一列舉。
這個應用的核心在於內嵌在canvas上的一個定時器:
- (define timer
- (new timer%
- [notify-callback
- (lambda ()
- (let ((dc (send this get-dc)))
- (send dc clear)
- (map (lambda (n)
- (send dc
- draw-rectangle (node-x n) (node-y n) 5 5))
- lst)
- (map (lambda (n)
- (set-node-x! n (+ (node-x n) 5)))
- lst)))
- ]
- [just-once? #f]))
每當超時時間發生時,notify-callback所綁定的回調函數就會被調用,完成在canvas上畫圖的功能,同時更新圖形所在的位置,這樣便造成了移動。
固然,如今這個程序還只是雛形而已,總代碼量爲101行。若是要完善成爲一個貪食蛇遊戲,還須要作不少工做,同時還須要進行一些設計,至少將Model、View和Controller分開吧。
從這裏也能夠看出,用Scheme來進行面向對象的開發也十分容易,並不須要用到Scheme的高級功能例如宏和續延等等。固然,若是能運用好這些高級功能,相信代碼會更加簡單。
續延的例子
上一篇經過一些例子講述瞭如何來理解continuation,這一篇講主要講述如何理解著名的Continuation Passing Style,即CPS。
在TSPL的第三章「
Continuation Passing Style」裏,Kent Dybvig在對Continuation總結的基礎上,引出了CPS的概念。由於Continuation是某個計算完成以後,要繼續進行的計算,那麼,對於每個函數調用,都隱含了一個Continuation即:函數調用返回後,要繼續進行的計算——或者是返回函數的返回值,或者是更進一步的計算。Kent在書中寫道:
「In particular, a continuation is associated with each procedure call. When one procedure invokes another via a nontail call, the called procedure receives an implicit continuation that is responsible for completing what is left of the calling procedure's body plus returning to the calling procedure's continuation. If the call is a tail call, the called procedure simply receives the continuation of the calling procedure.」
也就是說,函數調用是都被隱式地傳遞了一個Continuation。若是函數調用不是尾部調用,那麼該隱含的continuation將使用函數調用的結果來進行後續計算;若是是一個尾部調用,那麼該隱含的continuation就是調用方調用該函數後的continuation。例如:
對函數「-」的調用顯然不是尾部調用,所以,該調用的continuation即是對該調用的返回值進行除以10的操做。
那麼,什麼叫作CPS——Continuation Passing Style呢?CPS就是指將隱式傳遞給(關聯於)某個函數調用的continuation顯式地傳遞給這個函數。對於上面的例子,若是咱們將「-」函數改寫成現實傳遞continuation的版本,那就是:
- (define (my-minus x k) (k (- x 3)))
其中,參數k就是顯式傳遞給函數的continuation。爲了完成上述除以10的計算,對my-minus的調用就應該寫成(假設x值爲15):
- (my-minus 10 (lambda (v) (/ v 10)))
這裏的匿名函數就是那個k。Kent還寫道:
「CPS allows a procedure to pass more than one result to its continuation, because the procedure that implements the continuation can take any number of arguments.」
也就是說,CPS使得一個函數能夠傳遞多個計算結果給其continuation,由於實現continuation的函數能夠有任意數量的參數——固然,這也能夠用values函數來實現。另外,CPS容許向一個函數傳遞多個continuation,這樣就能夠根據不一樣的狀況來進行不一樣的後續計算。也就是說,經過CPS,咱們能夠對一個函數的執行過程進行控制(flow control)。
爲了加深一下印象,讓咱們來看看TSPL上的例子:將下面這段代碼用CPS改寫。
- (letrec ([f (lambda (x) (cons 'a x))]
- [g (lambda (x) (cons 'b (f x)))]
- [h (lambda (x) (g (cons 'c x)))])
- (cons 'd (h '())))
(關於letrec,能夠參考
這裏)。首先,咱們來改寫f。由於f使用尾部調用方式調用cons,其後續計算是基於cons的返回結果的, 所以, 對於f能夠改寫爲:
- [f (lambda (x k) (k (cons 'a x)))]
再來看g函數。因爲g函數以非尾部調用的方式調用了f,所以,g傳遞給f的continuation就不是簡單地返回一個值,而是須要進行必定的操做:
- [g (lambda (x k) (f x
- (lambda (v)
- (k (cons 'b v)))))]
須要注意的是,這裏g的含義是:以x和一個continuation調用f,將所得的結果進行continuation指定的計算,並在該計算的結果上應用k。
最後,h函數經過尾部調用的方式調用g,所以,對h調用的continuation就是對g調用的continuation。那麼,h能夠改寫爲:
- [h (lambda (x k)
- (g (cons 'c x) k))]
最後,將這些組合到一塊兒:
- (letrec ([f (lambda (x k) (k (cons 'a x)))]
- [g (lambda (x k) (f x (lambda (v) (k (cons 'b v)))))]
- [h (lambda (x k) (g (cons 'c x) k))])
- (h '() (lambda (v) (cons 'd v))))
通俗一點說來,continuation就像C語言裏的long_jump()函數,而CPS則相似於UNIX裏的管道:將一些值經過管道傳遞給下一個處理——只不過CPS的管道是函數級別而非進程級別的。這個觀點你們讓它爛在內心就行了,不然,若是某天你在宣揚這個觀點的時候,不當心碰上一個(自誇的)Scheme高手,他必定會勃然大怒:Scheme爲何要跟C比較?Scheme和C的理念徹底不同!因此,低調,再低調。
理論上,全部使用了call/cc的函數,均可以使用CPS來重寫,但Kent也認可,這個難度很大,並且有時候要修改Scheme所提供的基礎函數(primitives)。不過,仍是讓咱們來看看幾個將使用call/cc的函數用CPS改寫的例子。
- (define product
- (lambda (ls)
- (call/cc
- (lambda (break)
- (let f ([ls ls])
- (cond
- [(null? ls) 1]
- [(= (car ls) 0) (break 0)]
- [else (* (car ls) (f (cdr ls)))]))))))
首先,將call/cc的調用從函數體中除去,而後,爲product函數加上一個參數k,該參數接受一個參數。另外,由於product增長了一個參數,所以對f這個
命名let也須要增長一個參數。最後,在f的body裏面調用f,也須要改寫成CPS形式。由於對f的調用不是尾部調用,所以在f返回以前,須要進行計算,而後纔是對該結果進行下一步的計算。此時須要的後續計算爲:
- (lambda (v) (k (* (car ls) v)))
對於cond的每一個分支,都須要對其結果進行後續的k計算,這樣,就獲得告終果:
- (define product/k
- (lambda (ls k)
- (let f ([ls ls] [k k])
- (cond [(null? ls) (k 1)]
- [(= (car ls) 0) (k "error")]
- [else (f (cdr ls)
- (lambda (x)
- (k (* (car ls) x))))]))))
須要注意的是,因爲product/k是個遞歸過程,對於每一個返回的值,都會有後續操做,所以須要對cond表達式的每一個返回值應用continuation。
習題3.4.1是要求用兩個continuation來改寫reciprocal函數,以下:
- (define reciprocal
- (lambda (x ok error)
- (if (= x 0)
- (error)
- (ok (/ 1 x)))))
- (define ok
- (lambda (x)
- (display "ok ")
- x))
- (define error
- (lambda ()
- (display "error")
- (newline)))
- (reciprocal 0 ok error)
- (reciprocal 10 ok error)
習題3.4.2要求用CPS改寫
這裏的retry。
- (define retry #f)
- (define factorial
- (lambda (x)
- (if (= x 0)
- (call/cc (lambda (k) (set! retry k) 1))
- (* x (factorial (- x 1))))))
一樣,須要將factorial改寫成接受兩個參數的函數,第二個參數爲continuation。接下來,把對call/cc的調用去掉,改寫成對k的使用。而後,根據對factorial遞歸調用的非尾部性,肯定如何調用新的函數。結果以下:
- (define factorial/k
- (lambda (x k)
- (if (= x 0)
- (begin
- ( retry/k k)
- (k 1))
- (factorial/k
- (- x 1)
- (lambda (v)
- (k (* x v)))))))
- (factorial/k 4 (lambda (x) x))
- (retry/k 2)
- (retry/k 3)
習題3.4.3要求用CPS改寫下面的函數:
- (define reciprocals
- (lambda (ls)
- (call/cc
- (lambda (k)
- (map (lambda (x)
- (if (= x 0)
- (k "zero found")
- (/ 1 x)))
- ls)))))
這道題難度很大,所以Kent給出了提示:須要修改map函數爲接受continuation做爲額外的參數的形式。——至於緣由,我也說不清楚。
首先,本身實現一個非CPS版本的map函數map1:
- (define map1
- (lambda (p ls)
- (if (null? ls)
- '()
- (cons (p (car ls))
- (map1 p (cdr ls))))))
這裏,當ls爲空時,須要馬上對返回結果'()進行後續計算。而非空時,經過map1調用自身,並對結果進行後續計算。那這時就應該着重考慮這段代碼:
- (cons (p (car ls))
- (map1 p (cdr ls)))
根據對函數參數求值的順序,有兩種順序來進行這段代碼的計算。
首先,它計算出(p (car ls))獲得v1,其後續計算爲(map1 p (cdr ls))獲得v2,然後者的後續計算爲(cons v1 v2)並返回該結果。那麼,計算並獲得v1及其後續計算以下:
- (p (car ls)
- (lambda (v1)
- (map2/k p (cdr ls) k1)))
隨即進行後續的k1計算,而k1爲對v1和v2的後續計算:
- (lambda (v2)
- (k (cons v1 v2)))
將這兩個計算合併起來:
- (define (map2/k p ls k)
- (if (null? ls)
- (k '())
- (p (car ls)
- (lambda (v1)
- (map2/k p (cdr ls)
- (lambda (v2)
- (k (cons v1 v2))))))))
首先,它計算出(map1 p (cdr ls))獲得結果v2,其後續計算爲(p (car ls))獲得v1,然後者的後續計算爲(cons v1 v2)並返回結果。那麼,計算獲得v2及其後續計算爲:
- (map1/k p (cdr ls)
- (lambda (v2)
- (p (car ls) k1)))
隨後進行對v2和v1的計算,即:
- (lambda (v1)
- (k (cons v1 v2)))
最後將這兩個計算合併起來:
- (define map1/k
- (lambda (p ls k)
- (if (null? ls)
- (k '())
- (map1/k p (cdr ls)
- (lambda (v2)
- (p (car ls)
- (lambda (v1)
- (k (cons v1 v2)))))))))
有了CPS的map函數以後,寫出reciprocal的CPS形式就很簡單了:
- (define reciprocal1/k
- (lambda (ls k)
- (map1/k (lambda (x c)
- (if (= x 0)
- (k "zero")
- (c (/ 1 x))))
- ls
- k)))
其中,k是整個reciprocal1/k計算完成後的continuation,所以用於返回錯誤;而c則是計算完(/ 1 x)的continuation,只不過在這裏也是k而已。另外,不管是用map1/k,仍是map2/k,其結果應該是同樣的。
總結一下,當使用CPS來取代call/cc或者使用CPS時,若是函數中有對含有CPS的函數的調用,那麼,傳遞進去的continuation或者做爲函數,應用到傳遞來的參數上(非尾部調用);或者做爲一個返回值(尾部調用);若是沒有調用含有CPS的函數,則將其應用到返回值上。
楊輝(Pascal)三角
一個楊輝三角以下所示:
爲了計算某個位置上的值:
- (define pascal-triangle
- (lambda (row col)
- (cond ([or (= row 0) (= col 0)] 0)
- ([= row col] 1)
- (else (+ (pascal-triangle (- row 1) (- col 1))
- (pascal-triangle (- row 1) col))))))
沒錯,這是個樹形遞歸,會佔用較大的空間。那麼,來考慮一下通用的狀況:
f(0,0) = 0
f(0,1) = 0
f(1,1) = f(0,1)+f(0,0)
f(2,1) = f(1,1)+f(1,0)
f(2,2) = f(1,2)+f(1,1)
f(3,1) = f(2,1)+f(2,0)
f(3,2) = f(2,2)+f(2,1)
...
f(m-1,n-1) = f(m-2,n-1)+f(m-2,n-2)
f(m-1,n) = f(m-2,n)+f(m-2,n-1)
f(m,n) = f(m-1,n)+f(m-1,n-1)
f(m+1,n) = f(m,n)+f(m,n-1)
能夠看出,每一次計算下一個值的時候,都沒法徹底使用上一步計算的結果,因此到目前爲止我尚未找到一種使用尾遞歸的方式來改寫這個函數。若是哪位同窗可以用尾遞歸方式解出來,請及時通知我。
爲了打印出楊輝三角,須要用兩個循環變量來控制行和列的循環。每次增長一行,就須要對該行的每一列進行輸出,知道行、列值相等。以下:
- (define p-t
- (lambda (n)
- (let iter ([i 1] [j 1])
- (when (< i (+ n 1))
- (display (pascal-triangle i j)) (display " ")
- (if (= i j)
- (begin (newline) (iter (+ i 1) 1))
- (iter i (+ 1 j)))))))
此處i爲行號,j爲列號。(p-t 8)結果以下:
- 1
- 1 1
- 1 2 1
- 1 3 3 1
- 1 4 6 4 1
- 1 5 10 10 5 1
- 1 6 15 20 15 6 1
- 1 7 21 35 35 21 7 1
再次考慮是否能使用尾遞歸:
因爲Scheme提供do來完成循環,且能夠利用尾遞歸——其實,使用do編寫尾遞歸的關鍵因素是找到循環不變式,但目前我沒有找到:使用do來考慮上面的結果,若是要計算出第7行第3列的15,須要保存上一步的兩個計算結果5和10,而爲了獲得5,又須要保存其上一步的結果1和4,爲了獲得10,又須要保存其上一步的結果6和4,此時需保存的結果變爲3個。考慮第8行第4列的35,最多的時候須要保存第5行的全部5個結果。因爲每一步保存結果個數不同,所以這種方式的尾遞歸行不通。
-
通過了三個月左右的集中學習(intensive learning),終於可使用Scheme作一些簡單的工做了,並且,也可以依葫蘆畫瓢作一些複雜點的工做了——然而,用Scheme語言編程,其重點是如何找到解決問題的方法,而不是如何去實現這個解決方法,由於Scheme提供了很強的表達能力,將程序員們從語言的細節以及語法糖蜜中解放出來,使得他們可以更專一於問題自己,而不是實現自己。
回想起本身接觸、學習函數式變成和Scheme的通過,其中充滿了曲折和坎坷。Scheme語言自己的簡單性致使了其靈活性,使得一我的能夠在幾天以內學完基本語法,但要使用好Scheme,須要長時間的訓練。另外,對於初學者來講一個難點就是Lisp方言太多,而每一個方言的各類實現也不盡相同,這就致使了在真正開始學習以前須要選擇一個合適的Scheme實現。
在2008年年末的時候,由於跟cuigang討論一個C++的問題,開始知道函數式編程範式,因而買了本《計算機程序的構造與解釋》(SICP)開始了Scheme之旅。然而,一方面函數式語言確實不符合本身一向的編程習慣,另外一方面這本書更注重數學方面,所以,開始的學習歷程很艱苦,不但沒法熟練使用尾遞歸,加之工做負載確實不小,因而便放棄了。
那是在將近三年之後了。在2011年年中,由於公司戰略調整,手裏基本上沒有什麼工做了。在某天整理書架時發現了SICP書,因而又開始學習了。這裏必須認可,我看書的習慣確實很差,由於不能先瀏覽幾遍,再開始精讀。入門的艱苦致使了又產生了放棄的念頭,此時無心之中發現了《
Simply Scheme》,號稱SICP的基礎。這本書確實不算太難,話了很大的篇幅來說述遞歸和尾遞歸,並提供了大量的基礎練習。經過結合「循環不變式」的知識,並瀏覽了一些Scheme的語言構造以後,終於可以用尾遞歸來解決問題了。那段時間爲了理解尾遞歸併解答相關的習題,常常在快睡着時忽然有了思路,因而起來上機調試。在Simply Scheme系列的(
2)、(
3)、(
4)中能夠看到這些習題。在學習了do、loop和命名let以後,忽然間好像醍醐灌頂,便有了這一篇:
[原]從新開始學習Scheme(2):線性遞歸以及循環不變式。根據循環不變式,咱們就能夠很輕鬆地用尾遞歸來解決這兩個問題:
1. 當n<3時,f(n)=n;不然,f(n) = f(n-1)+2f(n-2)+3f(n-3)。代碼以下:
- (define (f-i n)
- (let iter ((fn 4) (fn-1 2) (fn-2 1) (fn-3 0) (i n))
- (cond ((< n 3) n)
- ((= i 3) fn)
- (else
- (iter (+ fn (* 2 fn-1) (* 3 fn-2))
- fn
- fn-1
- fn-2
- (- i 1))))))
2. 求解1!+2!+3!+...+n!。代碼以下:
- (define (fac-sum n)
- (let iter ((fi-1 1) (c 1) (r 0))
- (if (= c (+ n 1)) r
- (iter (* fi-1 c) (+ c 1) (+ r (* fi-1 c))))))
快樂的日子老是短暫的。尚未完成Simply Scheme的一半,工做強度又大了起來,因而,Scheme的學習又放下了,直到工做中切切實實須要用到Scheme。正如我在「
[原]從新開始學習Scheme(1)」中所說的,出於興趣和工做須要來學習某項技能,其過程和結果都是不同的,各有長短吧。若是不是項目須要,我也不可能在這麼短的時間內如此高密度地學習一項技能;但正由於項目須要,不可能花大量的時間在本身的興趣點上,這樣就致使了許多問題遺留下來。所以,雖然在「從新開始學習Scheme」系列裏涵蓋了Scheme的幾個重要特性,但好像除了尾遞歸,其餘的特性我都只是摸到,甚至只是剛看到門檻而已。幸虧工做中使用這些特性的機會很少,所以仍是能夠自誇爲Scheme工程師。Scheme程序員?按照Lisp社區的說法,必需要可以寫一個Lisp解釋器,才能自稱爲Lisp程序員。這話一樣適合於Scheme。
在這樣一家公司,因爲戰略調整和重組是很是頻繁的事情,如今雖然開始作Scheme相關的工做,但恐怕過不了多久又會被安排去作其餘的東西,那後續的Scheme學習就會成爲鏡花水月——希望不要再在我身上發生這樣的事情了。
《 實用Common Lisp》:雖然是CL方言,但可讓讀者對FP和Lisp有個大概的認識
《
On Lisp》:高質量的一本書,裏面一些重要章節一樣適用於Scheme和CL,例如關於continuation的說明。
還有不少其餘的書,這裏不一一列舉。
"Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I":關於FP和Lisp的開山之做
"Why Functional Programming"
雖然只是剛剛具有了Scheme的基礎,但系統學習這一階段確實能夠結束了,畢竟,項目就在那裏,公司也不可能永遠讓員工處於學習狀態,只向員工投入資金而不向員工要產出的公司只能出如今夢裏。所以,之後基本不會有大量時間來集中學習Scheme了。我想,是時候總結一下了。——之後隨用隨學,一次一個小知識點。
=========================== End