开始学习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