開始學習Scheme

開始學習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。
例如:
  1. (define (my-cons x y)
  2.   (lambda (f)
  3.     (f x y)
  4.     )
  5.   )
  6. (define (my-car lst)
  7.   (lst
  8.    (lambda (x y) x)
  9.    )
  10.   )
  11. (define (my-cdr lst)
  12.   (lst
  13.    (lambda (x y) y)
  14.    )
  15.   )
對其的使用以下:
  1. > (define pair1 (my-cons 10 11))
  2. > pair1
  3. #<procedure>
  4. > (my-car pair1)
  5. 10
  6. > (my-cdr pair1)
  7. 11
根據上述規則,很顯然,C/C++的函數就不是一等函數,由於他們不知足第一個條件。在函數式編程中,使用另外一個函數做爲參數,或者返回一個函數,或者兩者兼有的函數稱爲高階函數(High-order function)。既然說到高階函數,就不能不說詞法閉包(Lexical Closure,或者簡稱爲閉包closure)。閉包指的是函數自己以及其自由變量(或非本地變量)的引用環境一塊兒構成的結構,其容許函數訪問處於其詞法做用域(lexical scope)以外的變量,例如:
  1. (define closure-demo
  2.   (let ((y 5))
  3.     (lambda (x)
  4.       (set! y (+ y x))
  5.       y)
  6.     )
  7.   )
這裏須要注意閉包與匿名函數的區別。
 
第三個基礎概念即是遞歸。其實對於遞歸沒有太多可說的,但必定要注意的是尾遞歸(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)
  1. (if (= n 0)
  2.     1
  3.     (* x (exp x (- n 1)))))

很容易看出來,這個計算的時間和空間複雜度均爲O(n)。這即是一個線性遞歸。爲了減小其空間複雜度,可使用線性迭代來代替(使用遞歸實現):html

      (define (exp-iter x n)

  1.   (define (iter x n r)
  2.     (if (= n 0)
  3.         r
  4.         (iter x (- n 1) (* r x))))
  5.   (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)

  1.   (define (iter x n r)
  2.     (cond ((= n 0) r)
  3.           ((even? n) (iter (* x x) (/ n 2) r))
  4.           (else (iter x (- n 1) (* r x)))))
  5.   (iter x n 1))

例2:a*n能夠寫成a+a*(n-1)的形式。那麼採用加法和遞歸來計算則是:小程序

      (define (mul x n)

  1.   (if (= n 0)
  2.       0
  3.       (+ x (mul x (- n 1)))))

一樣,能夠採用迭代的方式來計算:安全

      (define (mul-iter x n)

  1.   (define (iter x n r)
  2.     (if (= n 0)
  3.         0
  4.         (iter x (- n 1) (+ r x))))
  5.   (iter x n 0))

與例1類似,也能夠將迭代計算的時間複雜度降爲O(logn):

  1. (define (fast-mul-iter x n)
  2.   (define (iter x n r)
  3.     (cond ((= n 0) r)
  4.           ((even? n) (iter (+ x x) (/ n 2) r))
  5.           (else (iter x (- n 1) (+ r x)))))
  6.   (iter x n 0))
這個計算過程的循環不變式是什麼呢?
 
 
 
在「 學習Scheme」中提到了Scheme,或者說是函數式編程的一些基本概念,這些概念使得Scheme區別於其餘的編程語言,也使得函數式編程FP區別於其餘的編程範式。以前用了四篇博文詳細講述了遞歸以及尾遞歸,並給出了許多實際的範例。尤爲是「 [原]Scheme線性遞歸、線性迭代示例以及循環不變式」,詳細講述瞭如何設計並實現尾遞歸。下面,來看看第三個概念:閉包
 
「在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這些被引用的自由變量將和這個函數一同存在,即便已經離開了創造它們的環境也不例外。因此,有另外一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。」這是維基百科給出的說明。
 
Paul Graham在On Lisp一書中對於閉包的定義則爲:函數與一系列變量綁定的組合便是閉包。其實這裏也隱含了一個計算環境的問題,那就是函數定義的計算環境。
 
Closure的示例以下:
  1. (define closure-demo
  2.   (let ((y 5))
  3.     (lambda (x)
  4.       (set! y (+ y x))
  5.       y)
  6.     )
  7.   )
這裏使用了set!,所以其封裝了一個狀態,即自由變量y:
  1. > (closure-demo 5)
  2. 10
  3. > (closure-demo 5)
  4. 15
  5. > (closure-demo 5)
  6. 20
「閉包能夠用來在一個函數與一組「私有」變量之間創建關聯關係。在給定函數被屢次調用的過程當中,這些私有變量可以保持其持久性。變量的做用域僅限於包含它們的函數,所以沒法從其它程序代碼部分進行訪問。不過,變量的生存期是能夠很長,在一次函數調用期間所創建所生成的值在下次函數調用時仍然存在。正由於這一特色,閉包能夠用來完成信息隱藏,並進而應用於須要狀態表達的某些編程範型中。」
 
