除了嵌套过程 Scheme 还有一个额外的构造用来定义(临时)环境。叫做“let 表达式”(let、let*、letrec)。生成一种“块结构”(类似于 Algol 60)。let 是一个特殊的形式,它接受绑定和一个“块体”作为参数。在这些绑定定义的上下文中,在块体中包含的表达式接着顺序的执行并返回最后一个的值。let* 假定所有绑定将被顺序的建立(从第一个到最后一个),而 let 不做这种假定。Letrec 对于相互递归的过程是需要的。 (let ((seven 7) (thirteen 13))
(DisplayLine (* seven thirteen)) (+ seven thirteen))
; 91 ; 乘法的结果
; 20 ; 加法的结果
(let ((aNumber 7)
(twiceThat (* 2 aNumber)))
(display twiceThat) )
;ERROR: Undefined global variable aNumber
(let* ((aNumber 7)
(twiceThat (* 2 aNumber)))
(display twiceThat) )
;14 #t
这里我们需要使用 let* 来使相互依赖的绑定工作。MacScheme 的 let 显然的尝试从右到左的绑定符号,因此在被定义之前就遇到了 aNumber。
过程 - 定义, 测试, 调试
绑定 lambda 表达式到标识符导出了“命名”过程的概念。典型的 Scheme 程序将包含大量用户定义的过程,通常组织到必须在程序执行之前装载的一些文件中。使用 load 过程完成这个任务。 (load "HardDisk:cookieMonster.scm")
将尝试装载并求值在目录 HardDisk 中的文件 cookieMonster.scm 中的所有表达式。文件操纵的详情通常特定于特定 Scheme 实现。但是,习惯于对 Scheme 文件使用后缀“scm”。
我们可以“别名”任何数据类型,包括过程。它可以用来裁剪系统函数的名字来适合我们的偏好;带有额外的一层间接的代价。我们通常保护重要的系统定义的标识符不被意外的重定义。但是,对于经常需要的操作符(比如 car 和 cdr),导致的在执行速度上的损失通常是不可接受的高。 (define head car)
;head
(define tail cdr)
;tail
(head (tail '(vampire banshee troll)))
;banshee
(set! + *) ; sneaky: redefine '+' as a multiplication symbol ?
;ERROR: Integrable procedures may not be redefined
请注意 Scheme 的绑定模型是完全一般性的。在对“被动”对象(数据)能做的事情和对“主动”对象(过程)能做的事情之间没有区别。例如,我们可以轻易的写返回另一个过程作为值的过程。 (define GenericMagicLamp
; 这是一个我们希望返回的"lambda"表达式
(lambda (noOfWishes)
(define count 0) ;局部变量
(lambda ()
(if (< count noOfWishes)
(begin
(set! count (+ 1 count))
'Granted)
'(you've had all you're going to get !))
) ))
; GenericMagicLamp
GenericMagicLamp 实现了阿拉丁神灯的概念,在其中关押了一个妖魔并通过摩擦震动来释放出来。出于感激的天性,她将接着提供惯例的三个愿望,通常带有针对 meta-circularity 的某种保护(就是说,拒绝能带给你额外愿望的愿望)。我们的过程包含对一个局部变量 count 的声明,它将被初始化为零并在每次满足愿望之后增加一。在这一点上,注意到 Scheme 变量都有“无限的范围”是很重要的,这意味着,(例如)与 Pascal 中的其他变量不同,在它们在其中定义的过程的连续调用之间,它们不会失去它们的值。因此 Count 将开始于为零的一个值,在第一个愿望之后保持为一的一个值,第二次之后是二,在第三次调用之后是三。尽管 GenericMagicLamp 自身不会满足任何愿望,但是它可以用做一个发生器,用来构造任意数目的神灯,带有在创建时刻定义的愿望数目。这是嵌套在这个过程之中的第二个 lambda 表达式的作用。记住这个 Scheme 函数总是返回它们遇到的最后一个表达式的值。在这里是第二个 lambda 表达式,因此 GenericMagicLamp 是返回另一过程作为它的调用结果的一个过程。 (define AlladinsLamp (GenericMagicLamp 3))
;AlladinsLamp
可以用来制造这么一个灯(一个过程)并让 AlladinsLamp 充当它的标识符。记住这个过程还继承了在创建发生的时候、它在其中建立的环境中的所有绑定变量是很重要的。Count 因此对 AlladinsLamp 是可以访问的,并且它仍被绑定为零。如果我们现在开始摩擦,我们就会得到我们期望的行为了(这些愿望的“形态”将保持私有)。 (AlladinsLamp)
;granted
(AlladinsLamp)
;granted
(AlladinsLamp)
;granted
(AlladinsLamp)
;"you've had all you're going to get !"
(set! count 0)
;ERROR: Undefined global variable count
;hard luck - "count" is kept private to this lamp object, and can't be accessed !
递归
递归具体化了自引用的概念,并且它是 Scheme 指定一段代码的重复执行的最有效的工具。阐明它的一般想法的一个好例子是众所周知的一个漫画,一个人拿着他自己的照片,在照片种他要年轻几岁。照片中的人依次拿着同样的照片(照片中的人拿着同样的照片 ...) 一直到“降到最底部”、照片中的人是个婴儿。
可以使用递归来以非常幽雅的方式描述结构和处理过程二者[Roberts (1986), Burge (1975)]。唯一的要求是我们可以对其使用递归的、事物的某种程度上的规律性。这种规律性必须包含在元素和它们的内部连接的本质中,它引发直接或间接的线性、层次、或网络结构。结构的递归描述是常见的。上面提及的照片就是一个例子。在科学和自然中很多结构是高度递归性的,通过它们的成长过程来探索某种规律性,在计算机科学同样业富于这种描述。考虑把二叉树规定作为“一个叶子或一对二叉树”;或把表规定为“一个空表或跟随着一个表的一个元素”。表的递归本质很容易的映射到通常处理它们的递归方式上。在图表 2.13 中展示了一种适用的情况。这里递归的调用 cdr 函数来“走”到我们感兴趣的元素那里。处理过程的递归描述在日常生活中也是很流行的。儿童的故事通常是高度递归的,而且这种描述好象非常容易理解和非常有趣(在那个年纪)。我们将查看下面的简单的例子。这种故事也非常容易“产生”(讲述)而很快变得无聊。
用数学归纳法证明的处理过程与递归描述密切相关。我们可以很非正式的把递归定义为把一个问题简化成在结构上是一致的、并某种程度上更加容易解决的一个或多个子问题的处理过程。这种想法可以接着应用到每个子问题,直到整个处理过程直到子问题的解决变得明显的的某个层次。我们可以接着“递归回溯”子问题链来把所有东西再次合到一起,所以对最初问题的解决将被构造出来。Hofstadter (1983) 给出了这种处理过程的愚蠢但有启迪意义的一个例子: “你如何做有 13 个石块的一个堆? - 放置一个石块在有 12 个石块的一个堆上。你如何做有 12 个石块的一个堆上? ..."。我们将把任务的完成留做读者的练习。当然我们必须总是小心的提供某种方式,能够预期这种描述或处理过程终止。想象一下执行了下列程序之后会发生什么 [Dybvig (1987), 37] (它的名字告诉我们不要尝试它)。 (define GoodBye (lambda () (display ".") (GoodBye)))
(GoodBye)
可以用迭代表示的所有东西都可以捕获到等价的递归公式中。与其他语言不同的是在 Scheme 中使用递归经常没有存储空间的处罚,因为解释器将自动的尝试把递归描述转换到一个迭代循环结构中。这总是由所谓的“尾部递归”来保证。如果在递归调用点上的一个过程的值是这个递归调用的值,则这个调用是尾部递归的。但是,如果在把这个结果“向上”传递回递归链之前对它进行了某些操作,则尾部递归不成立[Dybvig (1987), 71]。在需要对表元素的操作的重复性应用的时候,还有在其他通常使用重复结构(比如 do, for, while)的上下文中,尾部递归发生的非常频繁。因此 Scheme 的迭代过程只是充当语法糖衣。
在 Scheme 中递归最通常用于表处理。考虑一个程序,写众所周知的“Hairy MacLary”类型的儿童故事的[Dodd (undated)]一个变种。在这些故事中 Hairy McLary(一个 terrier 狗)出去散步并沿途“积累”朋友,所以在每个门口唱诵的名字表将逐渐变长。 (define StoryTeller
(lambda (mainCharacter someFriends)
(define chorus
(lambda (someFriends caravan)
; 检查终止
(if (null? someFriends)
#f
(begin
; 问候一个朋友
(DisplayLine (car someFriends))
; 并“罗列”所有已经加入的人
(display "with ")
(map (lambda (aFriend)
(DisplayLine aFriend)
(display "and "))
caravan)
(DisplayLine "...")
; 在另一个朋友上递归(并让这个人
; 加入 "caravan")
(chorus (cdr someFriends)
(append
caravan
; needs a list here !
(list (car someFriends))) ))) ))
; "StoryTeller" 函数的函数体
(DisplayLine "out of the gate and off for a walk went")
(DisplayLine mainCharacter " ...")
(newline)
(chorus someFriends (list mainCharacter))
; we will skip some more friends here, and also their
; encounter with Scarface Claw (the toughest Tom in town)
(newline)
(display "straight back home to bed") ))
;StoryTeller
在做调用带有如下数据的时候,这个尾部递归过程生成下面的熟悉的故事: (StoryTeller
"Hairy MacLary from Donaldson's Dairy"
'("Hercules Morse, as big as a horse"
"Bottomley Potts, covered in spots"
"Muffin McLay, like a bundle of hay"))
; out of the gate and off for a walk went
; Hairy MacLary from Donaldson's Dairy ...
; Hercules Morse, as big as a horse
; with Hairy MacLary from Donaldson's Dairy
; and ...
; Bottomley Potts, covered in spots
; with Hercules Morse, as big as a horse
; and Hairy MacLary from Donaldson's Dairy
; and ...
; Muffin McLay, like a bundle of hay
; with Bottomley Potts, covered in spots
; and Hercules Morse, as big as a horse
; and Hairy MacLary from Donaldson's Dairy
; and ...
; straight back home to bed#t
上面的程序是尾部递归的,因为我们在后续对 chorus 的调用返回之后不尝试更改它生成的任何数据。它还对展示非尾部递归的过程是有教益的。
这次我们的朋友们攒钱买一个新沙发,他们定时地希望总计他们的钱看是否已经买得起了。下面的过程做这个把戏,假定我们列出朋友和有关的财产。
|