5月17日总结

发布时间 2023-05-22 00:06:29作者: lmyyyy

求值器完整实现代码我已经上传到了GitHub仓库:TinySCM,感兴趣的童鞋可以前往查看。这里顺便强烈推荐UC Berkeley的同名课程CS 61A。

即使在变化中,它也丝毫未变。

——赫拉克利特

吾犹昔人,非昔人也。

——僧肇

绪论

在上一篇博客《SICP:元循环求值器(Python实现)》中,我们介绍了用Python对来实现一个Scheme求值器。然而,我们跳过了部分特殊形式(special forms)和基本过程(primitive procedures)实现的介绍,如特殊形式中的delay、cons-stream,基本过程中的force、streawn-car、stream-map等。事实上,以上特殊形式和基本过程都和惰性求值与流相关。这篇博客我们将介绍如何用Python来实现Scheme中的惰性求值和流,并使用惰性求值的原理来为我们的Scheme解释器增添尾递归的支持。
1 Scheme中的流简介

所谓流,一言以蔽之,就是使用了惰性求值技术的表。它初始化时并没有完全生成,而是能够动态地按需构造,从而同时提升程序的计算和存储效率。

我们先来比较两个程序,它们都计算出一个区间里的素数之和。其中第一个程序用标准的迭代(尾递归)风格写出:

(define (sum-primes a b)
(define (iter count accum)
(cond ((> count b) accum)
((prime? count) (iter (+ count 1) (+ count accum)))
(else (iter (+ count 1) accum))))
(iter a 0))

第二个程序完成同样的计算,其中使用了我们在博客《SICP: 层次性数据和闭包性质(Python实现)》中所介绍过的序列操作:

(define (sum-primes a b)
(reduce +
(filter prime? (enumerate-interval a b))))

在执行计算时,第一个程序只需要维持正在累加的和;而对于第二个程序而言,只有等enumerate-interval构造完这一区间所有整数的表后,filter才能开始工作,而且等过滤区工作完后还得将结果表传给reduce得到求和。显然,第一个程序完全不需要像第二个程序这么大的中间存储。

以上情况还不是最极端的,最极端的情况是下面这种,我们枚举并过滤出了10000到1000000内的所有素数,但实际上只取第二个:

(car (cdr (filter prime?
(enumerate-interval 10000 1000000))))

这程序槽点很多,首先要构造与一个大约包含了一百万个整数的表,然后再通过过滤整个表的方式去检查每个元素是否是素数,而后只取第二个,几乎抛弃了全部结果,这在时间和空间上都是极大的浪费。在更传统的程序设计风格中,我们完全可以交错进行枚举和过滤,并在找到第二个素数时立即停下来。

流是一种非常巧妙的想法,使我们在保留各种序列操作的同时,不会带来将序列作为表去操作引起的代价(时间上和空间上的)。从表面上看,流也是就是表,但对它们进行操作的过程名字不同。对于流而言有构造函数cons-stream,还有两个选择函数stream-cdr和stream-cdr,它们对任意的变量x和y都满足如下的约束条件:

scm> (equal? (stream-car (cons-stream x y)) x)

t

scm> (equal? (stream-cdr (cons-stream x