标签:区别 this 最好 关联关系 oop rac 基础概念 home 不同的
开始学习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的基本概念。
第一个,也是最基本的概念: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)是个好例子。最著名的估计是
这个,详情可以参考
田春冰河的博客。
线性递归以及循环不变式
例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)。这便是一个线性递归。为了减少其空间复杂度,可以使用线性迭代来代替(使用递归实现):
(define (exp-iter x n)
- (define (iter x n r)
- (if (= n 0)
- r
- (iter x (- n 1) (* r x))))
- (iter x n 1))
计算过程依然有改进空间,那便是可以降低时间复杂度。根据
x^n = x^(n/2)*x^(n/2)
可知,计算x^n的时间复杂度可以降低为O(logn)。此时,需要一个循环不变式来保证计算结果的正确性。设r初始值为1,则在计算过程中,从一个状态迁移到另一个状态(n为奇数迁移到n为偶数)时,r*x^n始终保持不变。而此时计算方法为:
n为奇数,则x^n = x*x^(n-1)
n为偶数,则x^n = x^(n/2)*x^(n/2) = (x^2)^(n/2)
因此,计算过程如下:
(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学习就会成为镜花水月——但愿不要再在我身上发生这样的事情了。
《
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
开始学习Scheme
标签:区别 this 最好 关联关系 oop rac 基础概念 home 不同的
原文地址:https://www.cnblogs.com/lsgxeva/p/10153790.html