看到這裏,咱們立刻就能想到一個概念:面向對象。根據對「對象」的經典定義——對象有狀態、行爲以及標識;對象的行爲和結構在通用的類中定義—— 能夠獲得,若是使用閉包,很輕鬆即可以定義一個類。另外,因爲向對象發消息須要一個實例,一些參數,並獲得發送消息以後的結果,所以,使用一個dispatcher即可以向對象發送消息。例如:
  1. (define (make-point-2D x y)
  2.   (define (get-x) x)
  3.   (define (get-y) y)
  4.   (define (set-x! new-x) (set! x new-x))
  5.   (define (set-y! new-y) (set! y new-y))
  6.   (lambda (selector . args) ; a dispatcher
  7.       (case selector
  8.         ((get-x) (apply get-x args))
  9.         ((get-y) (apply get-y args))
  10.         ((set-x! (apply set-x! args))
  11.         ((set-y! (apply set-x! args))
  12.         (else (error "don't understand " selector)))))
在這裏,make-point-2D是一個函數,它接受兩個參數,並返回一個閉包——由lambda定義的一個匿名函數。這個閉包中,引用的自由變量有:get-x,get-y,set-x!, set-y!。這些變量實際上是函數,由於函數是一等公民,所以能夠用變量將其進行傳遞。這就是一個基本的2D point類。該類的使用以下:
  1. > (define p1 (make-point-2D 10 20))
  2. > (p1 'get-x)
  3. 10
  4. > (p1 'get-y)
  5. 20
  6. > (p1 'set-x! 5)
  7. > (p1 'set- 10)
  8. > (list (p1 'get-x) (p1 'get-y))
注意,這些自由變量本身自己又是函數,有本身的計算環境,而它們所訪問的變量也是自由變量,所以它們也是閉包,它們的計算環境由lambda定義的匿名函數提供——lambda定義的dispatcher是個大閉包,get-*和set-*都是這個閉包裏的閉包。
 
利用閉包,還能夠實現繼承,如:
  1. (define (make-point-3D x y z) ; that is, point-3D _inherits_ from point-2D
  2.   (let ((parent (make-point-2D x y)))
  3.     (define (get-z) z)
  4.     (define (set-z! new-z) (set! z new-z))
  5.     (lambda (selector . args)
  6.       (case selector
  7.         ((get-z) (apply get-z args))
  8.         ((set- (apply set- args)) ; delegate everything else to the parent
  9.         (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"裏的first class continuation。關於continuation這個單詞的翻譯,有那麼幾種,有「延續」、「繼續」、「續延」。我的認爲,「續延」應當算是最好的了。這個翻譯是由裘宗燕老師提出來的。
 
Continuation是由Scheme引入函數式編程中的。這個概念至關重要,可是理解起來卻比較困難——也許對於我來講困難,但對於別人來講並不必定是這樣。說它重要,是由於應用面比較廣,好比協做例程、異常處理;還能夠模擬別的編程語言裏的return、實現無條件跳轉等。其最大,也是最普遍的應用之一就是用於web服務器的開發。
 
在對任意一個Scheme表達式求值時,必須記錄兩件事:
  1. 對什麼求值
  2. 對求出的值作什麼
第二件事即是continuation。
 
這個定義至關簡單。例如:
對於表達式(if (null? x) (quote ()) (cdr x))有六個continuations,分別等待以下值:
  1. (if (null? x) (quote ()) (cdr x))的值
  2. (null? x)的值
  3. null?的值
  4. x的值
  5. cdr的值
  6. x的值
其中,(cdr x)的值沒有列出來是由於她就是第一個continuation等待的值 。從這裏能夠看出,continuation是一個計算環境,它在等待一些計算,只有這些計算完成後,才能進行該計算環境相關的計算。也就是說,continuation是一個函數。
 
Scheme容許用call-with-current-continuation函數(間寫爲call/cc)對任意表達式的continuation進行捕捉。call/cc必須以一個函數p爲參數,而函數p又必須只有一個參數k,這個參數k就是被捕獲的continuation,也是一個函數。call/cc構造當前的continuation並傳遞給p。每當k被應用於一個值,它就返回call/cc的continuation的值,而這個值最終就是應用call/cc的值——或者說是(call/cc p)的值。例如:
  1. (call/cc
  2.   (lambda (k)
  3.     (* 5 4))) 
  4. (call/cc
  5.   (lambda (k)
  6.     (* 5 (k 4))))
  7. (+ 2
  8.    (call/cc
  9.      (lambda (k)
  10.        (* 5 (k 4)))))
第一個例子中,call/cc捕獲的continuation就是返回一個值,並將其傳遞給匿名函數。這個匿名函數並無使用k,所以,其結果爲20。第二個例子中的continuation與第一個相同,只不過在匿名函數中將continuation應用於4,所以結果爲4。第三個例子中的continuation是對其等待的值進行+2操做,所以,結果爲6。
 
維基百科上有個例子:
  1. (define the-continuation #f)
  2. (define (test)
  3.   (let ((i 0))
  4.     ; call/cc calls its first function argument, passing
  5.     ; a continuation variable representing this point in
  6.     ; the program as the argument to that function.
  7.     ;
  8.     ; In this case, the function argument assigns that
  9.     ; continuation to the variable the-continuation.
  10.     ;
  11.     (call/cc (lambda (k) (set! the-continuation k)))
  12.     ;
  13.     ; The next time the-continuation is called, we start here.
  14.     (set! i (+ i 1))
  15.     i))
經過這個例子能夠看出,continuation爲何又叫first class continuation——由於它和函數同樣,是一等公民。這個例子裏面用到的call/cc是call-with-current-continuation的一個別名。
 
下面再來看兩個比較複雜一點的例子。
例1:
  1. (let ([x (call/cc
  2.           (lambda (k) k))])
  3.   (x (lambda (ignore) "hi")))
這個例子中,call/cc將捕獲的continuation傳遞給(lambda (k) k),而後將該函數結果綁定到x,因爲函數(lambda (k) k)返回k,所以,x被綁定爲continuation自身。以後,將x的值應用於對錶達式(lambda (ignore) "hi")求值的結果(函數f)。因爲x是一個continuation,所以,此時continuation變爲對錶達式(lambda (ignore) "hi")的求值 ,此時continuation變爲將該求值的結果應用到該結果自身。同時,由於x所綁定的continuation改變了,所以x所綁定的值也被修改成這個結果(函數f),並應用到f,即(f f),所以,結果爲"hi"。
 
例2:
  1. (((call/cc
  2.    (lambda (k) k))
  3.   (lambda (x) x))
  4.  "HEY!")
其實這個例子與例1是同樣的,只不過寫法不同而已。但這個例子更具備迷惑性,你可能比較容易猜到結果,殊不知道怎麼得出這個結果的。個人理解也不必定正確,列在這裏供你們參考:
call/cc捕獲當前的continuation,由於(lambda (k) k),因此當前的continuation被返回並直接應用於(lambda (x) x),此時continuation就變爲對(lambda (x) x)求值,而後應用到(lambda (x) x),最後將結果應用到"HEY!",即((f f) "HEY!")。
 
下面這個例子就比較有意思了:
  1. (define retry #f) 
  2. (define factorial
  3.   (lambda (x)
  4.     (if (= x 0)
  5.         (call/cc
  6.          (lambda (k)
  7.            (set! retry k)
  8.            1))
  9.         (* x (factorial (- x 1))))))
這裏保存了continuation到retry中。那麼這個continuation是什麼樣呢?以(factorial 4)爲例:
  1. (* 4 (factorial 3))
  2. (* 4 (* 3 (factorial 2)))
  3. (* 4 (* 3 (* 2 (factorial 1))))
  4. (* 4 (* 3 (* 2 (* 1 (factorial 0)))))
  5. (* 4 (* 3 (* 2 (* 1 (call/cc (lambda (k) (set! retry k) 1))))))
由於(lambda (k) (set! retry k) 1)中並無應用k,所以,這個continuation應當是,返回當前值(此時爲1)並乘以4!,或者說,將4!乘到當前值上。擴展到n的狀況,則應當爲:以參數爲基數,作基數×n!操做。所以(factorial 4)而後(retry n)的結果應該是n*4!。
 
前面提到continuation的應用如協做例程、異常處理、模擬別的編程語言裏的return、實現無條件跳轉,下面就來看看。
return語句:
  1. (define (f return)
  2.   (return 2)
  3.   3)
  4. (display (f (lambda (x) x))) ; displays 3
  5. (display (call-with-current-continuation f)) ; displays 2
無條件跳轉:
  1. (define product
  2.   (lambda (ls)
  3.     (call/cc
  4.       (lambda (break)
  5.         (let f ([ls ls])
  6.           (cond
  7.             [(null? ls) 1]
  8.             [(= (car ls) 0) (break 0)]
  9.             [else (* (car ls) (f (cdr ls)))]))))))
計算過程當中碰到第一個0,該函數便會馬上退出,至關於break的功能。
 
異常處理:能夠參考 這裏。由於裏面涉及到了其餘的一些Scheme特性,而我尚未搞懂,所以暫時不作描述。
 
協做例程(coroutines):
  1. ;;; A naive queue for thread scheduling.
  2. ;;; It holds a list of continuations "waiting to run".
  3. (define *queue* '())
  4. (define (empty-queue?)
  5.      (null? *queue*))
  6.  
  7. (define (enqueue x)
  8.      (set! *queue* (append *queue* (list x))))
  9. (define (dequeue)
  10.      (let ((x (car *queue*)))
  11.        (set! *queue* (cdr *queue*))
  12.        x))
  13. ;;; This starts a new thread running (proc).
  14. (define (fork proc)
  15.      (call/cc
  16.       (lambda (k)
  17.         (enqueue k)
  18.         (proc))))
  19. ;;; This yields the processor to another thread, if there is one.
  20. (define (yield)
  21.      (call/cc
  22.       (lambda (k)
  23.         (enqueue k)
  24.         ((dequeue)))))
  25. ;;; This terminates the current thread, or the entire program
  26. ;;; if there are no other threads left.
  27. (define (thread-exit)
  28.      (if (empty-queue?)
  29.          (exit)
  30.          ((dequeue))))
  31. (define (do-stuff-n-print str)
  32.      (lambda ()
  33.        (let loop ((n 0))
  34.          (when (< n 10)
  35.          (display (format "~A ~A\n" str n))
  36.          (yield)
  37.          (loop (+ 1 n))))))
  38.  
  39. ;;; Create two threads, and start them running.
  40. (fork (do-stuff-n-print "This is AAA"))
  41. (fork (do-stuff-n-print "Hello from BBB"))
  42. (thread-exit)
這個例子稍顯繁瑣,下面會給出一個簡化的實現。這裏先說明一下這段代碼完成何種功能:其中有一個全局變量queue,並定義了enqueue和dequeue函數來向queue中添加或取出一個元素。(fork proc)首先將當前的continuation插入隊列,而後執行proc;而yield是首先將當前的continuation添加到隊列尾部,而後將隊首的continuation取出來並執行。do-stuff-n-print循環打印格式化後的str字串AAA(題外話:這裏有個命名的let,名字爲loop,其與內嵌的函數定義同樣),每循環一次,就對yield進行求值,先保存當前的continuation(即開始下一次循環),而後取出隊首的continuation(即對下當前fork以後的表達式求值),而後執行該求值過程。所以,程序執行第二個fork,即先打印BBB,而後取出隊首的continuation,即第二次執行打印AAA的loop,而後是yield——保存AAA的執行——執行BBB的第二次loop——……
 
這個例子能夠簡化爲:
  1. (define lwp-list '())
  2. (define lwp
  3.   (lambda (thunk)
  4.     (set! lwp-list (append lwp-list (list thunk))))) 
  5. (define start
  6.   (lambda ()
  7.     (let ([p (car lwp-list)])
  8.       (set! lwp-list (cdr lwp-list))
  9.       (p))))
  10. (define pause
  11.   (lambda ()
  12.     (call/cc
  13.       (lambda (k)
  14.         (lwp (lambda () (k #f)))
  15.         (start)))))
  16. (lwp (lambda () (let f () (pause) (display "h") (f))))
  17. (lwp (lambda () (let f () (pause) (display "e") (f))))
  18. (lwp (lambda () (let f () (pause) (display "y") (f))))
  19. (lwp (lambda () (let f () (pause) (display "!") (f))))
  20. (lwp (lambda () (let f () (pause) (newline) (f))))
  21. (start)
這段程序的執行永遠不會自動中止。其中,lwp將函數放入列表尾部,start函數取出列表的第一個元素(函數)並執行它,pause函數捕獲一個continuation,在這個continuation的處理函數中,把一個對該continuation求值的函數,經過lwp函數放入列表中,而後對start函數求值。而後分別將含有打印"h"、"e"、"y"、"!"語句的函數放入列表中。此後調用start函數,其過程以下:
  1. 取出函數(lambda () (let f () (pause) (display "h") (f)))
  2. 將f綁定爲不接受參數的函數,該函數依次執行(pause)、(display "h")和(f)——無限循環
  3. 執行(pause),即將含有(display "h")和(f)做爲continuation的函數放入列表尾部
  4. 執行(start)
  5. 取出函數(lambda () (let f () (pause) (display "e") (f)))
  6. 將f綁定爲不接受參數的函數,該函數依次執行(pause)、(display "e")和(f)——無限循環
  7. 執行(pause),即將含有(display "e")和(f)做爲continuation的函數放入列表尾部
  8. 執行(start)
  9. 取出函數(lambda () (let f () (pause) (display "y") (f)))
  10. 將f綁定爲不接受參數的函數,該函數依次執行(pause)、(display "y")和(f)——無限循環
  11. 執行(pause),即將含有(display "y")和(f)做爲continuation的函數放入列表尾部
  12. 執行(start)
  13. 取出函數(lambda () (let f () (pause) (display "!") (f)))
  14. 將f綁定爲不接受參數的函數,該函數依次執行(pause)、(display "!")和(f)——無限循環
  15. 執行(pause),即將含有(display "!")和(f)做爲continuation的函數放入列表尾部
  16. 執行(start)
  17. 取出函數(lambda () (let f () (pause) (newline) (f)))
  18. 將f綁定爲不接受參數的函數,該函數依次執行(pause)、(newline)和(f)——無限循環
  19. 執行(pause),即將含有(newline)和(f)做爲continuation的函數放入列表尾部
  20. 執行(start)
  21. 取出函數(lambda () (k #f)))
  22. 執行k,即輸出"h",調用(f)
  23. 執行(pause),即將含有(display "h")和(f)做爲continuation的函數放入列表尾部
  24. 執行(start)
  25. 取出函數(lambda () (k #f)))
  26. 執行k,即輸出"e",調用(f)
  27. 執行(pause),即將含有(display "e")和(f)做爲continuation的函數放入列表尾部
  28. 執行(start)
  29. 取出函數(lambda () (k #f)))
  30. 執行k,即輸出"y",調用(f)
  31. 執行(pause),即將含有(display "y")和(f)做爲continuation的函數放入列表尾部
  32. 執行(start)
  33. 取出函數(lambda () (k #f)))
  34. 執行k,即輸出"!",調用(f)
  35. 執行(pause),即將含有(display "!")和(f)做爲continuation的函數放入列表尾部
  36. 取出函數(lambda () (k #f)))
  37. 執行k,即輸出換行符,調用(f)
  38. 執行(pause),即將含有(newline)和(f)做爲continuation的函數放入列表尾部
  39. 回到20
 
最後,留下一個問題供你們討論吧——我目前不知道怎麼來解釋這個問題。這就是著名的陰陽謎題:
  1. (let* ((yin
  2.          ((lambda (cc)
  3.             (display #\@) cc)
  4.           (call/cc
  5.            (lambda (c) c))))
  6.        (yang
  7.          ((lambda (cc)
  8.             (display #\*) cc)
  9.           (call/cc
  10.            (lambda (c) c)))))
  11.     (yin yang))
其結果看起來應當是這樣:
  1. @*@**@***@****@*****@******@*******@********@*********@**********@***********@************@*************@**************@***************@****************@*****************@******************@*******************@********************@*********************@**********************@***********************@************************@*************************@**************************@***************************@****************************@*****************************@******************************@*******************************@********************************@*********************************@**********************************@***********************************@************************************@*************************************@**************************************@***************************************@****************************************@*****************************************@******************************************@*******************************************@********************************************@*********************************************@*********************
這段程序的執行永遠不會自動中止。
 
PS. 因爲我本身並無太理解continuation,所以,這篇就更像一個信息的翻譯和聚合,並無太多本身的理解在裏面。寫完這篇,我對於continuation依然仍是懵懵懂懂的,更談不上運用它。也許隨着時間的流逝,隨着對Scheme的熟悉,會逐漸熟悉continuation吧。
 
參考
 
 
 
 
 宏是擴展Scheme語言的手段。咱們知道,Scheme的哲學就是最小化語言核心,所以,Scheme語言自己只提供很是有限的一些特性。爲了讓Scheme更增強大,宏承擔起了擴展語言的任務。宏分爲兩種:衛生宏和不衛生宏。簡言之,衛生宏就是不會致使反作用,不會意外地捕捉到錯誤的標識。C語言裏面的宏則不是衛生宏,由於會致使反作用。
 
在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來知足需求:
  1. (define-syntax when
  2.   (syntax-rules ()
  3.     ((when pred exp exps ...)
  4.       (if pred (begin exp exps ...)))))
其中,body裏的when一般使用「_」代替。每次使用when時,就會被展開爲對if的使用。
 
宏是Scheme的一個很是強大的功能,網上有不少專門針對Scheme宏編程的資源,有興趣的能夠搜索一下。
 
參考:
    1. 維基百科
    2. Racket文檔
    3. Schemers.org
       
       
      圖形界面的小應用 
       
      在學習了一些Scheme基礎以後,我寫了一個小程序,其功能以下:
      • 一個菜單欄上有兩個菜單:File和Help
      • File菜單包含Start和Stop兩個菜單項
      • Help包含About菜單項
      • 點擊Start,程序將畫出三個連在一塊兒的空心小矩形,而後這三個小矩形同時向右移動
      • 點擊Stop,中止移動
      好吧,我認可,這就是個貪食蛇的雛形。記得當年學習C#時也寫了個最基本的貪食蛇遊戲,如今算是二進宮了,輕車熟路。
       
      在開始以前,須要先大體說明一下Racket的對象系統。
      定義一個類:
      1. (class superclass-expr decl-or-expr ...)
      例如:
      1. (class object%
      2.   (init size) ; initialization argument
      3.  
      4.   (define current-size size) ; field
      5.  
      6.   (super-new) ; superclass initialization
      7.  
      8.   (define/public (get-size)
      9.     current-size)
      10.  
      11.   (define/public (grow amt)
      12.     (set! current-size (+ amt current-size)))
      13.  
      14.   (define/public (eat other-fish)
      15.     (grow (send other-fish get-size))))
      這是一個匿名類,其基類爲object%,初始化參數爲size——相似於C++的初始化列表,接下來current-size指的是一個私有成員,其初始值由初始化參數size所指定。再以後是經過(super-new)對父類即object%類調用「構造函數」。以後是三個公有的成員函數。
       
      爲了可以建立這個類對象而不須要每次都把上面這一大段寫到代碼裏,能夠用define把這個匿名類綁定到一個變量上,好比叫作fish%。那麼須要建立一個fish%的對象就很簡單:
      1. (new fish% (size 10))
      須要注意的是,在Racket(也許其餘的Scheme實現也同樣)中,「{}」、「()」、「[]」是相同的,只不過必須匹配,如「{」必須匹配「}」。
       
      爲了調用一個類的函數,須要用如下兩種形式之一:
      1. (send obj-expr method-id arg ...)
      2. (send obj-expr method-id arg ... . arg-list-expr)
      如:
      1. (send (new fish% (size 10)) get-size)
      看到這裏你也許會感到很奇怪:爲何沒有析構函數?早在Lisp誕生初期,它就包含了垃圾收集功能,所以,根本不須要你釋放new獲得的對象。過了許多年以後,許多包含垃圾收集功能的語言誕生了。
       
      此外,結構體也是頗有用的東西,它與類的區別,跟C++中類與結構體的區別差很少,但Racket結構體提供了不少輔助函數——固然是經過宏和閉包來提供這些函數。結構體是經過struct來定義的。——沒猜錯的話,struct應該也是一個宏——尚未細看Racket的代碼。
      1. (struct node (x y) #:mutable)
      其使用以下所示:
      1. (node-x n) ; get x from a node n
      2. (set-node- n 10) ; set x to 10 of a node n
      3. (node? n) ; predicate, check if n is a node
      還有其餘的輔助函數,在此不一一列舉。
       
      這個應用的核心在於內嵌在canvas上的一個定時器:
      1. (define timer
      2.       (new timer%
      3.            [notify-callback
      4.             (lambda ()
      5.               (let ((dc (send this get-dc)))
      6.                 (send dc clear)
      7.                 (map (lambda (n)
      8.                        (send dc
      9.                              draw-rectangle (node-x n) (node-y n) 5 5))
      10.                      lst)
      11.                 (map (lambda (n)
      12.                        (set-node-x! n (+ (node-x n) 5)))
      13.                      lst)))
      14.             ]
      15.            [just-once? #f]))
      每當超時時間發生時,notify-callback所綁定的回調函數就會被調用,完成在canvas上畫圖的功能,同時更新圖形所在的位置,這樣便造成了移動。
       
      固然,如今這個程序還只是雛形而已,總代碼量爲101行。若是要完善成爲一個貪食蛇遊戲,還須要作不少工做,同時還須要進行一些設計,至少將Model、View和Controller分開吧。
       
      從這裏也能夠看出,用Scheme來進行面向對象的開發也十分容易,並不須要用到Scheme的高級功能例如宏和續延等等。固然,若是能運用好這些高級功能,相信代碼會更加簡單。
       

      續延的例子 
       
      先說一些題外話。
       
      關於函數式編程,這是Lisp 最多見的編程範式,它是構建在計算的基礎上的。而另外一個派別——命令式編程(包括面向過程範式、面向對象範式等等)則是構建在機器模型和彙編語言的基礎上的。那麼,函數式編程的特徵有哪些呢?一般說來,具有下面特徵的編程範式就能夠說是函數式編程:
      • 聲明式的。一個程序/過程更像是對問題的描述,或者對解決方法的描述,而不是如何進行一步步的計算。
      • 無(或者最小化)反作用的。所謂反作用(side-effect)指的是除了返回的計算結果以外,還修改了其餘的對象。C庫函數中strcat()函數就是典型具備反作用的函數。
      • 若是有反作用,這個反作用僅僅做用於惟一屬主爲該函數的對象。例如一個函數返回一個新構造的對象,該對象被修改了。
      • 計算結果只與輸入有關,與執行次數無關。不管執行多少次,一樣的定義域對應的值域徹底同樣
      • 返回值可被安全地修改
      至於閉包(closure),在Lisp中能夠說無處不在,所以要特別注意。所謂閉包,就是一個計算過程以及其所使用的計算環境所構成的東西。
       
      在「 開始學習Scheme :難學卻重要的Scheme特性」中提到了續延(continuation)以及call/cc,然而裏面有不少分析並非能夠形式化的,所以,很難做爲普適的分析方法。此次讓經過對「 開始學習Scheme:難學卻重要的Scheme特性 」中的例子從新進行說明,來看看比較形式化的方法。
       
      首先回憶一下,continuation是指某個計算的將來路徑。它是一個過程(procedure),而過程又是閉包(closure),所以continuation就是closure。
       
      來看看下面的表達式
      1. (if (null? x) (quote ()) (cdr x))
      假定求值的順序爲從右到左,那它所包含的continuation有:
      1. 等待x的continuation
      2. 等待cdr的continuation
      3. 等待 x的continuation
      4. 等待 null?的continuation
      5. 等待 (null? x)的continuation
      6. 等待 (if (null? x) (quote ()) (cdr x))的continuation
      根據定義,第一個continuation能夠寫成:
      1. (lambda (v1) (if (null? x) (quote ()) (cdr v1)))
      一旦x值肯定以後,就能夠將該continuation應用到x的值上:
      1. ((lambda (v1) (if (null? x) (quote ()) (cdr v1))) x)
      而第二個continuation在第一個continuation的基礎上,等待cdr的值:
      1. ((lambda (v2) (if (null? x) (quote ()) (v2 v1))) cdr)
      以此類推,後面的continuation以下:
      1. ((lambda (v3) (if (null? v3) (quote ()) (v2 v1))) x)
      2. ((lambda (v4) (if (v4 v3) (quote ()) (v2 v1))) null?)
      3. ((lambda (v5) (if v5 (quote ()) (v2 v1))) (null? x))
      4. ((lambda (v6) v6) (if v5 (quote ()) (v2 v1)))
      也許這個例子比較複雜一些,那咱們仍是看看最簡單的這個吧:
      1. (/ (- x 3) 10)
      在求出(- x 3)的值val以後,其後續計算爲:
      1. (/ val 10)
      那麼該continuation就能夠寫成:
      1. (lambda (val) (/ val 10))
      將(- x 2)的值做爲該continuation的參數,即可以獲得結果了。
       
      再說一個題外話:這裏「-」操做被隱式地傳遞了一個continuation,即(lambda (val) (/ val 10))。若是將continuation顯式傳遞的話,那就是另一個大話題:CPS(Continuation Passing Style)。
       
      再回憶一下call-with-current-continuation,它捕獲當前的continuation,並執行其函數參數。該函數參數是一個函數,接受continuation爲參數並執行相應的操做。所以,call/cc的含義爲:捕獲當前的continuation,而後執行相應的操做。
       
      在上面的例子中,val就是當前continuation所期待的值,用call/cc來改寫,則是以下形式:
      1. (/ (call/cc
      2.     (lambda (cc)
      3.       (cc (- x 3))))
      4.    10)
      所以,能夠將對call/cc的調用視爲後續計算所須要的val,而後將該continuation做爲call/cc的函數參數的參數,執行(cc (- x 3))操做。
       
      所以,對於call/cc操做,能夠分爲兩步來分析:
      1. 用val替代call/cc調用——肯定continuation是什麼
      2. 使用該continuation作什麼
      此處須要特別注意的是:在第二步中,一旦將該continuation應用以後,該continuation後面的全部continuation都會被拋棄,表達式當即返回。所以捕獲的continuation又被稱爲逃逸函數(escape procedure)。在上面的例子中,若是(cc (- x 3))以後還有任何後續操做,這些操做都將被忽略。
       
      根據這個規則,能夠將上面的形式反推回去。當前的continuation是:
      1. (/ val 10)
      2. ;; 由於continuation是procedure,所以應當使用lambda來表示:
      3. (lambda (val) (/ val 10))
      對當前continuation作什麼:
      1. (lambda (cc) (cc (- x 3)))
      2. ;;因爲已經得到了當前的continuation,所以將該函數應用於當前的continuation,以下:
      3. ((lambda (cc) (cc (- x 3))) (lambda (val) (/ val 10)))
      有了這個分析的基礎,如今來看看使用這兩個步驟來分析使用call/cc的函數。
      1. (call/cc
      2.   (lambda (k)
      3.     (* 5 4)))
      4. ;; CC:
      5. (define cc1 (lambda (v) v))
      6. ;; What to do with CC:
      7. ((lambda (cc) (* 5 4)) cc1)
      8. (call/cc
      9.   (lambda (k)
      10.     (* 5 (k 4))))
      11. ;; CC:
      12. (define cc2 (lambda (v) v))
      13. ;; What to do with CC:
      14. ((lambda (cc) (cc 4)) cc2)
      15. (+ 2
      16.    (call/cc
      17.      (lambda (k)
      18.        (* 5 (k 4)))))
      19. ;; CC:
      20. (define cc3 (lambda (v) (+ 2 v)))
      21. ;; What to do with cCC:
      22. (cc3 4)
      這三個相對簡單些,所以不作說明。
      1. (define the-continuation #f)
      2. (define (test)
      3.   (let ((i 0))
      4.     (call/cc (lambda (k) (set! the-continuation k)))
      5.     ( i (+ i 1))
      6.     i))
      7. ;; CC:
      8. (define cc4
      9.   (let ((i 0))
      10.     (lambda (v)
      11.       v
      12.       ( i (+ i 1))
      13.       i)))
      14. ;; What to do with CC:
      15. ((lambda (cc)
      16.   (set! the-continuation cc)) cc4)
      17. ;; Testing
      18. (cc4 0)
      19. (cc4 0)
      20. (test)
      21. (the-continuation 0)
      22. (the-continuation 0)
      此處,cc4引用了一個自由變量i。因爲全部對test這個closure的調用(而不是每一個closure的示例)都共享變量i,所以,i在cc4中也應當是一個共享變量。所以,cc4 應該是這樣:
      1. (define cc4
      2.   (lambda (v)
      3.     (let ((i 0))
      4.       v
      5.       (set! i (+ i 1))
      6.       i)))
      下面這個例子就複雜一些:
      1. (let ([x (call/cc
      2.           (lambda (k) k))])
      3.   (x (lambda (ignore) "hi")))
      4. ;; CC:
      5. (define cc5
      6.   (lambda (v)
      7.     (let (( x v))
      8.       (x (lambda (ignore) "hi")))))
      9. ;; What to do with cc:
      10. ((lambda (cc)
      11.   (cc (lambda (ignore) "hi")))) cc5)
      12. ;; Testing
      13. (cc5 (lambda (i) "hI"))
      關鍵在於「what to do with cc」部分。由於(lambda (cc) cc)只是返回cc,並且是在let中,所以須要對cc作什麼就變成了(cc (lambda (ignore) "hi"))。一樣:
      1. (((call/cc
      2.    (lambda (k) k))
      3.   (lambda (x) x))
      4.  "HEY!")
      5. ;; CC
      6. (define cc6
      7.   (lambda (v)
      8.     ((v (lambda (x) x))
      9.      "HEY~!")))
      10. ;; What to do with cc:
      11. ((lambda (cc) (cc (lambda (x) x))) cc6)
      在下面這個例子中:
      1. (define retry #f) 
      2. (define factorial
      3.   (lambda (x)
      4.     (if (= x 0)
      5.         (call/cc (lambda (k) (set! retry k) 1))
      6.         (* x (factorial (- x 1))))))
      7. ;; CC:
      8. (define f
      9.   (lambda (x)
      10.     (lambda (v)
      11.       (if (= x 0)
      12.           v
      13.           (* x ((f (- x 1)) v))))))
      14. (define cc7 (f 4))
      15. ;; What to do with cc:
      16. ((lambda (cc) (set! retry cc) ((cc 1)) cc7)
      17. ;; Testing
      18. (retry 1)
      19. (retry 2)
      20. (retry 5)
      要注意當x不等於0的狀況。此時因爲 (f (- x 1)) 返回的是一個continuation,所以,須要將其應用到v上。另外就是call/cc捕獲到當前的continuation是將f應用於某個數的結果。
       
      對於下面這個函數:
      1. (define product
      2.   (lambda (ls)
      3.     (call/cc
      4.       (lambda (break)
      5.         (let f ([ls ls])
      6.           (cond
      7.             [(null? ls) 1]
      8.             [(= (car ls) 0) (break 0)]
      9.             [else (* (car ls) (f (cdr ls)))]))))))
      因爲用到了命名let,不是很直觀,所以,首先將其改爲以下形式:
      1. (define product-variant
      2.   (lambda (ls)
      3.     (call/cc
      4.       (lambda (break)
      5.           (cond
      6.             [(null? ls) 1]
      7.             [(= (car ls) 0) (break 0)]
      8.             [else (* (car ls) (product (cdr ls)))])))))
      其continuation以下:
      1. ;; CC:
      2. (define cc8 (lambda (v) v))
      3. ;; What to do with cc:
      4. (define (p-1 ls)
      5.   (lambda (cc)
      6.     (cond
      7.       [(null? ls) 1]
      8.       [(= (car ls) 0) (cc 0)]
      9.       [else (* (car ls)
      10.                [(p-1 (cdr ls)) cc])])))
      11. ((p-1 '(1 2 3 0 5)) cc8)
      12. ((p-1 '(5 4 3 2)) cc8)
      因爲p-1是一個以cc爲參數的函數,所以,(product (cdr ls))就須要轉換成[(p-1 (cdr ls)) cc]
       
        理解CPS  
       
       上一篇經過一些例子講述瞭如何來理解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。例如:
      1. (/ (- x 3) 10)
      對函數「-」的調用顯然不是尾部調用,所以,該調用的continuation即是對該調用的返回值進行除以10的操做。
       
      那麼,什麼叫作CPS——Continuation Passing Style呢?CPS就是指將隱式傳遞給(關聯於)某個函數調用的continuation顯式地傳遞給這個函數。對於上面的例子,若是咱們將「-」函數改寫成現實傳遞continuation的版本,那就是:
      1. (define (my-minus x k) (k (- x 3)))
      其中,參數k就是顯式傳遞給函數的continuation。爲了完成上述除以10的計算,對my-minus的調用就應該寫成(假設x值爲15):
      1. (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改寫。
      1. (letrec ([f (lambda (x) (cons 'a x))]
      2.          [g (lambda (x) (cons 'b (f x)))]
      3.          [h (lambda (x) (g (cons 'c x)))])
      4.   (cons 'd (h '())))
      (關於letrec,能夠參考 這裏)。首先,咱們來改寫f。由於f使用尾部調用方式調用cons,其後續計算是基於cons的返回結果的, 所以, 對於f能夠改寫爲:
      1. [f (lambda (x k) (k (cons 'a x)))]
      再來看g函數。因爲g函數以非尾部調用的方式調用了f,所以,g傳遞給f的continuation就不是簡單地返回一個值,而是須要進行必定的操做:
      1. [g (lambda (x k) (f x
      2.                     (lambda (v)
      3.                       (k (cons 'b v)))))]
      須要注意的是,這裏g的含義是:以x和一個continuation調用f,將所得的結果進行continuation指定的計算,並在該計算的結果上應用k。
      最後,h函數經過尾部調用的方式調用g,所以,對h調用的continuation就是對g調用的continuation。那麼,h能夠改寫爲:
      1. [h (lambda (x k)
      2.      (g (cons 'c x) k))]
      最後,將這些組合到一塊兒:
      1. (letrec ([f (lambda (x k) (k (cons 'a x)))]
      2.          [g (lambda (x k) (f x (lambda (v) (k (cons 'b v)))))]
      3.          [h (lambda (x k) (g (cons 'c x) k))])
      4.   (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改寫的例子。
      1. (define product
      2.   (lambda (ls)
      3.     (call/cc
      4.       (lambda (break)
      5.         (let f ([ls ls])
      6.           (cond
      7.             [(null? ls) 1]
      8.             [(= (car ls) 0) (break 0)]
      9.             [else (* (car ls) (f (cdr ls)))]))))))
      首先,將call/cc的調用從函數體中除去,而後,爲product函數加上一個參數k,該參數接受一個參數。另外,由於product增長了一個參數,所以對f這個 命名let也須要增長一個參數。最後,在f的body裏面調用f,也須要改寫成CPS形式。由於對f的調用不是尾部調用,所以在f返回以前,須要進行計算,而後纔是對該結果進行下一步的計算。此時須要的後續計算爲:
      1. (lambda (v) (k (* (car ls) v)))
      對於cond的每一個分支,都須要對其結果進行後續的k計算,這樣,就獲得告終果:
      1. (define product/k
      2.   (lambda (ls k)
      3.     (let f ([ls ls] [k k])
      4.       (cond [(null? ls) (k 1)]
      5.             [(= (car ls) 0) (k "error")]
      6.             [else (f (cdr ls)
      7.                    (lambda (x)
      8.                      (k (* (car ls) x))))]))))
      須要注意的是,因爲product/k是個遞歸過程,對於每一個返回的值,都會有後續操做,所以須要對cond表達式的每一個返回值應用continuation。
       
      習題3.4.1是要求用兩個continuation來改寫reciprocal函數,以下:
      1. (define reciprocal
      2.   (lambda (x ok error)
      3.     (if (= x 0)
      4.         (error)
      5.         (ok (/ 1 x)))))
      6. (define ok
      7.   (lambda (x)
      8.     (display "ok ")
      9.     x))
      10. (define error
      11.   (lambda ()
      12.     (display "error")
      13.     (newline)))
      14. (reciprocal 0 ok error)
      15. (reciprocal 10 ok error)
      習題3.4.2要求用CPS改寫 這裏的retry。
      1. (define retry #f) 
      2. (define factorial
      3.   (lambda (x)
      4.     (if (= x 0)
      5.         (call/cc (lambda (k) (set! retry k) 1))
      6.         (* x (factorial (- x 1))))))
      一樣,須要將factorial改寫成接受兩個參數的函數,第二個參數爲continuation。接下來,把對call/cc的調用去掉,改寫成對k的使用。而後,根據對factorial遞歸調用的非尾部性,肯定如何調用新的函數。結果以下:
      1. (define factorial/k
      2.   (lambda (x k)
      3.     (if (= x 0)
      4.         (begin
      5.           ( retry/k k)
      6.           (k 1))
      7.         (factorial/k
      8.          (- x 1)
      9.          (lambda (v)
      10.            (k (* x v)))))))
      11. (factorial/k 4 (lambda (x) x))
      12. (retry/k 2)
      13. (retry/k 3)
      習題3.4.3要求用CPS改寫下面的函數:
      1. (define reciprocals
      2.   (lambda (ls)
      3.     (call/cc
      4.       (lambda (k)
      5.         (map (lambda (x)
      6.                (if (= x 0)
      7.                    (k "zero found")
      8.                    (/ 1 x)))
      9.              ls)))))
      這道題難度很大,所以Kent給出了提示:須要修改map函數爲接受continuation做爲額外的參數的形式。——至於緣由,我也說不清楚。
      首先,本身實現一個非CPS版本的map函數map1:
      1. (define map1
      2.   (lambda (p ls)
      3.     (if (null? ls)
      4.         '()
      5.         (cons (p (car ls))
      6.               (map1 p (cdr ls))))))
      這裏,當ls爲空時,須要馬上對返回結果'()進行後續計算。而非空時,經過map1調用自身,並對結果進行後續計算。那這時就應該着重考慮這段代碼:
      1. (cons (p (car ls))
      2.       (map1 p (cdr ls)))
      根據對函數參數求值的順序,有兩種順序來進行這段代碼的計算。
      • 當求值順序爲從左向右時
      首先,它計算出(p (car ls))獲得v1,其後續計算爲(map1 p (cdr ls))獲得v2,然後者的後續計算爲(cons v1 v2)並返回該結果。那麼,計算並獲得v1及其後續計算以下:
      1. (p (car ls)
      2.    (lambda (v1)
      3.      (map2/k p (cdr ls) k1)))
      隨即進行後續的k1計算,而k1爲對v1和v2的後續計算:
      1. (lambda (v2)
      2.   (k (cons v1 v2)))
      將這兩個計算合併起來:
      1. (define (map2/k p ls k)
      2.   (if (null? ls)
      3.       (k '())
      4.       (p (car ls)
      5.          (lambda (v1)
      6.            (map2/k p (cdr ls)
      7.                    (lambda (v2)
      8.                      (k (cons v1 v2))))))))
      • 當求值順序爲從右向左時
      首先,它計算出(map1 p (cdr ls))獲得結果v2,其後續計算爲(p (car ls))獲得v1,然後者的後續計算爲(cons v1 v2)並返回結果。那麼,計算獲得v2及其後續計算爲:
      1. (map1/k p (cdr ls)
      2.         (lambda (v2)
      3.           (p (car ls) k1)))
      隨後進行對v2和v1的計算,即:
      1. (lambda (v1)
      2.   (k (cons v1 v2)))
      最後將這兩個計算合併起來:

      1. (define map1/k
      2.   (lambda (p ls k)
      3.     (if (null? ls)
      4.         (k '())
      5.         (map1/k p (cdr ls)
      6.                 (lambda (v2)
      7.                   (p (car ls)
      8.                      (lambda (v1)
      9.                        (k (cons v1 v2)))))))))
      有了CPS的map函數以後,寫出reciprocal的CPS形式就很簡單了:
      1. (define reciprocal1/k
      2.   (lambda (ls k)
      3.     (map1/k (lambda (x c)
      4.               (if (= x 0)
      5.                   (k "zero")
      6.                   (c (/ 1 x))))
      7.             ls
      8.             k)))
      其中,k是整個reciprocal1/k計算完成後的continuation,所以用於返回錯誤;而c則是計算完(/ 1 x)的continuation,只不過在這裏也是k而已。另外,不管是用map1/k,仍是map2/k,其結果應該是同樣的。
       
      總結一下,當使用CPS來取代call/cc或者使用CPS時,若是函數中有對含有CPS的函數的調用,那麼,傳遞進去的continuation或者做爲函數,應用到傳遞來的參數上(非尾部調用);或者做爲一個返回值(尾部調用);若是沒有調用含有CPS的函數,則將其應用到返回值上。
楊輝(Pascal)三角
       

 
 一個楊輝三角以下所示:
爲了計算某個位置上的值:
  1. (define pascal-triangle
  2.   (lambda (row col)
  3.     (cond ([or (= row 0) (= col 0)] 0)
  4.           ([= row col] 1) 
  5.           (else (+ (pascal-triangle (- row 1) (- col 1))
  6.                    (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)
能夠看出,每一次計算下一個值的時候,都沒法徹底使用上一步計算的結果,因此到目前爲止我尚未找到一種使用尾遞歸的方式來改寫這個函數。若是哪位同窗可以用尾遞歸方式解出來,請及時通知我。
 
爲了打印出楊輝三角,須要用兩個循環變量來控制行和列的循環。每次增長一行,就須要對該行的每一列進行輸出,知道行、列值相等。以下:
  1. (define p-t
  2.   (lambda (n)
  3.     (let iter ([i 1] [j 1])
  4.       (when (< i (+ n 1))
  5.         (display (pascal-triangle i j)) (display " ")
  6.         (if (= i j)
  7.             (begin (newline) (iter (+ i 1) 1))
  8.             (iter i (+ 1 j)))))))
此處i爲行號,j爲列號。(p-t 8)結果以下:
  1. 1 1 
  2. 1 2 1 
  3. 1 3 3 1 
  4. 1 4 6 4 1 
  5. 1 5 10 10 5 1 
  6. 1 6 15 20 15 6 1 
  7. 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個結果。因爲每一步保存結果個數不同,所以這種方式的尾遞歸行不通。
 
 
 
    1. 通過了三個月左右的集中學習(intensive learning),終於可使用Scheme作一些簡單的工做了,並且,也可以依葫蘆畫瓢作一些複雜點的工做了——然而,用Scheme語言編程,其重點是如何找到解決問題的方法,而不是如何去實現這個解決方法,由於Scheme提供了很強的表達能力,將程序員們從語言的細節以及語法糖蜜中解放出來,使得他們可以更專一於問題自己,而不是實現自己。
      • 起步
      回想起本身接觸、學習函數式變成和Scheme的通過,其中充滿了曲折和坎坷。Scheme語言自己的簡單性致使了其靈活性,使得一我的能夠在幾天以內學完基本語法,但要使用好Scheme,須要長時間的訓練。另外,對於初學者來講一個難點就是Lisp方言太多,而每一個方言的各類實現也不盡相同,這就致使了在真正開始學習以前須要選擇一個合適的Scheme實現。
       
      在2008年年末的時候,由於跟cuigang討論一個C++的問題,開始知道函數式編程範式,因而買了本《計算機程序的構造與解釋》(SICP)開始了Scheme之旅。然而,一方面函數式語言確實不符合本身一向的編程習慣,另外一方面這本書更注重數學方面,所以,開始的學習歷程很艱苦,不但沒法熟練使用尾遞歸,加之工做負載確實不小,因而便放棄了。
      • 重拾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)。代碼以下:
      1. (define (f-i n) 
      2.   (let iter ((fn 4) (fn-1 2) (fn-2 1) (fn-3 0) (i n)) 
      3.     (cond ((< n 3) n) 
      4.           ((= i 3) fn) 
      5.           (else 
      6.            (iter (+ fn (* 2 fn-1) (* 3 fn-2)) 
      7.                  fn 
      8.                  fn-1 
      9.                  fn-2 
      10.                  (- i 1))))))
      2. 求解1!+2!+3!+...+n!。代碼以下:
      1. (define (fac-sum n)
      2.   (let iter ((fi-1 1) (c 1) (r 0))
      3.     (if (= c (+ n 1)) r
      4.         (iter (* fi-1 c) (+ c 1) (+ r (* fi-1 c))))))
      • 爲了工做而學習Scheme
      快樂的日子老是短暫的。尚未完成Simply Scheme的一半,工做強度又大了起來,因而,Scheme的學習又放下了,直到工做中切切實實須要用到Scheme。正如我在「 [原]從新開始學習Scheme(1)」中所說的,出於興趣和工做須要來學習某項技能,其過程和結果都是不同的,各有長短吧。若是不是項目須要,我也不可能在這麼短的時間內如此高密度地學習一項技能;但正由於項目須要,不可能花大量的時間在本身的興趣點上,這樣就致使了許多問題遺留下來。所以,雖然在「從新開始學習Scheme」系列裏涵蓋了Scheme的幾個重要特性,但好像除了尾遞歸,其餘的特性我都只是摸到,甚至只是剛看到門檻而已。幸虧工做中使用這些特性的機會很少,所以仍是能夠自誇爲Scheme工程師。Scheme程序員?按照Lisp社區的說法,必需要可以寫一個Lisp解釋器,才能自稱爲Lisp程序員。這話一樣適合於Scheme。
       
      在這樣一家公司,因爲戰略調整和重組是很是頻繁的事情,如今雖然開始作Scheme相關的工做,但恐怕過不了多久又會被安排去作其餘的東西,那後續的Scheme學習就會成爲鏡花水月——希望不要再在我身上發生這樣的事情了。
      • 書目
      計算機程序的構造與解釋》:函數式編程和Scheme的入門教材。正如 王垠所說,這本書只有前三章對於初學者有價值。
      Simply Scheme》:雖然SICP的做者認爲這本書的做者在恭維SICP,但確實難度要低一些。
      《 實用Common Lisp》:雖然是CL方言,但可讓讀者對FP和Lisp有個大概的認識
      On Lisp》:高質量的一本書,裏面一些重要章節一樣適用於Scheme和CL,例如關於continuation的說明。
      The Scheme Programming Language》:很少說了,估計地位跟TCPL之類的書差很少。
      還有不少其餘的書,這裏不一一列舉。
      • 參考資料
      Dr Racket:Scheme的一個實現,提供全面的文檔和圖形庫
      Schemers.org:Scheme社區
      ReadScheme.org:論文聚合點
      "Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I":關於FP和Lisp的開山之做
      "Why Functional Programming"
      • 真的能夠說學會了,不用再學習了嗎?
      雖然只是剛剛具有了Scheme的基礎,但系統學習這一階段確實能夠結束了,畢竟,項目就在那裏,公司也不可能永遠讓員工處於學習狀態,只向員工投入資金而不向員工要產出的公司只能出如今夢裏。所以,之後基本不會有大量時間來集中學習Scheme了。我想,是時候總結一下了。——之後隨用隨學,一次一個小知識點。
       
       

=========================== End

相關文章
相關標籤/搜索