提交 9142d633 编写于 作者: W wizardforcel

2023-03-30 16:48:47

上级 00d5ff65
......@@ -8,7 +8,7 @@
尽管计算机很强大,但它是一个苛刻的监工。它的程序必须是正确的,我们想说的话必须在每个细节上都准确无误。就像在每一个其他的象征性活动中一样,我们通过争论确信程序的真实性。Lisp 本身可以被赋予一个语义(顺便说一下,另一个模型),如果一个程序的功能可以被指定,比方说,在谓词演算中,逻辑的证明方法可以被用来进行一个可接受的正确性论证。不幸的是,随着程序变得越来越大和复杂,就像他们经常做的那样,规范本身的充分性、一致性和正确性变得值得怀疑,因此完整的正确性的形式论证很少伴随着大型程序。因为大程序是从小程序发展而来的,所以我们开发一个标准程序结构的武器库是至关重要的,我们已经确定了这些标准程序结构的正确性——我们称之为习惯用法——并学会使用有价值的组织技术将它们组合成更大的结构。这些技术将在本书中详细讨论,理解它们对于参与名为编程的创新事业是必不可少的。最重要的是,强大的组织技术的发现和掌握加速了我们创建大型重要项目的能力。相反,由于编写大型程序非常费力,我们被刺激去发明新的方法来减少大量的功能和细节以适应大型程序。
与程序不同,计算机必须遵守物理定律。如果它们希望快速运行——每个状态变化几纳秒——它们必须只将电子传输很短的距离(最多![f3-fig-5001.jpg](../images/f3-fig-5001.jpg)英尺)。空间中如此集中的大量设备所产生的热量必须被移除。一门精致的工程艺术在功能多样性和设备密度之间取得了平衡。无论如何,硬件总是在比我们关心的编程水平更原始的水平上运行。将我们的 Lisp 程序转换成“机器”程序的过程本身就是我们编程的抽象模型。他们的研究和创造给了与编程任意模型相关的组织程序大量的洞察力。当然,计算机本身也可以这样建模。想想看:最小的物理开关元件的行为是通过微分方程描述的量子力学来模拟的,微分方程的详细行为是通过在计算机上执行的计算机程序中表示的数值近似来捕捉的,计算机程序由以下组成。。。!
与程序不同,计算机必须遵守物理定律。如果它们希望快速运行——每个状态变化几纳秒——它们必须只将电子传输很短的距离(最多 1.5 英尺)。空间中如此集中的大量设备所产生的热量必须被移除。一门精致的工程艺术在功能多样性和设备密度之间取得了平衡。无论如何,硬件总是在比我们关心的编程水平更原始的水平上运行。将我们的 Lisp 程序转换成“机器”程序的过程本身就是我们编程的抽象模型。他们的研究和创造给了与编程任意模型相关的组织程序大量的洞察力。当然,计算机本身也可以这样建模。想想看:最小的物理开关元件的行为是通过微分方程描述的量子力学来模拟的,微分方程的详细行为是通过在计算机上执行的计算机程序中表示的数值近似来捕捉的,计算机程序由以下组成。。。!
分开确定这三个焦点不仅仅是战术上的方便。尽管,正如他们所说的,这都是头脑中的,这种逻辑上的分离导致了这些焦点之间符号交通的加速,这些焦点的丰富性、活力和潜力在人类经验中只有生命本身的进化才能超越。在最好的情况下,焦点之间的关系是亚稳态的。计算机总是不够大或不够快。硬件技术的每一次突破都导致更大规模的编程企业、新的组织原则和抽象模型的丰富。每个读者都应该定期问自己“向什么目标前进,向什么目标前进?”—但是不要问得太频繁,以免你因为苦乐参半的哲学的便秘而错过编程的乐趣。
......
......@@ -6,7 +6,7 @@
> 有没有可能软件不像其他任何东西,它注定要被抛弃:它的全部意义在于总是把它看成一个肥皂泡?
>
> —艾伦·J·佩利
> —艾伦·J·佩利
自 1980 年以来,本书中的材料一直是麻省理工学院入门级计算机科学学科的基础。当第一版出版时,我们已经教了四年这种材料,直到第二版的出现,又过了十二年。我们感到高兴的是,我们的工作已被广泛采纳并纳入其他案文。我们已经看到我们的学生接受了这本书里的思想和程序,并把它们作为新的计算机系统和语言的核心。从字面上理解一句古老的犹太法典双关语,我们的学生已经成为我们的建设者。我们很幸运有这样有能力的学生和这样有成就的建设者。
......@@ -24,7 +24,7 @@
> 电脑就像小提琴。你可以想象一个新手先试留声机,再试小提琴。他说,后者听起来很可怕。这是我们从人文主义者和大多数计算机科学家那里听到的观点。他们说,对于特定的目的来说,计算机程序是好的,但是它们不灵活。小提琴和打字机都不是,除非你学会如何使用它们。
>
> —马文·明斯基,“为什么编程是表达理解不佳、表述不严谨的想法的好媒介”
> —马文·明斯基,“为什么编程是表达理解不佳、表述不严谨的想法的好媒介”
“计算机程序的结构和解释”是麻省理工学院计算机科学的入门课程。这是麻省理工学院所有电子工程或计算机科学专业学生的必修课,占“公共核心课程”的四分之一,其中还包括两个关于电路和线性系统的科目以及一个关于数字系统设计的科目。自 1978 年以来,我们一直参与这一学科的发展,自 1980 年秋季以来,我们每年向 600 至 700 名学生教授这一材料的现有形式。这些学生中的大部分以前很少或没有接受过正式的计算培训,尽管许多人玩过一点计算机,少数人有丰富的编程或硬件设计经验。
......@@ -40,4 +40,4 @@
Scheme 是我们使用的 Lisp 方言,它试图将 Lisp 和 Algol 的强大和优雅结合在一起。从 Lisp 中,我们获得了源自简单语法的元语言能力,程序作为数据对象的统一表示,以及垃圾收集堆分配的数据。从 Algol 中,我们得到了词法范围和块结构,这是 Algol 委员会中编程语言设计先驱们的礼物。我们希望引用约翰·雷诺兹和彼得·兰丁对 Church 的 lambda 演算与编程语言结构之间关系的见解。我们也认识到我们对数学家的亏欠,他们在计算机出现的几十年前就探索了这个领域。这些先驱包括阿隆佐·邱奇、巴克利·罗瑟、斯蒂芬·克莱尼和哈斯克尔·库里。
—哈罗德·艾贝尔森和杰拉德·让伊·萨斯曼
\ No newline at end of file
——哈罗德·艾贝尔森和杰拉德·让伊·萨斯曼
\ No newline at end of file
......@@ -4,7 +4,7 @@
《计算机程序的结构和解释 (SICP JS)的 JavaScript 改编是在新加坡国立大学(NUS)为课程 CS1101S 开发的。该课程由 Low Kok Lim 共同教授了六年,他良好的教学判断对该课程和该项目的成功至关重要。CS1101S 教学团队包括许多新加坡国立大学的同事和 300 多名本科生助教。他们在过去九年里不断的反馈让我们解决了无数特定于 JavaScript 的问题,消除了不必要的复杂性,同时保留了 SICP 和 JavaScript 的基本特性。
SICP JS 是一个软件项目,也是一个图书项目。我们在 2008 年从原作者处获得了![f6-fig-5001.jpg](../images/f6-fig-5001.jpg)图书来源。早期的 SICP JS 工具链是由刘航开发,冯飘飘完善的。陈·何岸开发子印刷版本的第一批工具,乔林·谭在此基础上开发子第一版电子书的工具,心悦和王千将这些工具重新用于当前的比较版。Samuel Fang 设计并开发了 SICP JS 的网络版。
SICP JS 是一个软件项目,也是一个图书项目。我们在 2008 年从原作者处获得了![f6-fig-5001.jpg](img/f6-fig-5001.jpg)图书来源。早期的 SICP JS 工具链是由刘航开发,冯飘飘完善的。陈·何岸开发子印刷版本的第一批工具,乔林·谭在此基础上开发子第一版电子书的工具,心悦和王千将这些工具重新用于当前的比较版。Samuel Fang 设计并开发了 SICP JS 的网络版。
SICP JS 和 CS1101S 的网络版很大程度上依赖于一个叫做源码学院的软件系统,它支持的 JavaScript 子语言叫做源码。在 SICP JS 的准备过程中,许多学生对源学院做出了贡献,系统将他们突出地列为“贡献者”自 2020 年以来,新加坡国立大学课程 CS4215“编程语言实现”的学生贡献了几个在 SICP JS 中使用的编程语言实现:3.4 节中使用的源代码的并发版本由郑群·库和乔纳森·陈开发;第 4.2 节中使用的惰性实现是由杰卢利·艾哈迈德、伊恩·肯德尔·邓肯、克鲁兹·乔马里·埃万格利斯塔和奥尔登·谭开发的;第 4.3 节中使用的不确定性实现是由阿尔萨兰·吉玛和阿努巴夫开发的;Daryl Tan 帮助将这些实现整合到学院中。
......@@ -44,7 +44,7 @@ Al Moyé安排我们将这些材料教授给惠普公司的工程师,并制作
丹·弗里德曼(Dan Friedman)是该计划社区的长期领导者。该社区更广泛的工作超越了语言设计问题,包括重大的教育创新,如基于 Schemer's Inc .的 EdScheme 的高中课程,以及 Mike Eisenberg、Brian Harvey 和 Matthew Wright 的精彩书籍。
我们感谢那些为使这本书成为一本真正的书做出贡献的人,特别是特里·艾林、拉里·科恩和麻省理工学院出版社的保罗·贝奇。艾拉·马泽尔找到了精彩的封面图片。对于第二版,我们特别感谢伯纳德和艾拉·马泽尔对书籍设计的帮助,以及非凡的巫师大卫·琼斯![f6-fig-5002.jpg](../images/f6-fig-5002.jpg)。我们也非常感谢那些对新草稿发表深刻评论的读者:雅各布·卡泽尼尔森、哈迪·梅尔、吉姆·米勒,特别是布莱恩·哈维,他对这本书的贡献就像朱莉对他的书《简单计划》的贡献一样。
我们感谢那些为使这本书成为一本真正的书做出贡献的人,特别是特里·艾林、拉里·科恩和麻省理工学院出版社的保罗·贝奇。艾拉·马泽尔找到了精彩的封面图片。对于第二版,我们特别感谢伯纳德和艾拉·马泽尔对书籍设计的帮助,以及非凡的巫师大卫·琼斯![f6-fig-5002.jpg](img/f6-fig-5002.jpg)。我们也非常感谢那些对新草稿发表深刻评论的读者:雅各布·卡泽尼尔森、哈迪·梅尔、吉姆·米勒,特别是布莱恩·哈维,他对这本书的贡献就像朱莉对他的书《简单计划》的贡献一样。
最后,我们要感谢多年来鼓励这项工作的各组织的支持,包括由 Ira Goldstein 和 Joel Birnbaum 促成的惠普公司的支持,以及由鲍勃·卡恩促成的 DARPA 的支持。
......
......@@ -185,7 +185,7 @@ circumference;
要求评估规则应用于四种不同的组合。我们可以通过用树的形式表示这种组合来获得这个过程的图片,如图[图 1.1](#c1-fig-0001) 所示。每个组合都由一个节点表示,节点的分支对应于该组合的运算符和操作数。终端节点(即没有分支的节点)代表运算符或数字。从树的角度来看求值,我们可以想象操作数的值向上渗透,从终端节点开始,然后在越来越高的级别上组合。总的来说,我们将会看到递归是一种非常强大的处理层次化、树状对象的技术。事实上,评估规则的“向上过滤值”形式是一种被称为树累积的通用过程的示例。
![c1-fig-0001.jpg](../images/c1-fig-0001.jpg)
![c1-fig-0001.jpg](img/c1-fig-0001.jpg)
[图 1.1](#c1-fig-0001a) 树形表示,显示每个子表达式的值。
......@@ -220,7 +220,7 @@ function square(x) {
我们可以这样理解:
![c1-fig-5001.jpg](../images/c1-fig-5001.jpg)
![c1-fig-5001.jpg](img/c1-fig-5001.jpg)
我们这里有一个复合函数,它被命名为`square`。函数表示将某物乘以自身的运算。要相乘的东西被赋予了一个本地名`x`,这个名字的作用相当于代词在自然语言中的作用。评估声明创建了这个复合函数,并将其与名称`square`相关联。 [](#c1-fn-0006)
......@@ -394,7 +394,7 @@ JavaScript 使用应用顺序求值,部分原因是因为避免了对表达式
我们在这一点上可以定义的函数类的表达能力是非常有限的,因为我们没有办法进行测试,也没有办法根据测试的结果执行不同的操作。例如,我们不能声明一个函数,它通过测试一个数是否为非负来计算这个数的绝对值,并根据规则在每种情况下采取不同的操作
![c1-fig-5002.jpg](../images/c1-fig-5002.jpg)
![c1-fig-5002.jpg](img/c1-fig-5002.jpg)
这个构造是一个案例分析,可以使用一个条件表达式用 JavaScript 写成
......@@ -418,7 +418,7 @@ predicate ? consequent-expression : alternative-expression
如果我们喜欢单独处理零的情况,我们可以通过编写来指定计算一个数的绝对值的函数
![c1-fig-5003.jpg](../images/c1-fig-5003.jpg)
![c1-fig-5003.jpg](img/c1-fig-5003.jpg)
在 JavaScript 中,我们通过将条件表达式作为替代表达式嵌套在其他条件表达式中来表达具有多个案例的案例分析:
......@@ -541,7 +541,7 @@ a === 4
将下面的表达式翻译成 JavaScript
![c1-fig-5004.jpg](../images/c1-fig-5004.jpg)
![c1-fig-5004.jpg](img/c1-fig-5004.jpg)
##### 练习 1.3
......@@ -603,7 +603,7 @@ function sqrt(x) {
如何计算平方根?最常见的方法是使用牛顿的逐次逼近法,即每当我们对一个数 x 的平方根值有一个猜测 y 时,我们可以通过对 y 与 x / y 求平均值来执行一个简单的操作,以获得一个更好的猜测值(更接近实际的平方根)。 [^(18)](#c1-fn-0018) 例如,我们可以如下计算 2 的平方根。假设我们最初的猜测是 1:
![c1-fig-5006.jpg](../images/c1-fig-5006.jpg)
![c1-fig-5006.jpg](img/c1-fig-5006.jpg)
继续这个过程,我们获得了对平方根越来越好的近似。
......@@ -707,7 +707,7 @@ function sqrt_iter(guess, x) {
牛顿的立方根方法基于以下事实:如果 y 是 x 的立方根的近似值,则该值给出了更好的近似值
![c1-fig-5007.jpg](../images/c1-fig-5007.jpg)
![c1-fig-5007.jpg](img/c1-fig-5007.jpg)
使用此公式实现类似于平方根函数的立方根函数。(在 1.3.4 节中,我们将看到如何实现牛顿法作为这些平方根和立方根函数的抽象。)
......@@ -717,7 +717,7 @@ function sqrt_iter(guess, x) {
请注意,计算平方根的问题自然会分解成许多子问题:如何判断猜测是否足够好,如何改进猜测,等等。这些任务中的每一项都由单独的功能来完成。整个`sqrt`程序可以被视为一簇功能(如图[图 1.2](#c1-fig-0008) 所示),反映了问题分解成子问题。
![c1-fig-0002.jpg](../images/c1-fig-0002.jpg)
![c1-fig-0002.jpg](img/c1-fig-0002.jpg)
[图 1.2](#c1-fig-0008a)`sqrt`程序的功能分解。
......@@ -867,7 +867,7 @@ function factorial(n) {
我们可以用 1.1.5 节的替代模型来观看动作计算 6 中的这个函数!,如图[图 1.3](#c1-fig-0009) 所示。
![c1-fig-0003.jpg](../images/c1-fig-0003.jpg)
![c1-fig-0003.jpg](img/c1-fig-0003.jpg)
[图 1.3](#c1-fig-0009a) 一个用于计算 6 的线性递归过程!。
......@@ -897,7 +897,7 @@ function fact_iter(product, counter, max_count) {
和前面一样,我们可以用代入模型来形象化计算 6 的过程!,如图[图 1.4](#c1-fig-0010) 所示。
![c1-fig-0004.jpg](../images/c1-fig-0004.jpg)
![c1-fig-0004.jpg](img/c1-fig-0004.jpg)
[图 1.4](#c1-fig-0010a) 用于计算 6 的线性迭代过程!。
......@@ -984,7 +984,7 @@ function k(n) {
一般来说,斐波纳契数可以由规则来定义
![c1-fig-5008.jpg](../images/c1-fig-5008.jpg)
![c1-fig-5008.jpg](img/c1-fig-5008.jpg)
我们可以立即将这个定义转化为计算斐波纳契数的递归函数:
......@@ -1000,11 +1000,11 @@ function fib(n) {
考虑一下这种计算的模式。为了计算`fib(5)`,我们计算`fib(4)``fib(3)`。为了计算`fib(4)`,我们计算`fib(3)``fib(2)`。一般来说,演化后的流程看起来像一棵树,如图[图 1.5](#c1-fig-0012) 所示。请注意,分支在每一层都分裂成两个(底部除外);这反映了一个事实,即`fib`函数每次被调用时都会调用自己两次。
![c1-fig-0005.jpg](../images/c1-fig-0005.jpg)
![c1-fig-0005.jpg](img/c1-fig-0005.jpg)
[图 1.5](#c1-fig-0012a) 计算`fib(5)`中生成的树递归过程。
这个函数作为一个典型的树递归是有指导意义的,但是它是一个计算斐波那契数的糟糕方法,因为它做了太多多余的计算。请注意图 1.5 中的[](#c1-fig-0012)中的`fib(3)`的整个计算——几乎一半的工作——都是重复的。事实上,不难看出,函数将计算`fib(1)``fib(0)`的次数(一般来说是上述树中的叶子数)恰恰是 Fib( n + 1)。为了了解这有多糟糕,我们可以展示 Fib( n )的值随着 n 呈指数增长。更准确地说(见练习 1.13),Fib( n )是最接近 ϕ ^n / ![c1-fig-5009.jpg](../images/c1-fig-5009.jpg)的整数,其中
这个函数作为一个典型的树递归是有指导意义的,但是它是一个计算斐波那契数的糟糕方法,因为它做了太多多余的计算。请注意图 1.5 中的[](#c1-fig-0012)中的`fib(3)`的整个计算——几乎一半的工作——都是重复的。事实上,不难看出,函数将计算`fib(1)``fib(0)`的次数(一般来说是上述树中的叶子数)恰恰是 Fib( n + 1)。为了了解这有多糟糕,我们可以展示 Fib( n )的值随着 n 呈指数增长。更准确地说(见练习 1.13),Fib( n )是最接近 ϕ ^n / ![c1-fig-5009.jpg](img/c1-fig-5009.jpg)的整数,其中
```
ϕ = (1 + )/2 ≈ 1.6180
......@@ -1104,13 +1104,13 @@ count_change(100);
下面这个数字的模式叫做帕斯卡三角形。
![c1-fig-5010.jpg](../images/c1-fig-5010.jpg)
![c1-fig-5010.jpg](img/c1-fig-5010.jpg)
三角形边上的数字都是 1,三角形里面的每个数字都是它上面两个数字之和。写一个函数,通过递归过程计算帕斯卡三角形的元素。
##### 练习 1.13
证明 Fib( n )是最接近ϕ^n/![c1-fig-5009.jpg](../images/c1-fig-5009.jpg)的整数,其中 ϕ = (1 + ![c1-fig-5009.jpg](../images/c1-fig-5009.jpg) )/2。提示:用归纳法和斐波那契数列的定义证明 fib(n)=(ϕ^n–ψ^n)/![c1-fig-5009.jpg](../images/c1-fig-5009.jpg),其中ψ=(1—![c1-fig-5009.jpg](../images/c1-fig-5009.jpg))/2。
证明 Fib( n )是最接近ϕ^n/![c1-fig-5009.jpg](img/c1-fig-5009.jpg)的整数,其中 ϕ = (1 + ![c1-fig-5009.jpg](img/c1-fig-5009.jpg) )/2。提示:用归纳法和斐波那契数列的定义证明 fib(n)=(ϕ^n–ψ^n)/![c1-fig-5009.jpg](img/c1-fig-5009.jpg),其中ψ=(1—![c1-fig-5009.jpg](img/c1-fig-5009.jpg))/2。
### 1.2.3 订单增长
......@@ -1138,7 +1138,7 @@ k1 f (n) ≤ R(n) ≤ k2 f (n)
如果 x 足够小,角度的正弦(以弧度表示)可以通过利用近似 sin x ≈ x 和三角恒等式来计算
![c1-fig-5011.jpg](../images/c1-fig-5011.jpg)
![c1-fig-5011.jpg](img/c1-fig-5011.jpg)
来减少罪的论点的篇幅。(在本练习中,如果角度的大小不大于 0.1 弧度,则认为该角度“足够小”。)这些想法被合并到以下功能中:
......@@ -1325,7 +1325,7 @@ function gcd(a, b) {
* * *
我们可以用这个定理来得到欧几里德算法的增长阶估计。设 n 为函数的两个输入中较小的一个。如果流程需要 k 步,那么我们必须有 n≥fib(k)≈ϕ^k/![c1-fig-5009.jpg](../images/c1-fig-5009.jpg)。因此步数 k 随着 n 的对数(以 ϕ 为底)增长。因此,增长的顺序是θ(logn)。
我们可以用这个定理来得到欧几里德算法的增长阶估计。设 n 为函数的两个输入中较小的一个。如果流程需要 k 步,那么我们必须有 n≥fib(k)≈ϕ^k/![c1-fig-5009.jpg](img/c1-fig-5009.jpg)。因此步数 k 随着 n 的对数(以 ϕ 为底)增长。因此,增长的顺序是θ(logn)。
##### 练习 1.20
......@@ -1333,7 +1333,7 @@ function gcd(a, b) {
### 示例:测试素性
本节描述了检查整数 n 的素性的两种方法,一种是使用增长顺序θ(![c1-fig-5012.jpg](../images/c1-fig-5012.jpg)),另一种是使用增长顺序θ(logn)的“概率”算法。本节末尾的练习提出了基于这些算法的编程项目。
本节描述了检查整数 n 的素性的两种方法,一种是使用增长顺序θ(![c1-fig-5012.jpg](img/c1-fig-5012.jpg)),另一种是使用增长顺序θ(logn)的“概率”算法。本节末尾的练习提出了基于这些算法的编程项目。
##### 寻找约数
......@@ -1363,7 +1363,7 @@ function is_prime(n) {
}
```
`find_divisor`的最终测试是基于这样的事实:如果 n 不是质数,那么它必须有一个小于或等于![c1-fig-5012.jpg](../images/c1-fig-5012.jpg)的除数。 [^( 42 )](#c1-fn-0042) 这意味着算法只需要测试 1 和![c1-fig-5012.jpg](../images/c1-fig-5012.jpg)之间的除数。因此,将 n 识别为素数所需的步骤数将具有增长顺序θ(![c1-fig-5012.jpg](../images/c1-fig-5012.jpg))。
`find_divisor`的最终测试是基于这样的事实:如果 n 不是质数,那么它必须有一个小于或等于![c1-fig-5012.jpg](img/c1-fig-5012.jpg)的除数。 [^( 42 )](#c1-fn-0042) 这意味着算法只需要测试 1 和![c1-fig-5012.jpg](img/c1-fig-5012.jpg)之间的除数。因此,将 n 识别为素数所需的步骤数将具有增长顺序θ(![c1-fig-5012.jpg](img/c1-fig-5012.jpg))。
##### 费马试验
......@@ -1448,7 +1448,7 @@ function report_prime(elapsed_time) {
}
```
使用这个函数,写一个函数`search_for_primes`,检查指定范围内连续奇数整数的素性。用你的函数求大于 1000 的三个最小素数;大于 10000;大于 10 万;大于 100 万。注意测试每个素数所需的时间。因为测试算法的增长顺序是θ(![c1-fig-5012.jpg](../images/c1-fig-5012.jpg)),所以你应该预计测试 10,000 左右的素数所需的时间大约是测试 1000 左右的素数所需的时间的![c1-fig-5013.jpg](../images/c1-fig-5013.jpg)倍。你的计时数据证实了这一点吗?10 万和 100 万的数据在多大程度上支持了![c1-fig-5012.jpg](../images/c1-fig-5012.jpg)的预测?你的结果是否符合你机器上的程序运行时间与计算所需的步骤数成正比的概念?
使用这个函数,写一个函数`search_for_primes`,检查指定范围内连续奇数整数的素性。用你的函数求大于 1000 的三个最小素数;大于 10000;大于 10 万;大于 100 万。注意测试每个素数所需的时间。因为测试算法的增长顺序是θ(![c1-fig-5012.jpg](img/c1-fig-5012.jpg)),所以你应该预计测试 10,000 左右的素数所需的时间大约是测试 1000 左右的素数所需的时间的![c1-fig-5013.jpg](img/c1-fig-5013.jpg)倍。你的计时数据证实了这一点吗?10 万和 100 万的数据在多大程度上支持了![c1-fig-5012.jpg](img/c1-fig-5012.jpg)的预测?你的结果是否符合你机器上的程序运行时间与计算所需的步骤数成正比的概念?
##### 练习 1.23
......@@ -1541,7 +1541,7 @@ function sum_cubes(a, b) {
第三个函数计算数列中一系列项的总和
![c1-fig-5014.jpg](../images/c1-fig-5014.jpg)
![c1-fig-5014.jpg](img/c1-fig-5014.jpg)
收敛到 π /8(非常慢): [^(49)](#c1-fn-0049)
......@@ -1565,7 +1565,7 @@ function name(a, b) {
这种常见模式的存在是一个强有力的证据,表明有一个有用的抽象正等待被呈现出来。事实上,数学家很久以前就发现了级数求和的抽象概念,并发明了“西格玛符号”,例如
![c1-fig-5016.jpg](../images/c1-fig-5016.jpg)
![c1-fig-5016.jpg](img/c1-fig-5016.jpg)
来表达这个概念。sigma 符号的强大之处在于,它允许数学家处理求和本身的概念,而不仅仅是特定的和,例如,制定与被求和的特定序列无关的和的一般结果。
......@@ -1639,7 +1639,7 @@ function pi_sum(a, b) {
一旦我们有了`sum`,我们就可以用它作为构建进一步概念的基础。例如,在极限值 a 和 b 之间的函数 f 的定积分可以使用以下公式进行数值近似
![c1-fig-5017.jpg](../images/c1-fig-5017.jpg)
![c1-fig-5017.jpg](img/c1-fig-5017.jpg)
对于 dx 的小值。我们可以将它直接表示为一个函数:
......@@ -1664,7 +1664,7 @@ integral(cube, 0, 1, 0.001);
辛普森法则是一种比上述方法更精确的数值积分方法。使用辛普森法则,函数 f 在 a 和 b 之间的积分近似为
![c1-fig-5018.jpg](../images/c1-fig-5018.jpg)
![c1-fig-5018.jpg](img/c1-fig-5018.jpg)
其中 h=(b–a)/n,对于某些偶数整数 n ,以及 y[k]=f(a+KH)。(增加 n 会增加近似的精确度。)声明一个函数,该函数将参数 f、a 、 b 和 n 作为参数,并返回使用辛普森规则计算的积分值。使用您的函数在 0 和 1 之间积分`cube`(其中 n = 100, n = 1000),并将结果与上面所示的`integral`函数的结果进行比较。
......@@ -1685,7 +1685,7 @@ function sum(term, a, next, b) {
##### 练习 1.31
1. a .`sum`函数只是大量类似抽象中最简单的一个,这些抽象可以被捕捉为高阶函数。写一个类似的函数叫做`product`,它返回一个函数在给定范围内各点的值的乘积。演示如何根据`product`定义`factorial`。同样使用`product`计算 π 的近似值,使用公式[^(52)](#c1-fn-0052)![c1-fig-5019.jpg](../images/c1-fig-5019.jpg)
1. a .`sum`函数只是大量类似抽象中最简单的一个,这些抽象可以被捕捉为高阶函数。写一个类似的函数叫做`product`,它返回一个函数在给定范围内各点的值的乘积。演示如何根据`product`定义`factorial`。同样使用`product`计算 π 的近似值,使用公式[^(52)](#c1-fn-0052)![c1-fig-5019.jpg](img/c1-fig-5019.jpg)
2. b. 如果你的`product`函数生成了一个递归过程,那就写一个生成迭代过程的。如果它生成一个迭代过程,那么就写一个生成递归过程的程序。
......@@ -1768,7 +1768,7 @@ const plus4 = x => x + 4;
我们可以如下阅读 lambda 表达式:
![c1-fig-5020.jpg](../images/c1-fig-5020.jpg)
![c1-fig-5020.jpg](img/c1-fig-5020.jpg)
像任何以函数作为其值的表达式一样,lambda 表达式可以用作应用程序中的函数表达式,例如
......@@ -2018,7 +2018,7 @@ function sqrt(x) {
不幸的是,这种定点搜索并不收敛。考虑一个最初的猜测 yT2【1】。下一个猜测是 y[2]=x/y[1]下一个猜测是 y[3]=x/y[2]=x/(x/y 这导致了一个无限循环,其中两个猜测 y1 和 y2 一遍又一遍地重复,围绕着答案振荡。
控制这种波动的一个方法是防止猜测变化太大。由于答案总是在我们的猜测 y 和 x / y 之间,所以我们可以通过将 y 与 x / y 进行平均来做出一个新的猜测,这个新的猜测距离 y 没有距离 x / y 远,这样在 y 之后的下一个猜测就是![c1-fig-5021.jpg](../images/c1-fig-5021.jpg) ( 【T21 做出这样一系列猜测的过程,简单来说就是寻找 y![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)![c1-fig-5021.jpg](../images/c1-fig-5021.jpg)(y+x/y)不动点的过程:
控制这种波动的一个方法是防止猜测变化太大。由于答案总是在我们的猜测 y 和 x / y 之间,所以我们可以通过将 y 与 x / y 进行平均来做出一个新的猜测,这个新的猜测距离 y 没有距离 x / y 远,这样在 y 之后的下一个猜测就是![c1-fig-5021.jpg](img/c1-fig-5021.jpg) ( 【T21 做出这样一系列猜测的过程,简单来说就是寻找 y![c1-fig-5022.jpg](img/c1-fig-5022.jpg)![c1-fig-5021.jpg](img/c1-fig-5021.jpg)(y+x/y)不动点的过程:
```
function sqrt(x) {
......@@ -2026,7 +2026,7 @@ function sqrt(x) {
}
```
(注意 y=![c1-fig-5021.jpg](../images/c1-fig-5021.jpg)(y+x/y)是方程 y=x/y 的简单变换;要导出它,将 y 加到等式的两边,然后除以 2。)
(注意 y=![c1-fig-5021.jpg](img/c1-fig-5021.jpg)(y+x/y)是方程 y=x/y 的简单变换;要导出它,将 y 加到等式的两边,然后除以 2。)
通过这种修改,平方根函数可以工作。事实上,如果我们解开定义,我们可以看到这里生成的平方根的近似序列与我们在 1.1.7 节中的原始平方根函数生成的序列完全相同。这种将逐次逼近平均到一个解的方法,我们称之为平均阻尼的技术,通常有助于定点搜索的收敛。
......@@ -2042,11 +2042,11 @@ function sqrt(x) {
一个无限的连分数是以下形式的表达式
![c1-fig-5023.jpg](../images/c1-fig-5023.jpg)
![c1-fig-5023.jpg](img/c1-fig-5023.jpg)
作为一个例子,一个人可以表明,在 n[I]t31】和 d[I]t35】都等于 1 的情况下,无限连分数展开产生 1/ ϕ ,其中 ϕ 是黄金分割比例(在 1.2.2 节中描述)。逼近无穷连分数的一种方法是在给定项数后截断展开式。这种截断——所谓的 k 项有限连分式——具有以下形式
![c1-fig-5024.jpg](../images/c1-fig-5024.jpg)
![c1-fig-5024.jpg](img/c1-fig-5024.jpg)
1. a. Suppose that `n` and `d` are functions of one argument (the term index i) that return the N[i] and D[i] of the terms of the continued fraction. Declare a function `cont_frac` such that evaluating `cont_frac(n, d, k)` computes the value of the k-term finite continued fraction. Check your function by approximating 1ϕ using
......@@ -2066,7 +2066,7 @@ function sqrt(x) {
德国数学家 J.H. Lambert 在 1770 年发表了正切函数的连分式表示:
![c1-fig-5025.jpg](../images/c1-fig-5025.jpg)
![c1-fig-5025.jpg](img/c1-fig-5025.jpg)
其中 x 以弧度为单位。声明一个函数`tan_cf(x, k)`,它根据 Lambert 公式计算正切函数的近似值。和练习 1.37 一样,`k`指定了要计算的项数。
......@@ -2074,7 +2074,7 @@ function sqrt(x) {
上面的例子展示了将函数作为参数传递的能力如何显著增强了我们的编程语言的表达能力。通过创建返回值本身就是函数的函数,我们可以获得更强的表达能力。
我们可以通过再次查看 1.3.3 节末尾描述的定点例子来说明这一点。我们将平方根函数的新版本公式化为定点搜索,从观察到![c1-fig-5005.jpg](../images/c1-fig-5005.jpg)是函数 y![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)x/y 的定点开始。然后我们使用平均阻尼使近似收敛。平均阻尼本身是一种有用的通用技术。也就是说,给定一个函数 f,我们考虑这个函数在 x 的值等于 x 和 f ( x )的平均值。
我们可以通过再次查看 1.3.3 节末尾描述的定点例子来说明这一点。我们将平方根函数的新版本公式化为定点搜索,从观察到![c1-fig-5005.jpg](img/c1-fig-5005.jpg)是函数 y![c1-fig-5022.jpg](img/c1-fig-5022.jpg)x/y 的定点开始。然后我们使用平均阻尼使近似收敛。平均阻尼本身是一种有用的通用技术。也就是说,给定一个函数 f,我们考虑这个函数在 x 的值等于 x 和 f ( x )的平均值。
我们可以用下面的函数来表达平均阻尼的概念:
......@@ -2109,15 +2109,15 @@ function cube_root(x) {
##### 牛顿方法
当我们第一次介绍平方根函数时,在 1.1.7 节中,我们提到这是牛顿法的特例。如果 x![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)g(x)是一个可微函数,那么方程 g ( x ) = 0 的一个解就是函数 x![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)f(x)的一个不动点其中
当我们第一次介绍平方根函数时,在 1.1.7 节中,我们提到这是牛顿法的特例。如果 x![c1-fig-5022.jpg](img/c1-fig-5022.jpg)g(x)是一个可微函数,那么方程 g ( x ) = 0 的一个解就是函数 x![c1-fig-5022.jpg](img/c1-fig-5022.jpg)f(x)的一个不动点其中
![c1-fig-5026.jpg](../images/c1-fig-5026.jpg)
![c1-fig-5026.jpg](img/c1-fig-5026.jpg)
而 Dg ( x )是在 x 处求的 g 的导数。牛顿法就是我们上面看到的不动点法的运用,通过寻找函数 f 的不动点来逼近方程的一个解。 [^(64)](#c1-fn-0064) 对于许多函数 g 以及对于 x 的足够好的初始猜测,牛顿法非常迅速地收敛到 g ( x ) = 0 的解。 [^(65)](#c1-fn-0065)
为了将牛顿法实现为函数,首先要表达导数的思想。注意,“导数”和平均阻尼一样,是将一个函数转换成另一个函数的东西。例如,函数 xx3 的导数就是函数 x3x2。一般来说,如果 g 是一个函数,而 dx 是一个小数字,那么 g 的导数 Dg 是这样一个函数,它在任意数字 x 处的值由下式给出(在小 dx 的范围内)
![c1-fig-5027.jpg](../images/c1-fig-5027.jpg)
![c1-fig-5027.jpg](img/c1-fig-5027.jpg)
因此,我们可以将导数的概念(取 dx 为 0.00001)表示为函数
......@@ -2133,7 +2133,7 @@ function deriv(g) {
const dx = 0.00001;
```
`average_damp`一样,`deriv`是一个以函数为自变量,以值返回函数的函数。例如,为了逼近 x![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)x³在 5 处的导数(其精确值为 75 ),我们可以计算
`average_damp`一样,`deriv`是一个以函数为自变量,以值返回函数的函数。例如,为了逼近 x![c1-fig-5022.jpg](img/c1-fig-5022.jpg)x³在 5 处的导数(其精确值为 75 ),我们可以计算
```
function cube(x) { return x * x * x; }
......@@ -2173,7 +2173,7 @@ function fixed_point_of_transform(g, transform, guess) {
这个非常普通的函数以一个计算某个函数的函数`g`、一个转换函数`g`和一个初始猜测为参数。返回的结果是转换函数的固定点。
使用这个抽象,我们可以改写本节中的第一个平方根计算(这里我们寻找平均阻尼版本的 y![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)x/y)作为这个通用方法的一个实例:
使用这个抽象,我们可以改写本节中的第一个平方根计算(这里我们寻找平均阻尼版本的 y![c1-fig-5022.jpg](img/c1-fig-5022.jpg)x/y)作为这个通用方法的一个实例:
```
function sqrt(x) {
......@@ -2184,7 +2184,7 @@ function sqrt(x) {
}
```
类似地,我们可以将这一节中的第二次平方根计算(牛顿法的一个实例,用于找到 y![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)y²–x 的牛顿变换的一个固定点)表示为
类似地,我们可以将这一节中的第二次平方根计算(牛顿法的一个实例,用于找到 y![c1-fig-5022.jpg](img/c1-fig-5022.jpg)y²–x 的牛顿变换的一个固定点)表示为
```
function sqrt(x) {
......@@ -2228,7 +2228,7 @@ double(double(double))(inc)(5);
##### 练习 1.42
设 f 和 g 为两个单参数函数。 g 后的成分 f 定义为函数 x![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)f(g(x))。声明一个实现组合的函数`compose`。例如,如果`inc`是一个参数加 1 的函数,
设 f 和 g 为两个单参数函数。 g 后的成分 f 定义为函数 x![c1-fig-5022.jpg](img/c1-fig-5022.jpg)f(g(x))。声明一个实现组合的函数`compose`。例如,如果`inc`是一个参数加 1 的函数,
```
compose(square, inc)(6);
......@@ -2237,7 +2237,7 @@ compose(square, inc)(6);
##### 练习 1.43
如果 f 是一个数值函数, n 是一个正整数,那么我们就可以形成 f 的第 n 次重复应用,定义为在 x 处的值为 f ( f ( )的函数。。。(f(x)。。。))。例如:如果 f 是函数 x ![c1-fig-5022.jpg](../images/c1-fig-5022.jpg) x + 1,那么 f 的第 n 次重复应用就是函数 x![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)x+n。如果 f 是一个数的平方运算,那么 f 的第 n 次重复应用就是将其自变量提升到 2 ^( n ) 次方的函数。编写一个函数,将计算 f 和正整数 n 的函数作为输入,并返回计算第 n 次重复应用 f 的函数。您的函数应该能够按如下方式使用:
如果 f 是一个数值函数, n 是一个正整数,那么我们就可以形成 f 的第 n 次重复应用,定义为在 x 处的值为 f ( f ( )的函数。。。(f(x)。。。))。例如:如果 f 是函数 x ![c1-fig-5022.jpg](img/c1-fig-5022.jpg) x + 1,那么 f 的第 n 次重复应用就是函数 x![c1-fig-5022.jpg](img/c1-fig-5022.jpg)x+n。如果 f 是一个数的平方运算,那么 f 的第 n 次重复应用就是将其自变量提升到 2 ^( n ) 次方的函数。编写一个函数,将计算 f 和正整数 n 的函数作为输入,并返回计算第 n 次重复应用 f 的函数。您的函数应该能够按如下方式使用:
```
repeated(square, 2)(5);
......@@ -2252,7 +2252,7 @@ repeated(square, 2)(5);
##### 练习 1.45
我们在 1.3.3 节中看到,试图通过天真地找到一个固定点 y ![c1-fig-5022.jpg](../images/c1-fig-5022.jpg) x / y 来计算平方根并不收敛,这可以通过平均阻尼来解决。同样的方法也适用于寻找作为平均阻尼的 y![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)x/y²的不动点的立方根。不幸的是,该过程不适用于第四根——单个平均阻尼不足以对 y![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)x/y³收敛进行定点搜索。另一方面,如果我们平均阻尼两次(即,使用平均阻尼的平均阻尼为 y![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)x/y³),定点搜索确实收敛。根据 y![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)x/y^n^(–1)的重复平均阻尼,做一些实验来确定作为定点搜索计算 n 根需要多少个平均阻尼。用这个实现一个简单的函数,用练习 1.43 中的`fixed_point``average_damp``repeated`函数计算 n 次方根。假设您需要的任何算术运算都可以作为原语获得。
我们在 1.3.3 节中看到,试图通过天真地找到一个固定点 y ![c1-fig-5022.jpg](img/c1-fig-5022.jpg) x / y 来计算平方根并不收敛,这可以通过平均阻尼来解决。同样的方法也适用于寻找作为平均阻尼的 y![c1-fig-5022.jpg](img/c1-fig-5022.jpg)x/y²的不动点的立方根。不幸的是,该过程不适用于第四根——单个平均阻尼不足以对 y![c1-fig-5022.jpg](img/c1-fig-5022.jpg)x/y³收敛进行定点搜索。另一方面,如果我们平均阻尼两次(即,使用平均阻尼的平均阻尼为 y![c1-fig-5022.jpg](img/c1-fig-5022.jpg)x/y³),定点搜索确实收敛。根据 y![c1-fig-5022.jpg](img/c1-fig-5022.jpg)x/y^n^(–1)的重复平均阻尼,做一些实验来确定作为定点搜索计算 n 根需要多少个平均阻尼。用这个实现一个简单的函数,用练习 1.43 中的`fixed_point``average_damp``repeated`函数计算 n 次方根。假设您需要的任何算术运算都可以作为原语获得。
##### 练习 1.46
......@@ -2348,7 +2348,7 @@ repeated(square, 2)(5);
这个定理在 1845 年被法国数学家和工程师加布里埃尔·拉米证明,他主要以对数学物理的贡献而闻名。为了证明该定理,我们考虑成对(a[k]T5、b[k]T9),其中 a[k]b[k],为此欧几里德算法终止于 k 步。这个证明是基于这样的主张,如果(a[k][+1],b[k][+1])(a[k],b[k])(a[k][-1] 那么我们必须有 b[k][+1]b[k]+b[k][–1]。 为了验证该声明,考虑通过应用变换 a[k][–1]=b[k],b[k][–1]= a[k]除以来定义缩减步长第二个等式是指 a[k]=QB[k]+b[k][–1]对于某正整数 q 。而既然 q 至少必须是 1 我们有 a[k]=QB[k]+b[k][–1]b[k]+b[k 但是在前面的缩减步骤中我们有 b[k][+1]=a[k]。因此,b[k][+1]=a[k]b[k]+b[k][–1]。这证实了这一说法。现在我们可以在 k 上用归纳法证明定理,算法需要终止的步数。对于 k = 1,结果是正确的,因为这仅仅要求 b 至少与 Fib(1) = 1 一样大。现在,假设对于所有小于或等于 k 的整数,结果为真,并为 k + 1 建立结果。让(a[k][+1],b[k][+1])(a[k],b[k])(a[k 通过我们的归纳假设,我们有 b[k][–1]Fib(k–1)和 b[k]Fib(k)。因此,应用我们刚刚证明的要求以及斐波纳契数的定义,给出了 b[k][+1]b[k]+b[k][–1]Fib(k+Fib(]]
[42](#c1-fn-0042a) 如果 d 是 n 的约数,那么 n / d 也是。但是 d 和 n / d 不能都大于![c1-fig-5012.jpg](../images/c1-fig-5012.jpg)
[42](#c1-fn-0042a) 如果 d 是 n 的约数,那么 n / d 也是。但是 d 和 n / d 不能都大于![c1-fig-5012.jpg](img/c1-fig-5012.jpg)
皮耶·德·费玛(1601-1665)被认为是现代数论的创始人。他获得了许多重要的数论结果,但他通常只公布结果,而不提供证明。费马的小定理在他 1640 年写的一封信中陈述过。欧拉在 1736 年给出了第一个公开发表的证明(在莱布尼茨未发表的手稿中发现了更早的相同证明)。费马最著名的结果——被称为费马大定理——在 1637 年被他写在《算术》(由三世纪的希腊数学家丢番图所著)一书中,并注明“我发现了一个真正了不起的证明,但这个范围太小,不足以包含它”寻找费马大定理的证明成为数论中最著名的挑战之一。普林斯顿大学的安德鲁·怀尔斯终于在 1995 年给出了一个完整的解决方案。
......@@ -2362,7 +2362,7 @@ repeated(square, 2)(5);
[48](#c1-fn-0048a) 原始函数`display`返回它的参数,但也打印它。这里的`" *** "`是一个字符串,一个我们作为参数传递给`display`函数的字符序列。2.3.1 节更全面地介绍了字符串。
[49](#c1-fn-0049a) 这一系列,通常以对等形式写成![c1-fig-5015.jpg](../images/c1-fig-5015.jpg),是由于莱布尼茨。我们将在 3.5.3 节中看到如何使用它作为一些奇特的数字技巧的基础。
[49](#c1-fn-0049a) 这一系列,通常以对等形式写成![c1-fig-5015.jpg](img/c1-fig-5015.jpg),是由于莱布尼茨。我们将在 3.5.3 节中看到如何使用它作为一些奇特的数字技巧的基础。
请注意,我们使用了块结构(1.1.8 节)来将`pi_next``pi_term`的声明嵌入到`pi_sum`中,因为这些函数不太可能用于任何其他目的。我们将在 1.3.2 节中看到如何完全摆脱它们。
......@@ -2394,7 +2394,7 @@ f(10);
[60](#c1-fn-0060a) 要在计算器上获得余弦的固定点,将其设置为弧度模式,然后重复按 cos 按钮,直到数值不再变化。
[61](#c1-fn-0061a) ![c1-fig-5022.jpg](../images/c1-fig-5022.jpg)(读作“映射到”)是数学家书写 lambda 表达式的方式。 y x / y 表示`y => x / y`,即在 y 处取值为 x / y 的函数。
[61](#c1-fn-0061a) ![c1-fig-5022.jpg](img/c1-fig-5022.jpg)(读作“映射到”)是数学家书写 lambda 表达式的方式。 y x / y 表示`y => x / y`,即在 y 处取值为 x / y 的函数。
注意,这是一个应用程序,它的函数表达式本身就是一个应用程序。练习 1.4 已经展示了形成这种应用程序的能力,但这只是一个玩具示例。在这里,我们开始看到这种应用程序的真正需求——当应用一个作为高阶函数返回值而获得的函数时。
......
......@@ -60,7 +60,7 @@ function linear_combination(a, b, x, y) {
我们在这里使用了一个强大的综合策略:如意算盘。我们还没有说有理数是如何表示的,或者函数`numer``denom``make_rat`应该如何实现。即便如此,如果我们有这三个函数,我们就可以用下面的关系式进行加、减、乘、除和等式检验:
![c2-fig-5001.jpg](../images/c2-fig-5001.jpg)
![c2-fig-5001.jpg](img/c2-fig-5001.jpg)
我们可以将这些规则表示为函数:
......@@ -186,7 +186,7 @@ print_rat(add_rat(one_third, one_third));
我们可以想象有理数系统的结构,如图[图 2.1](#c2-fig-0002) 所示。水平线代表隔离系统不同“层次”的抽象障碍。在每一层,屏障将使用数据抽象的程序(上图)与实现数据抽象的程序(下图)分开。使用有理数的程序只根据有理数包提供的“公共使用”的函数来操纵它们:`add_rat``sub_rat``mul_rat``div_rat``equal_rat`。反过来,这些仅根据构造函数和选择器`make_rat``numer``denom`来实现,它们本身是成对实现的。只要可以通过使用`pair``head``tail`来操作 pairs,pairs 是如何实现的细节与其余的有理数包无关。实际上,每一层的功能都是定义抽象障碍和连接不同层的接口。这个简单的想法有很多优点。一个优点是它使程序更容易维护和修改。任何复杂的数据结构都可以用编程语言提供的原始数据结构以多种方式表示。当然,表示的选择会影响对其进行操作的程序;因此,如果表示在以后某个时间被改变,所有这样的程序可能必须相应地被修改。在大型程序的情况下,这项任务可能既费时又费钱,除非通过设计将对表示的依赖限制在很少的程序模块中。
![c2-fig-0001.jpg](../images/c2-fig-0001.jpg)
![c2-fig-0001.jpg](img/c2-fig-0001.jpg)
[图 2.1](#c2-fig-0002a) 有理数包中的数据抽象壁垒。
......@@ -231,7 +231,7 @@ function print_point(p) {
但是数据到底是什么意思呢?仅仅说“由给定的选择器和构造器实现的东西”是不够的显然,不是每一组任意的三个函数都可以作为有理数实现的适当基础。我们需要保证,如果我们从一对整数`n``d`中构造一个有理数`x`,那么提取`x`中的`numer``denom`并将它们相除将会产生与用`n`除以`d`相同的结果。换句话说,`make_rat``numer``denom`必须满足条件,对于任意整数`n`和任意非零整数`d`,如果`x``make_rat(n, d)`,则
![c2-fig-5002.jpg](../images/c2-fig-5002.jpg)
![c2-fig-5002.jpg](img/c2-fig-5002.jpg)
事实上,这是`make_rat``numer``denom`必须满足的唯一条件,以便形成有理数表示的合适基础。一般来说,我们可以认为数据是由一些选择器和构造函数的集合定义的,以及这些函数必须满足的特定条件,以便成为有效的表示。 [](#c2-fn-0005)
......@@ -299,7 +299,7 @@ Alyssa P. Hacker 正在设计一个系统来帮助人们解决工程问题。她
电气工程师将使用艾丽莎的系统来计算电量。他们有时需要用公式计算两个电阻 R1 和 R2 的并联等效电阻 R [p] 的值
![c2-fig-5003.jpg](../images/c2-fig-5003.jpg)
![c2-fig-5003.jpg](img/c2-fig-5003.jpg)
电阻值通常只在电阻制造商保证的一定容差范围内。例如,如果您购买一个标有“6.8 欧姆,10%容差”的电阻器,您只能确定该电阻器的电阻在 6.8–0.68 = 6.12 和 6.8 + 0.68 = 7.48 欧姆之间。因此,如果有一个 6.8 欧姆的 10%电阻与一个 4.7 欧姆的 5%电阻并联,组合电阻的范围可以从约 2.58 欧姆(如果两个电阻在下限)到约 2.97 欧姆(如果两个电阻在上限)。
......@@ -388,11 +388,11 @@ function width(i) {
经过大量的工作,Alyssa P. Hacker 交付了她完成的系统。几年后,当她完全忘记这件事的时候,她接到一个愤怒的用户 Lem E. Tweakit 打来的疯狂电话。Lem 似乎已经注意到,并联电阻的公式可以用两种代数等价的方式来表示:
![c2-fig-5004.jpg](../images/c2-fig-5004.jpg)
![c2-fig-5004.jpg](img/c2-fig-5004.jpg)
![c2-fig-5005.jpg](../images/c2-fig-5005.jpg)
![c2-fig-5005.jpg](img/c2-fig-5005.jpg)
他编写了以下两个程序,每个程序都以不同的方式计算 parallelresistors 公式:
......@@ -427,13 +427,13 @@ Lem 抱怨说,Alyssa 的程序对这两种计算方式给出了不同的答案
正如我们所看到的,pairs 提供了一种原始的“粘合剂”,我们可以用它来构造复合数据对象。[图 2.2](#c2-fig-0007) 显示了一种标准的可视化配对方式——在本例中,配对由`pair(1, 2)`组成。在这个被称为盒子和指针符号的表示中,每个复合对象被显示为一个盒子的指针。一对的盒子有两个部分,左边部分是头部,右边部分是尾部。
![c2-fig-0002.jpg](../images/c2-fig-0002.jpg)
![c2-fig-0002.jpg](img/c2-fig-0002.jpg)
[图 2.2](#c2-fig-0007a)`pair(1, 2)`的盒指针表示。
我们已经看到,`pair`不仅可以用来组合数字,也可以用来组合成对。(在做练习 2.2 和 2.3 时,你利用了这个事实,或者应该利用了这个事实。)因此,pairs 提供了一个通用的构建模块,我们可以从中构建各种数据结构。[图 2.3](#c2-fig-0008) 显示了使用配对组合数字 1、2、3 和 4 的两种方式。
![c2-fig-0003.jpg](../images/c2-fig-0003.jpg)
![c2-fig-0003.jpg](img/c2-fig-0003.jpg)
[图 2.3](#c2-fig-0008a) 两种方式组合 1、2、3、4 使用成对。
......@@ -445,7 +445,7 @@ Lem 抱怨说,Alyssa 的程序对这两种计算方式给出了不同的答案
我们可以用 pairs 构建的一个有用的结构是一个序列——数据对象的有序集合。当然,有许多方法可以用对来表示序列。图 2.4 给出了一个特别直观的表示,其中序列 1,2,3,4 被表示为一串对。每对的`head`是链中对应的物品,该对的`tail`是链中的下一对。最后一对的`tail`表示序列的结束,在盒指针图中表示为对角线,在程序中表示为 JavaScript 的原始值`**null**`。整个序列由嵌套的`pair`操作构成:
![c2-fig-0004.jpg](../images/c2-fig-0004.jpg)
![c2-fig-0004.jpg](img/c2-fig-0004.jpg)
[图 2.4](#c2-fig-0009a) 把序列 1、2、3、4 表示为一串对子。
......@@ -802,13 +802,13 @@ pair(list(1, 2), list(3, 4));
作为三个条目的列表,第一个条目本身就是一个列表,`[1, [2, **null**]]`[图 2.5](#c2-fig-0010) 显示了这种结构的线对表示。
![c2-fig-0005.jpg](../images/c2-fig-0005.jpg)
![c2-fig-0005.jpg](img/c2-fig-0005.jpg)
[图 2.5](#c2-fig-0010a)`pair(list(1, 2), list(3, 4))`构成的结构。
另一种考虑元素是序列的序列的方式是树。序列的元素是树的分支,本身是序列的元素是子树。[图 2.6](#c2-fig-0011) 显示了图 2.5 中的树形结构。
![c2-fig-0006.jpg](../images/c2-fig-0006.jpg)
![c2-fig-0006.jpg](img/c2-fig-0006.jpg)
[图 2.6](#c2-fig-0011a) 图 2.5 中的列表结构视为一棵树。
......@@ -1089,7 +1089,7 @@ function even_fibs(n) {
信号处理工程师会发现很自然地将这些过程概念化为流经级联阶段的信号,每个阶段实现程序计划的一部分,如图[图 2.7](#c2-fig-0012) 所示。在`sum_odd_squares`中,我们从一个枚举器开始,它生成一个由给定树的叶子组成的“信号”。该信号通过一个滤波器,该滤波器除了奇数元素之外,其他元素都被滤除。产生的信号依次通过图,图是一个将`square`功能应用于每个元素的“传感器”。然后,映射的输出被馈送到一个累加器,该累加器使用`+`从初始 0 开始合并元素。`even_fibs`的计划是类似的。
![c2-fig-0007.jpg](../images/c2-fig-0007.jpg)
![c2-fig-0007.jpg](img/c2-fig-0007.jpg)
[图 2.7](#c2-fig-0012a) 函数`sum_odd_squares`(上图)和`even_fibs`(下图)的信号流程图揭示了两个程序之间的共性。
......@@ -1324,7 +1324,7 @@ function accumulate_n(op, init, seqs) {
假设我们将向量 v=(v[I])表示为数列,将矩阵 m=(m[ij])表示为向量序列(矩阵的行)。例如,矩阵
![c2-fig-5006.jpg](../images/c2-fig-5006.jpg)
![c2-fig-5006.jpg](img/c2-fig-5006.jpg)
表示为以下序列:
......@@ -1336,7 +1336,7 @@ list(list(1, 2, 3, 4),
有了这种表示,我们可以用序列运算简明地表达基本的矩阵和向量运算。这些运算(在任何矩阵代数书籍中都有描述)如下:
![c2-fig-5007.jpg](../images/c2-fig-5007.jpg)
![c2-fig-5007.jpg](img/c2-fig-5007.jpg)
我们可以将点积定义为 [^(15)](#c2-fn-0015)
......@@ -1495,7 +1495,7 @@ function remove(item, sequence) {
“八皇后难题”询问如何将八个皇后放置在棋盘上,使得没有皇后被其他皇后牵制(即,没有两个皇后在同一行、列或对角线上)。图 2.8 显示了一种可能的解决方案。解决这个难题的一个方法是全面展开工作,在每列中放置一个皇后。一旦我们放置了 k–1 张皇后牌,我们必须将 k 张皇后牌放置在一个不会检查棋盘上任何一张皇后牌的位置。我们可以递归地制定这种方法:假设我们已经生成了将 k–1 皇后放置在棋盘的前 k–1 列中的所有可能方式的序列。对于这些方法中的每一种,通过在第 k 列的每一行中放置一个皇后来生成一组扩展的位置。现在过滤这些,只保留第 k 列中的皇后相对于其他皇后安全的位置。这产生了将 k 皇后放置在第一个 k 列中的所有方式的顺序。通过继续这个过程,我们将不仅产生一个解决方案,而是这个难题的所有解决方案。
![c2-fig-0008.jpg](../images/c2-fig-0008.jpg)
![c2-fig-0008.jpg](img/c2-fig-0008.jpg)
[图 2.8](#c2-fig-0015a) 八皇后谜题的一种解法。
......@@ -1538,7 +1538,7 @@ flatmap(new_row =>
本节介绍了一种简单的绘图语言,它展示了数据抽象和闭包的强大功能,并以一种基本的方式利用了高阶函数。该语言旨在使试验模式变得容易,例如图 2.9 中的模式,这些模式由重复的元素组成,这些元素被移动和缩放。 [^(20)](#c2-fn-0020) 在这种语言中,被组合的数据对象被表示为函数而不是列表结构。正如满足闭包属性的`pair`允许我们轻松构建任意复杂的列表结构一样,这种语言中的操作也满足闭包属性,允许我们轻松构建任意复杂的模式。
![c2-fig-0009.jpg](../images/c2-fig-0009.jpg)
![c2-fig-0009.jpg](img/c2-fig-0009.jpg)
[图 2.9](#c2-fig-0016a) 用图片语言生成的设计。
......@@ -1548,11 +1548,11 @@ flatmap(new_row =>
这种画面语言的优雅之处在于只有一种元素,叫做画家。画家绘制图像,该图像被移动和缩放以适合指定的平行四边形框架。例如,有一个我们称之为`wave`的原始画家,他画了一个粗糙的线条画,如图[图 2.10](#c2-fig-0017) 所示。图画的实际形状取决于画框——[图 2.10](#c2-fig-0017) 中的所有四幅图像都是由同一个`wave`画家绘制的,但是相对于四个不同的画框。画家可以比这更精细:名为`rogers`的原始画家为麻省理工学院的创始人威廉·巴顿·罗杰斯画了一幅画,如图[图 2.11](#c2-fig-0018) 所示。[^(21)](#c2-fn-0021)[图 2.11](#c2-fig-0018) 中的四幅图像是针对与[图 2.10](#c2-fig-0017) 中的`wave`图像相同的四帧绘制的。
![c2-fig-0010.jpg](../images/c2-fig-0010.jpg)
![c2-fig-0010.jpg](img/c2-fig-0010.jpg)
[图 2.10](#c2-fig-0017a)`wave`画师产生的图像,关于四个不同的帧。用虚线显示的帧不是图像的一部分。
![c2-fig-0011.jpg](../images/c2-fig-0011.jpg)
![c2-fig-0011.jpg](img/c2-fig-0011.jpg)
[图 2.11](#c2-fig-0018a) 麻省理工学院创始人兼首任校长威廉·巴顿·罗杰斯的画像,与图 2.10[中的四个画框相对而画(原图由麻省理工学院博物馆提供)。](#c2-fig-0017)
......@@ -1567,7 +1567,7 @@ const wave4 = below(wave2, wave2);
在以这种方式构建一个复杂的图像时,我们利用了这样一个事实,即画家在语言的组合方式下是封闭的。两个画师的`beside``below`本身就是画师;因此,我们可以将其作为制作更复杂的画家的元素。与使用`pair`构建列表结构一样,组合方式下的数据闭合对于仅使用少量操作创建复杂结构的能力至关重要。
![c2-fig-0012.jpg](../images/c2-fig-0012.jpg)
![c2-fig-0012.jpg](img/c2-fig-0012.jpg)
[图 2.12](#c2-fig-0019a) 创建复杂图形,从[图 2.10](#c2-fig-0017)`wave`画师开始。
......@@ -1599,11 +1599,11 @@ function right_split(painter, n) {
}
```
![c2-fig-0013.jpg](../images/c2-fig-0013.jpg)
![c2-fig-0013.jpg](img/c2-fig-0013.jpg)
[图 2.13](#c2-fig-0020a)`right_split``corner_split`的递归计划。
![c2-fig-0014.jpg](../images/c2-fig-0014.jpg)
![c2-fig-0014.jpg](img/c2-fig-0014.jpg)
[图 2.14](#c2-fig-0021a) 将递归运算`right_split`应用于画师`wave``rogers`。组合四个`corner_split`图形产生对称的`square_limit`,如图 2.9 中的[所示。](#c2-fig-0016)
......@@ -1692,7 +1692,7 @@ const up_split = split(below, beside);
[图 2.15](#c2-fig-0022) 显示了一个帧及其相关向量。根据数据抽象,我们还不需要具体说明框架是如何表示的,只是说有一个构造函数`make_frame`,它接受三个向量并产生一个框架,以及三个相应的选择器`origin_frame``edge1_frame``edge2_frame`(见练习 2.47)。
![c2-fig-0015.jpg](../images/c2-fig-0015.jpg)
![c2-fig-0015.jpg](img/c2-fig-0015.jpg)
[图 2.15](#c2-fig-0022a) 一个帧由三个向量描述——一个原点和两条边。
......@@ -2009,7 +2009,7 @@ JavaScript 解释器读取双引号`"`后的字符,直到找到另一个双引
为了简单起见,我们将考虑一个非常简单的符号微分程序,它处理只使用两个参数的加法和乘法运算构建的表达式。任何这种表达式的微分可以通过应用以下归约规则来进行:
![c2-fig-5008.jpg](../images/c2-fig-5008.jpg)
![c2-fig-5008.jpg](img/c2-fig-5008.jpg)
注意,后两个规则本质上是递归的。也就是说,要得到一个和的导数,我们首先要找到各项的导数,然后将它们相加。每一项都可能是一个需要分解的表达式。分解成越来越小的碎片,最终会产生要么是常数要么是变量的碎片,它们的导数要么是 0,要么是 1。
......@@ -2126,7 +2126,7 @@ list("+", list("*", list("*", "x", "y"), list("+", 1, 0)),
程序产生正确的答案;然而,它们并不简单。的确如此
![c2-fig-5009.jpg](../images/c2-fig-5009.jpg)
![c2-fig-5009.jpg](img/c2-fig-5009.jpg)
但是我们希望程序知道 x 0 = 0,1 y = y ,0 + y = y 。第二个例子的答案应该是简单的`y`。如第三个例子所示,当表达式很复杂时,这就成了一个严重的问题。
......@@ -2187,7 +2187,7 @@ list("+", list("*", "x", "y"), list("*", "y", list("+", "x", 3)))
展示如何扩展基本的区分器来处理更多种类的表达式。例如,实现差异化规则
![c2-fig-5010.jpg](../images/c2-fig-5010.jpg)
![c2-fig-5010.jpg](img/c2-fig-5010.jpg)
通过在`deriv`程序中添加一个新的子句,并定义适当的功能`is_exp``base``exponent``make_exp`。(您可以使用字符串`"**"`来表示取幂运算。)建立这样的规则:任何事物的 0 次幂都是 1,任何事物的 1 次幂都是事物本身。
......@@ -2327,7 +2327,7 @@ function intersection_set(set1, set2) {
通过以树的形式排列集合元素,我们可以比有序列表表示做得更好。树的每个节点保存集合中的一个元素,称为该节点上的“条目”,以及到另外两个(可能是空的)节点中的每一个的链接。“左”链接指向比节点处的元素小的元素,“右”链接指向比节点处的元素大的元素。[图 2.16](#c2-fig-0026) 显示了一些代表集合{1,3,5,7,9,11}的树。同一集合可以由树以多种不同的方式来表示。对于有效的表示,我们唯一需要的是左子树中的所有元素都小于节点条目,而右子树中的所有元素都大于节点条目。
![c2-fig-0016.jpg](../images/c2-fig-0016.jpg)
![c2-fig-0016.jpg](img/c2-fig-0016.jpg)
[图 2.16](#c2-fig-0026a) 表示集合{1,3,5,7,9,11}的各种二叉树。
......@@ -2380,7 +2380,7 @@ function adjoin_set(x, set) {
上面声称搜索树可以在对数数量的步骤中执行,这是基于树是“平衡的”的假设,即每棵树的左右子树具有大约相同数量的元素,使得每个子树包含其父树的大约一半的元素。但是我们怎么能确定我们建造的树是平衡的呢?即使我们从平衡的树开始,用`adjoin_set`添加元素也可能产生不平衡的结果。由于新邻接元素的位置取决于该元素与集合中已有项目的比较情况,我们可以预期,如果我们“随机”添加元素,树将趋于平均平衡。但这不是保证。例如,如果我们从一个空集开始,并依次邻接数字 1 到 7,我们最终会得到一个高度不平衡的树,如图[图 2.17](#c2-fig-0027) 所示。在这棵树中,所有左边的子树都是空的,所以它比简单的有序列表没有优势。解决这个问题的一种方法是定义一种操作,将任意树转换成具有相同元素的平衡树。然后,我们可以在每隔几个`adjoin_set`操作之后执行这个转换,以保持我们的集合平衡。还有其他方法可以解决这个问题,其中大多数涉及设计新的数据结构,搜索和插入都可以在θ(logn 步中完成。 [^(36)](#c2-fn-0036)
![c2-fig-0017.jpg](../images/c2-fig-0017.jpg)
![c2-fig-0017.jpg](img/c2-fig-0017.jpg)
[图 2.17](#c2-fig-0027a) 由邻接 1 到 7 依次产生的不平衡树。
......@@ -2509,7 +2509,7 @@ BACADAEAFABBAAAGAH
[图 2.18](#c2-fig-0028) 显示了上面给出的 A 到 H 代码的霍夫曼树。叶子上的权重表示该树是为 A 出现的相对频率为 8、B 出现的相对频率为 3、其他字母出现的相对频率为 1 的消息而设计的。
![c2-fig-0018.jpg](../images/c2-fig-0018.jpg)
![c2-fig-0018.jpg](img/c2-fig-0018.jpg)
[图 2.18](#c2-fig-0028a) 一棵霍夫曼编码树。
......@@ -2735,7 +2735,7 @@ Sha boom
我们从简单的复数例子开始。我们将看到类型标签和数据导向样式如何使我们能够为复数设计单独的矩形和极坐标表示,同时保持抽象“复数”数据对象的概念。我们将通过为复数(`add_complex``sub_complex``mul_complex``div_complex`)定义算术函数来实现这一点,这些函数使用通用选择器来访问复数的各个部分,而与数字的表示方式无关。产生的复数系统,如图 2.19 所示,包含两种不同的抽象障碍。“水平”抽象障碍与图 2.1 中的角色相同。它们将“高级”操作与“低级”表示隔离开来。此外,还有一个“垂直”障碍,让我们能够单独设计和安装替代的表现形式。
![c2-fig-0019.jpg](../images/c2-fig-0019.jpg)
![c2-fig-0019.jpg](img/c2-fig-0019.jpg)
[图 2.19](#c2-fig-0029a) 复数系统中的数据抽象障碍。
......@@ -2750,7 +2750,7 @@ Sha boom
| 实部(z1+z2) | = | 实部(z1)+实部(z2) |
| 虚部(z1+z2) | = | 虚部(z1)+虚部(z2) |
![c2-fig-0020.jpg](../images/c2-fig-0020.jpg)
![c2-fig-0020.jpg](img/c2-fig-0020.jpg)
[图 2.20](#c2-fig-0030a) 复数为平面上的点。
......@@ -2800,7 +2800,7 @@ function div_complex(z1, z2) {
为了使不同的选择具体化,想象有两个程序员,Ben Bitdiddle 和 Alyssa P. Hacker,他们独立地设计复数系统的表示。Ben 选择用矩形来表示复数。有了这个选择,选择一个复数的实部和虚部就简单了,就像用给定的实部和虚部构造一个复数一样。为了找到幅度和角度,或者用给定的幅度和角度构造一个复数,他使用了三角关系
| x = r cos A | r = ![c2-fig-5011.jpg](../images/c2-fig-5011.jpg) |
| x = r cos A | r = ![c2-fig-5011.jpg](img/c2-fig-5011.jpg) |
| y=rsinA | A = arctan( y , x ) |
其将实部和虚部( x 、 y )与幅度和角度( r 、 A )相关联。因此,本的表示由以下选择器和构造器给出:
......@@ -2974,7 +2974,7 @@ function make_from_mag_ang(r, a) {
由此产生的复数系统具有图 2.21 所示的结构。该系统被分解为三个相对独立的部分:复数算术运算、Alyssa 的极坐标实现和 Ben 的直角坐标实现。极坐标和矩形实现可能是由 Ben 和 Alyssa 分别编写的,这两种实现都可以被第三个程序员用作底层表示,以抽象构造器/选择器接口的形式实现复数运算函数。
![c2-fig-0021.jpg](../images/c2-fig-0021.jpg)
![c2-fig-0021.jpg](img/c2-fig-0021.jpg)
[图 2.21](#c2-fig-0031a) 通用复数运算系统的结构。
......@@ -2990,7 +2990,7 @@ function make_from_mag_ang(r, a) {
我们需要的是进一步模块化系统设计的方法。这是由称为数据导向编程的编程技术提供的。要理解数据导向编程是如何工作的,首先要观察到,每当我们处理一组不同类型的通用操作时,我们实际上是在处理一个二维表,该表包含一个轴上的可能操作和另一个轴上的可能类型。表中的条目是为每种类型的参数实现每个操作的函数。在上一节开发的复数系统中,操作名、数据类型和实际函数之间的对应分布在通用接口函数的各种条件子句中。但是同样的信息也可以组织成表格,如图 2.22 所示。
![c2-fig-0022.jpg](../images/c2-fig-0022.jpg)
![c2-fig-0022.jpg](img/c2-fig-0022.jpg)
[图 2.22](#c2-fig-0032a) 复数系统运算表。
......@@ -3222,7 +3222,7 @@ function apply_generic(op, arg) { return head(arg)(op); }
[图 2.23](#c2-fig-0033) 显示了我们将要构建的系统的结构。注意抽象障碍。从使用“数字”的人的角度来看,有一个函数`add`可以处理提供的任何数字。函数`add`是一个通用接口的一部分,它允许使用数字的程序统一访问独立的普通算术、有理算术和复杂算术包。任何单个算术包(如复杂包)本身都可以通过通用函数(如`add_complex`)访问,这些函数组合了为不同表示(如矩形和极坐标)设计的包。此外,该系统的结构是可加的,因此人们可以分别设计单独的算术包,然后将它们组合起来,形成一个通用的算术系统。
![c2-fig-0023.jpg](../images/c2-fig-0023.jpg)
![c2-fig-0023.jpg](img/c2-fig-0023.jpg)
[图 2.23](#c2-fig-0033a) 通用算术系统。
......@@ -3376,7 +3376,7 @@ function make_complex_from_mag_ang(r, a){
我们这里有一个两级标记系统。一个典型的复数,如矩形形式的 3 + 4 i ,将被表示为如图[图 2.24](#c2-fig-0034) 所示。外部标签(`"complex"`)用于将编号指向复合包装。一旦进入复杂包装,下一个标签(`"rectangular"`)用于将数字指向矩形包装。在一个大而复杂的系统中,可能有许多层次,每一层都通过一般操作与下一层连接。当数据对象被“向下”传递时,用于将它导向适当包的外部标签被剥离(通过应用`contents`),下一层标签(如果有)变得可见,用于进一步的分派。
![c2-fig-0024.jpg](../images/c2-fig-0024.jpg)
![c2-fig-0024.jpg](img/c2-fig-0024.jpg)
[图 2.24](#c2-fig-0034a) 以矩形形式表示 3 + 4i。
......@@ -3483,7 +3483,7 @@ function apply_generic(op, args) {
上面介绍的强制方案依赖于类型对之间自然关系的存在。通常在不同类型之间的关系上有更多的“全局”结构。例如,假设我们正在构建一个通用的算术系统来处理整数、有理数、实数和复数。在这样一个系统中,把一个整数看成一种特殊的有理数是很自然的,这种有理数又是一种特殊的实数,而实数又是一种特殊的复数。我们实际拥有的是所谓的类型的层次结构,例如,整数是有理数的一个子类型(也就是说,任何可以应用于有理数的运算都可以自动应用于整数)。相反,我们说有理数形成了整数的一个超类型。这里我们有一个非常简单的层次结构,其中每个类型最多有一个超类型和一个子类型。这种结构称为塔,如图[图 2.25](#c2-fig-0035) 所示。
![c2-fig-0025.jpg](../images/c2-fig-0025.jpg)
![c2-fig-0025.jpg](img/c2-fig-0025.jpg)
[图 2.25](#c2-fig-0035a) 一座塔的类型。
......@@ -3499,7 +3499,7 @@ function apply_generic(op, args) {
如我们所见,如果我们系统中的数据类型可以自然地排列在一个塔中,这就大大简化了处理不同类型上的一般操作的问题。不幸的是,通常情况并非如此。[图 2.26](#c2-fig-0036) 展示了一个更复杂的混合类型排列,这个显示了不同类型的几何图形之间的关系。我们看到,一般来说,一个类型可能有不止一个子类型。例如,三角形和四边形都是多边形的子类型。此外,一个类型可以有多个超类型。例如,等腰直角三角形可以被认为是等腰三角形或直角三角形。这个多重超类型的问题特别棘手,因为这意味着没有唯一的方法在层次结构中“提升”一个类型。找到“正确的”超类型,在其中对一个对象应用一个操作,可能需要在整个类型网络中对一个函数(如`apply_generic`)进行大量的搜索。因为一个类型通常有多个子类型,所以在类型层次结构中强制一个值“向下”也有类似的问题。在大型系统的设计中,处理大量相互关联的类型同时仍然保持模块性是非常困难的,这也是当前研究的一个领域。 [^(49)](#c2-fn-0049)
![c2-fig-0026.jpg](../images/c2-fig-0026.jpg)
![c2-fig-0026.jpg](img/c2-fig-0026.jpg)
[图 2.26](#c2-fig-0036a) 几何图形类型之间的关系。
......@@ -3692,7 +3692,7 @@ function mul_term_by_all_terms(t1, L) {
这就是多项式加法和乘法的全部内容。注意,由于我们使用通用函数`add``mul`对项进行运算,我们的多项式包能够自动处理通用算术包已知的任何类型的系数。如果我们包含一个强制机制,比如 2.5.2 节中讨论的那些机制之一,那么我们也能够自动处理不同系数类型的多项式的运算,比如
![c2-fig-5012.jpg](../images/c2-fig-5012.jpg)
![c2-fig-5012.jpg](img/c2-fig-5012.jpg)
因为我们在通用算术系统中安装了多项式加法和乘法函数`add_ poly``mul_poly`作为类型`polynomial``add``mul`运算,所以我们的系统也能够自动处理多项式运算,例如
......@@ -3814,11 +3814,11 @@ function div_terms(L1, L2) {
我们可以扩展我们的通用算术系统,使其包含有理函数。这些“分数”的分子和分母都是多项式,例如
![c2-fig-5014.jpg](../images/c2-fig-5014.jpg)
![c2-fig-5014.jpg](img/c2-fig-5014.jpg)
该系统应该能够对有理函数进行加、减、乘、除运算,并执行如下计算
![c2-fig-5015.jpg](../images/c2-fig-5015.jpg)
![c2-fig-5015.jpg](img/c2-fig-5015.jpg)
(此处的总和已通过去除共同因素进行了简化。普通的“交叉乘法”会在一个五次多项式上产生一个四次多项式。)
......
......@@ -570,7 +570,7 @@ const paul_acc = make_joint(peter_acc, "open sesame", "rosebud");
[图 3.1](#c3-fig-0001) 显示了一个由三个框架组成的简单环境结构,标记为 I、II 和 III。在图中,A、B、C 和 D 是指向环境的指针。c 和 D 指向同一个环境。名字`z``x`绑定在第二帧,而`y``x`绑定在第一帧,环境 D 中`x`的值为 3。相对于环境 B 的`x`的值也是 3。这确定如下:我们检查序列中的第一帧(帧 III)并且没有找到`x`的绑定,所以我们前进到封闭环境 D 并且在帧 I 中找到绑定。另一方面,环境 A 中的`x`的值是 7,因为序列中的第一帧(帧 II)包含从`x`到 7 的绑定。关于环境 A,帧 II 中`x`到 7 的绑定被说成是遮蔽了帧 I 中`x`到 3 的绑定。
![c3-fig-0001.jpg](../images/c3-fig-0001.jpg)
![c3-fig-0001.jpg](img/c3-fig-0001.jpg)
[图 3.1](#c3-fig-0001a) 一个简单的环境结构。
......@@ -606,7 +606,7 @@ const square = x => x * x;
[图 3.2](#c3-fig-0002) 显示了评估该声明语句的结果。全局环境包含程序环境。为了减少混乱,在这个图之后,我们将不显示全局环境(因为它总是相同的),但是从程序环境向上的指针提醒我们它的存在。函数对象是一对,其代码指定函数有一个参数`x`和一个函数体`**return** x * x;`。函数的环境部分是一个指向程序环境的指针,因为 lambda 表达式就是在这个环境中被求值以产生函数的。程序框架中添加了一个新的绑定,它将函数对象与名称`square`相关联。
![c3-fig-0002.jpg](../images/c3-fig-0002.jpg)
![c3-fig-0002.jpg](img/c3-fig-0002.jpg)
[图 3.2](#c3-fig-0002a) 在程序环境中评估`**function** square(x) { **return** x * x; }`产生的环境结构。
......@@ -616,7 +616,7 @@ const square = x => x * x;
为了说明这个规则是如何遵循的,[图 3.3](#c3-fig-0003) 举例说明了在程序环境中对表达式`square(5)`求值所创建的环境结构,其中`square`是在[图 3.2](#c3-fig-0002) 中生成的函数。应用该函数会创建一个新的环境,在图中标记为 E1,它以一个帧开始,在该帧中,函数的参数`x`被绑定到参数 5。注意,环境 E1 中的名称`x`后面是一个没有等号的冒号,这表示参数`x`被视为一个变量。 [^(15)](#c3-fn-0015) 从该框架向上的指针表示该框架的封闭环境是程序环境。这里选择程序环境,因为这是作为`square`功能对象的一部分指示的环境。在 E1,我们评估函数的主体,`**return** x * x;`。因为在 E1`x`的值是 5,所以结果是`5 * 5`,即 25。
![c3-fig-0003.jpg](../images/c3-fig-0003.jpg)
![c3-fig-0003.jpg](img/c3-fig-0003.jpg)
[图 3.3](#c3-fig-0003a) 在程序环境中评估`square(5)`创建的环境。
......@@ -647,7 +647,7 @@ function f(a) {
我们可以使用环境模型来分析同一个示例。[图 3.4](#c3-fig-0004) 显示了通过评估程序环境中`f``square``sum_of_squares`的定义而创建的三个功能对象。每个函数对象都由一些代码和一个指向程序环境的指针组成。
![c3-fig-0004.jpg](../images/c3-fig-0004.jpg)
![c3-fig-0004.jpg](img/c3-fig-0004.jpg)
[图 3.4](#c3-fig-0004a) 程序框中的功能对象。
......@@ -657,7 +657,7 @@ function f(a) {
return sum_of_squares(a + 1, a * 2);
```
![c3-fig-0005.jpg](../images/c3-fig-0005.jpg)
![c3-fig-0005.jpg](img/c3-fig-0005.jpg)
[图 3.5](#c3-fig-0005a) 使用[图 3.4](#c3-fig-0004) 中的函数评估`f(5)`创建的环境。
......@@ -736,7 +736,7 @@ W1(50);
[图 3.6](#c3-fig-0006) 显示了在程序环境中声明`make_withdraw`函数的结果。这产生了一个包含指向程序环境的指针的函数对象。到目前为止,这与我们已经看到的例子没有什么不同,只是函数体中的返回表达式本身是一个 lambda 表达式。
![c3-fig-0006.jpg](../images/c3-fig-0006.jpg)
![c3-fig-0006.jpg](img/c3-fig-0006.jpg)
[图 3.6](#c3-fig-0006a) 在程序环境中定义`make_withdraw`的结果。
......@@ -748,7 +748,7 @@ const W1 = make_withdraw(100);
像往常一样,我们首先建立一个环境 E1,其中参数`balance`被绑定到参数 100。在这个环境中,我们评估`make_withdraw`的主体,即返回表达式是 lambda 表达式的返回语句。对这个 lambda 表达式的求值构造了一个新的 function 对象,其代码由 lambda 表达式指定,其环境是 E1,即在其中对 lambda 表达式求值以产生函数的环境。结果函数对象是调用`make_withdraw`返回的值。这在程序环境中被绑定到`W1`,因为常量声明本身在程序环境中被评估。[图 3.7](#c3-fig-0007) 显示了最终的环境结构。
![c3-fig-0007.jpg](../images/c3-fig-0007.jpg)
![c3-fig-0007.jpg](img/c3-fig-0007.jpg)
[图 3.7](#c3-fig-0007a) 评估`**const** W1 = make_withdraw(100);`的结果。
......@@ -772,13 +772,13 @@ if (balance >= amount) {
由此产生的环境结构如图[图 3.8](#c3-fig-0008) 所示。被求值的表达式同时引用了`amount``balance`。变量`amount`将在环境中的第一帧中找到,而`balance`将通过跟随包围环境指针到 E1 找到。
![c3-fig-0008.jpg](../images/c3-fig-0008.jpg)
![c3-fig-0008.jpg](img/c3-fig-0008.jpg)
[图 3.8](#c3-fig-0008a) 应用功能对象`W1`创建的环境。
当赋值被执行时,`balance`在 E1 的绑定被改变。在完成对`W1`的调用时,`balance`为 50,包含`balance`的框架仍然被函数对象`W1`指向。绑定`amount`的框架(我们在其中执行了改变`balance`的代码)不再相关,因为构建它的函数调用已经终止,并且没有从环境的其他部分指向该框架的指针。下次调用`W1`时,这将构建一个绑定`amount`的新框架,其封闭环境是 E1。我们看到 E1 作为“位置”来保存函数对象`W1`的本地状态变量。[图 3.9](#c3-fig-0009) 显示了调用`W1`后的情况。
![c3-fig-0009.jpg](../images/c3-fig-0009.jpg)
![c3-fig-0009.jpg](img/c3-fig-0009.jpg)
[图 3.9](#c3-fig-0009a) 环境调用到`W1`后。
......@@ -790,7 +790,7 @@ const W2 = make_withdraw(100);
这就产生了[图 3.10](#c3-fig-0010) 的环境结构,说明`W2`是一个函数对象,也就是一对带有一些代码和一个环境。`W2`的环境 E2 是通过调用`make_withdraw`创建的。它包含一个具有自己的本地绑定的框架用于`balance`。另一方面,`W1``W2`具有相同的代码:由`make_withdraw`主体中的 lambda 表达式指定的代码。 [^(17)](#c3-fn-0017) 我们在这里看到为什么`W1``W2`表现为独立的对象。对`W1`的调用引用存储在 E1 的状态变量`balance`,而对`W2`的调用引用存储在 E2 的`balance`。因此,对一个对象的本地状态的更改不会影响另一个对象。
![c3-fig-0010.jpg](../images/c3-fig-0010.jpg)
![c3-fig-0010.jpg](img/c3-fig-0010.jpg)
[图 3.10](#c3-fig-0010a) 使用`**const** W2 = make_withdraw(100);`创建第二个对象。
......@@ -849,7 +849,7 @@ function sqrt(x) {
现在我们可以使用环境模型来看看为什么这些内部声明的行为符合预期。[图 3.11](#c3-fig-0011) 显示了在表达式`sqrt(2)`的求值中,内部函数`is_good_enough``guess`等于 1 时第一次被调用的点。
![c3-fig-0011.jpg](../images/c3-fig-0011.jpg)
![c3-fig-0011.jpg](img/c3-fig-0011.jpg)
[图 3.11](#c3-fig-0011a)`sqrt`功能同内部声明。
......@@ -965,11 +965,11 @@ set_balance(account, new-value)
举个例子,假设`x`被绑定到`list(list("a", "b"), "c", "d")``y`被绑定到`list("e", "f")`,如图[图 3.12](#c3-fig-0012) 所示。对表达式`set_head(x, y)`求值会修改`x`绑定到的对,用`y`的值替换它的`head`。操作结果如图[图 3.13](#c3-fig-0013) 所示。结构`x`已经修改,现在等同于`list(list("e", "f"), "c", "d")`。由被替换的指针标识的代表列表`list("a", "b")`的对现在从原始结构中分离出来。 [^(20)](#c3-fn-0020)
![c3-fig-0012.jpg](../images/c3-fig-0012.jpg)
![c3-fig-0012.jpg](img/c3-fig-0012.jpg)
[图 3.12](#c3-fig-0012a) 列出了`x`:`list(list("a", "b"), "c", "d")``y`:`list("e", "f")`
![c3-fig-0013.jpg](../images/c3-fig-0013.jpg)
![c3-fig-0013.jpg](img/c3-fig-0013.jpg)
[图 3.13](#c3-fig-0013a) 图 3.12 中`set_head(x, y)`对列表的影响。
......@@ -981,11 +981,11 @@ const z = pair(y, tail(x));
`x``y`绑定到图 3.12 的原列表中。名称`z`现在被绑定到由`pair`操作创建的新对;`x`绑定的列表不变。`set_tail`的操作与`set_head`类似。唯一不同的是,替换的是指针对的`tail`指针,而不是`head`指针。执行`set_tail(x, y)`[图 3.12](#c3-fig-0012) 列表的影响如图[图 3.15](#c3-fig-0015) 所示。这里`x``tail`指针已经被指向`list("e", "f")`的指针所取代。还有,曾经是`x``tail`的列表`list("c", "d")`,现在脱离了结构。
![c3-fig-0014.jpg](../images/c3-fig-0014.jpg)
![c3-fig-0014.jpg](img/c3-fig-0014.jpg)
[图 3.14](#c3-fig-0014a) 图 3.12 中`**const** z = pair(y, tail(x));`对列表的影响。
![c3-fig-0015.jpg](../images/c3-fig-0015.jpg)
![c3-fig-0015.jpg](img/c3-fig-0015.jpg)
[图 3.15](#c3-fig-0015a) 图 3.12 中`set_tail(x, y)`对列表的影响。
......@@ -1120,7 +1120,7 @@ const z1 = pair(x, x);
如图[图 3.16](#c3-fig-0016) 所示,`z1`为一对,其`head``tail`均指向同一对`x`。由`z1``head``tail`共享`x``pair`实现的直接方式的结果。一般来说,使用`pair`来构建列表将会产生一个互连的对结构,其中许多单独的对被许多不同的结构共享。
![c3-fig-0016.jpg](../images/c3-fig-0016.jpg)
![c3-fig-0016.jpg](img/c3-fig-0016.jpg)
[图 3.16](#c3-fig-0016a)`pair(x, x)`组成的列表`z1`
......@@ -1132,7 +1132,7 @@ const z2 = pair(list("a", "b"), list("a", "b"));
在这个结构中,两个`list("a", "b")`列表中的对是不同的,尽管它们包含相同的字符串。 [^(22)](#c3-fn-0022)
![c3-fig-0017.jpg](../images/c3-fig-0017.jpg)
![c3-fig-0017.jpg](img/c3-fig-0017.jpg)
[图 3.17](#c3-fig-0017a)`pair(list("a", "b"), list("a", "b"))`组成的列表`z2`
......@@ -1272,7 +1272,7 @@ head(x);
队列是从一端(称为队列的后)插入项目,从另一端(队列的前)删除项目的序列。[图 3.18](#c3-fig-0018) 显示了一个最初为空的队列,其中插入了物品`a``b`。然后移除`a`,插入`c``d`,移除`b`。因为项目总是按照它们被插入的顺序被移除,所以队列有时被称为先进先出缓冲器。
![c3-fig-0018.jpg](../images/c3-fig-0018.jpg)
![c3-fig-0018.jpg](img/c3-fig-0018.jpg)
[图 3.18](#c3-fig-0018a) 队列操作。
......@@ -1312,7 +1312,7 @@ head(x);
然后,队列被表示为一对指针,`front_ptr``rear_ptr`,它们分别指示普通列表中的第一对和最后一对。因为我们希望队列是一个可识别的对象,所以我们可以使用`pair`来组合这两个指针。因此,队列本身将是两个指针的`pair`[图 3.19](#c3-fig-0019) 说明了这种表示。
![c3-fig-0019.jpg](../images/c3-fig-0019.jpg)
![c3-fig-0019.jpg](img/c3-fig-0019.jpg)
[图 3.19](#c3-fig-0019a) 实现一个队列作为一个有前后指针的链表。
......@@ -1363,7 +1363,7 @@ function insert_queue(queue, item) {
}
```
![c3-fig-0020.jpg](../images/c3-fig-0020.jpg)
![c3-fig-0020.jpg](img/c3-fig-0020.jpg)
[图 3.20](#c3-fig-0020a)[图 3.19](#c3-fig-0019) 的队列上使用`insert_queue(q, "d")`的结果。
......@@ -1380,7 +1380,7 @@ function delete_queue(queue) {
}
```
![c3-fig-0021.jpg](../images/c3-fig-0021.jpg)
![c3-fig-0021.jpg](img/c3-fig-0021.jpg)
[图 3.21](#c3-fig-0021a)[图 3.20](#c3-fig-0020) 的队列上使用`delete_queue(q)`的结果。
......@@ -1438,7 +1438,7 @@ b: 2
c: 3
```
![c3-fig-0022.jpg](../images/c3-fig-0022.jpg)
![c3-fig-0022.jpg](img/c3-fig-0022.jpg)
[图 3.22](#c3-fig-0022a) 以表头列表表示的表格。
......@@ -1499,7 +1499,7 @@ function make_table() {
它有两个子表。(子表不需要特殊的头字符串,因为标识子表的键就是为了这个目的。)
![c3-fig-0023.jpg](../images/c3-fig-0023.jpg)
![c3-fig-0023.jpg](img/c3-fig-0023.jpg)
[图 3.23](#c3-fig-0023a) 一个二维表格。
......@@ -1662,13 +1662,13 @@ function memoize(f) {
我们的电路计算模型将由与构成电路的基本元件相对应的对象组成。有根导线,承载个数字信号。一个数字信号在任何时候都可能只有两个可能值 0 和 1 中的一个。还有各种类型的数字功能盒,将携带输入信号的导线连接到其他输出导线。这种盒产生从它们的输入信号计算的输出信号。输出信号延迟的时间取决于功能盒的类型。例如,反相器是反转其输入的原始功能盒。如果反相器的输入信号变为 0,那么一个反相器延迟之后,反相器会将其输出信号变为 1。如果反相器的输入信号变为 1,那么一个反相器延迟之后,反相器会将其输出信号变为 0。我们象征性地绘制一个逆变器,如图 3.24 中的[所示。一个与门,也如图](#c3-fig-0024)[图 3.24](#c3-fig-0024) 所示,是一个具有两个输入和一个输出的原始功能盒。它将其输出信号驱动到输入的逻辑与值。也就是说,如果它的两个输入信号都变为 1,那么一个与门延迟时间之后,与门将迫使它的输出信号为 1;否则输出将为 0。一个或门是一个类似的双输入原始功能盒,它驱动其输出信号为输入的逻辑或值。也就是说,如果至少一个输入信号为 1,则输出将变为 1;否则输出将变成 0。
![c3-fig-0024.jpg](../images/c3-fig-0024.jpg)
![c3-fig-0024.jpg](img/c3-fig-0024.jpg)
[图 3.24](#c3-fig-0024a) 数字逻辑仿真器中的原始函数。
我们可以将原函数连接在一起,构造更复杂的函数。为此,我们将一些功能框的输出连接到其他功能框的输入。例如[图 3.25](#c3-fig-0025) 所示的半加法器电路由一个或门、两个与门和一个反相器组成。它有两个输入信号, A 和 B ,有两个输出信号, S 和 C 。当 A 和 B 中恰好有一个为 1 时 S 变为 1,当 A 和 B 都为 1 时 C 变为 1。从图中可以看出,由于存在延迟,输出可能会在不同的时间生成。数字电路设计中的许多困难都源于这一事实。
![c3-fig-0025.jpg](../images/c3-fig-0025.jpg)
![c3-fig-0025.jpg](img/c3-fig-0025.jpg)
[图 3.25](#c3-fig-0025a) 一种半加法器电路。
......@@ -1731,7 +1731,7 @@ function full_adder(a, b, c_in, sum, c_out) {
已经将`full_adder`定义为一个函数,我们现在可以使用它作为构建模块来创建更复杂的电路。(例如,参见练习 3.30。)
![c3-fig-0026.jpg](../images/c3-fig-0026.jpg)
![c3-fig-0026.jpg](img/c3-fig-0026.jpg)
[图 3.26](#c3-fig-0026a) 一种全加器电路。
......@@ -1804,7 +1804,7 @@ function and_gate(a1, a2, output) {
[图 3.27](#c3-fig-0027) 显示了一个纹波进位加法器由 n 个全加法器串接而成。这是并行加法器的最简单形式,用于将两个 n 位二进制数相加。输入一一 [1] ,一一 [2] ,一一 [3] ,。。。、 A [n] 和 B [1、 B [2] 、 B [3] 、。。。、 B [n] 是要相加的两个二进制数(每个 A [k] 和 B [k] 是 0 或 1)。电路产生 S [1] , S [2] , S [3] ,。。。, S [n] ,nn 位的和,以及 C ,加法运算的进位。编写一个生成该电路的函数`ripple_carry_adder`。该函数应采用三个列表作为参数,每个列表包含三根 n 导线,即 A[k]、 B [k] 和 S[k]——以及另一根导线 C 。纹波进位加法器的主要缺点是需要等待进位信号传播。从一个 n 位纹波进位加法器获得完整输出所需的延迟是多少,用与门、或门和反相器的延迟表示?]
![c3-fig-0027.jpg](../images/c3-fig-0027.jpg)
![c3-fig-0027.jpg](img/c3-fig-0027.jpg)
[图 3.27](#c3-fig-0027a) 一个用于 n 位数的纹波进位加法器。
......@@ -2128,7 +2128,7 @@ dAE = FL
这样的约束可以认为是一个由原语加法器、乘法器和常量约束组成的网络([图 3.28](#c3-fig-0028) )。在图中,我们看到左侧的乘法器盒有三个端子,分别标为 m1、m2 和 p 。这些将倍增器连接到网络的其余部分,如下所示: m [1] 端子连接到连接器 C ,该连接器将保持摄氏温度。 m [2] 端子连接到连接器 w ,该连接器也连接到一个容纳 9。乘法器盒约束为 m*[1]和 m*[2]的乘积的 p 端子连接到另一个乘法器盒的 p 端子,其 m [2] 连接到常数 5,其 m [1]**
**![c3-fig-0028.jpg](../images/c3-fig-0028.jpg)
**![c3-fig-0028.jpg](img/c3-fig-0028.jpg)
[图 3.28](#c3-fig-0028a) 关系 9C = 5(F–32)表示为约束网络。
......@@ -2605,7 +2605,7 @@ balance = balance - amount;
[图 3.29](#c3-fig-0029) 中的时序图描绘了一系列事件,其中`balance`从 100 开始,彼得退出 10,保罗退出 25,然而`balance`的最终值是 75。如图所示,出现这种异常的原因是,保罗将 75 分配给`balance`是在假设要递减的`balance`的值为 100 的情况下进行的。然而,当彼得把`balance`改成 90 时,这个假设就失效了。这对银行系统来说是一个灾难性的失败,因为系统中的货币总量没有得到保存。在交易之前,总金额为 100 美元。后来,彼得有 10 美元,保罗有 25 美元,银行有 75 美元。 [^(41)](#c3-fn-0041)
![c3-fig-0029.jpg](../images/c3-fig-0029.jpg)
![c3-fig-0029.jpg](img/c3-fig-0029.jpg)
[图 3.29](#c3-fig-0029a) 时序图显示了两次银行取款中事件顺序的交错如何导致不正确的最终余额。
......@@ -2617,7 +2617,7 @@ balance = balance - amount;
对并发性的一个可能的限制是规定不能同时发生两个改变任何共享状态变量的操作。这是一个极其严格的要求。对于分布式银行,它要求系统设计者确保一次只能进行一项交易。这既低效又过于保守。[图 3.30](#c3-fig-0030) 显示了彼得和保罗共享一个银行账户,而保罗也有一个私人账户。该图显示了从共享账户中的两次提款(一次由 Peter 提取,一次由 Paul 提取)以及向 Paul 的私人账户中的一次存款。 [^(43)](#c3-fn-0043) 从共享账户的两次提款必须不能并发(因为两者都访问和更新同一个账户),保罗的存款和提款必须不能并发(因为两者都访问和更新保罗钱包中的金额)。但是,允许保罗向他的私人账户存款与彼得从共享账户提款同时进行应该没有问题。
![c3-fig-0030.jpg](../images/c3-fig-0030.jpg)
![c3-fig-0030.jpg](img/c3-fig-0030.jpg)
[图 3.30](#c3-fig-0030a) 银行 1 的联名账户和银行 2 的私人账户同时存取款。
......@@ -3374,7 +3374,7 @@ stream_ref(primes, 50);
思考由`sieve`建立的信号处理系统是很有趣的,如图[图 3.31](#c3-fig-0031) 中的“亨德森图”所示。 [^(63)](#c3-fn-0063) 输入流输入到一个“un `pair` er”中,该“un`pair`er”将流的第一个元素与流的其余部分分开。第一个元素用于构建一个整除滤波器,其余的元素通过该滤波器,滤波器的输出被馈送到另一个筛箱。然后,原始的第一元件被连接到内部筛的输出,以形成输出流。因此,不仅流是无限的,信号处理器也是无限的,因为筛子中包含一个筛子。
![c3-fig-0031.jpg](../images/c3-fig-0031.jpg)
![c3-fig-0031.jpg](img/c3-fig-0031.jpg)
[图 3.31](#c3-fig-0031a) 把素筛看成一个信号处理系统。每条实线代表一个正在传输的值流。从`head``pair``filter`的虚线表示这是单个值而不是流。
......@@ -3444,7 +3444,7 @@ const primes = pair(2,
integers_starting_from(3)));
```
这个定义并不像看起来那么简单,因为我们将通过检查 n 是否能被小于或等于![c3-fig-5001.jpg](../images/c3-fig-5001.jpg)的素数整除来测试一个数 n 是否是素数:
这个定义并不像看起来那么简单,因为我们将通过检查 n 是否能被小于或等于![c3-fig-5001.jpg](img/c3-fig-5001.jpg)的素数整除来测试一个数 n 是否是素数:
```
function is_prime(n) {
......@@ -3459,7 +3459,7 @@ function is_prime(n) {
}
```
这是一个递归定义,因为`primes`是根据`is_prime`谓词定义的,谓词本身使用`primes`流。这个函数起作用的原因是,在任何时候,已经生成了足够多的`primes`流来测试我们接下来需要检查的数字的素性。也就是说,对于我们测试的每一个 n ,要么 n 不是素数(在这种情况下,已经生成了一个素数将其除),要么 n 是素数(在这种情况下,已经生成了一个素数——即,小于 n 的素数——大于![c3-fig-5001.jpg](../images/c3-fig-5001.jpg))。 [^(65)](#c3-fn-0065)
这是一个递归定义,因为`primes`是根据`is_prime`谓词定义的,谓词本身使用`primes`流。这个函数起作用的原因是,在任何时候,已经生成了足够多的`primes`流来测试我们接下来需要检查的数字的素性。也就是说,对于我们测试的每一个 n ,要么 n 不是素数(在这种情况下,已经生成了一个素数将其除),要么 n 是素数(在这种情况下,已经生成了一个素数——即,小于 n 的素数——大于![c3-fig-5001.jpg](img/c3-fig-5001.jpg))。 [^(65)](#c3-fn-0065)
##### 练习 3.53
......@@ -3539,7 +3539,7 @@ function expand(num, den, radix) {
在 2.5.3 节中,我们看到了如何实现一个多项式算术系统,将多项式表示为一列项。用类似的方法,我们可以用幂级数来表示,比如
![c3-fig-5003.jpg](../images/c3-fig-5003.jpg)
![c3-fig-5003.jpg](img/c3-fig-5003.jpg)
表示为无限的流。我们将序列 a[0]+a[1]x+a+[2]x²+a[3]x³+表示为其元素为系数的流。。。
......@@ -3549,9 +3549,9 @@ function expand(num, den, radix) {
c + a0x + a1x2 + a2x3 + a3x4 + · · ·
```
其中 c 是任意常数。定义一个函数`integrate_series`,它将流 a [0] , a [1] , a [2] ,作为输入。。。代表一个幂级数和返回流一个一个 [0] ,![c3-fig-5004.jpg](../images/c3-fig-5004.jpg)一个, [1] ,![c3-fig-5005.jpg](../images/c3-fig-5005.jpg),一个, [2] ,。。。级数的积分的非常数项的系数。(由于结果没有常数项,所以不代表一个幂级数;当我们使用`integrate_series`时,我们将使用`pair`将适当的常量连接到流的开头。)
其中 c 是任意常数。定义一个函数`integrate_series`,它将流 a [0] , a [1] , a [2] ,作为输入。。。代表一个幂级数和返回流一个一个 [0] ,![c3-fig-5004.jpg](img/c3-fig-5004.jpg)一个, [1] ,![c3-fig-5005.jpg](img/c3-fig-5005.jpg),一个, [2] ,。。。级数的积分的非常数项的系数。(由于结果没有常数项,所以不代表一个幂级数;当我们使用`integrate_series`时,我们将使用`pair`将适当的常量连接到流的开头。)
2. b. The function x ![c3-fig-5007.jpg](../images/c3-fig-5007.jpg) e^x is its own derivative. This implies that e^x and the integral of e^x are the same series, except for the constant term, which is e⁰ = 1\. Accordingly, we can generate the series for e^x as
2. b. The function x ![c3-fig-5007.jpg](img/c3-fig-5007.jpg) e^x is its own derivative. This implies that e^x and the integral of e^x are the same series, except for the constant term, which is e⁰ = 1\. Accordingly, we can generate the series for e^x as
```
const exp_series = pair(1, () => integrate_series(exp_series));
......@@ -3628,7 +3628,7 @@ display_stream(sqrt_stream(2));
我们可以用同样的方式处理的另一个迭代是基于我们在第 1.3.1 节中看到的交替序列生成对 π 的近似:
![c3-fig-5008.jpg](../images/c3-fig-5008.jpg)
![c3-fig-5008.jpg](img/c3-fig-5008.jpg)
我们首先生成级数的被加数流(奇数整数的倒数,符号交替)。然后我们取越来越多的项的和的流(使用练习 3.55 的`partial_sums`函数)并将结果缩放 4:
......@@ -3656,7 +3656,7 @@ display_stream(pi_stream);
18 世纪瑞士数学家莱昂哈德·欧拉发明了这样一种加速器,它能很好地处理交替数列(具有交替符号的项的数列)的部分和。在欧拉的技巧中,如果 S[n]是原和序列的第 n 项,那么加速序列有项
![c3-fig-5009.jpg](../images/c3-fig-5009.jpg)
![c3-fig-5009.jpg](img/c3-fig-5009.jpg)
因此,如果原始序列被表示为值流,则变换后的序列由下式给出
......@@ -3914,7 +3914,7 @@ function pairs(s, t) {
我们开始讨论流时,将它们描述为信号处理系统中“信号”的计算模拟。事实上,我们可以使用流以非常直接的方式来模拟信号处理系统,将连续时间间隔内的信号值表示为流的连续元素。例如,我们可以实现一个积分器或加法器,对于一个输入流 x=(x[I])、一个初始值 C 和一个小增量 dt ,累加总和
![c3-fig-5010.jpg](../images/c3-fig-5010.jpg)
![c3-fig-5010.jpg](img/c3-fig-5010.jpg)
并返回值流 S=(S[I])。下面的`integral`函数让人想起整数流的“隐式风格”定义(第 3.5.2 节):
......@@ -3929,7 +3929,7 @@ function integral(integrand, initial_value, dt) {
[图 3.32](#c3-fig-0036) 是对应于`integral`功能的信号处理系统图。输入流由 dt 缩放并通过加法器,加法器的输出通过相同的加法器返回。`integ`定义中的自参考通过反馈回路反映在图中,反馈回路将加法器的输出连接到其中一个输入。
![c3-fig-0032.jpg](../images/c3-fig-0032.jpg)
![c3-fig-0032.jpg](img/c3-fig-0032.jpg)
[图 3.32](#c3-fig-0036a)`integral`功能视为信号处理系统。
......@@ -3937,7 +3937,7 @@ function integral(integrand, initial_value, dt) {
我们可以使用流来模拟电路,以表示一系列时间内的电流或电压值。例如,假设我们有一个由电阻 R 的电阻器和电容 C 的电容器串联而成的 RC 电路。电路对注入电流 i 的电压响应 v 由[图 3.33](#c3-fig-0037) 中的公式确定,其结构如随附的信号流程图所示。
![c3-fig-0033.jpg](../images/c3-fig-0033.jpg)
![c3-fig-0033.jpg](img/c3-fig-0033.jpg)
[图 3.33](#c3-fig-0037a) 一个 RC 电路和相关的信号流程图。
......@@ -3947,7 +3947,7 @@ function integral(integrand, initial_value, dt) {
Alyssa P. Hacker 正在设计一个系统来处理来自物理传感器的信号。她希望产生的一个重要特征是描述输入信号的过零点的信号。也就是说,每当输入信号从负变为正时,结果信号应为+1;每当输入信号从正变为负时,结果信号应为–1;否则,结果信号应为 0。(假设 0 输入的符号为正。)例如,典型的输入信号及其相关的过零信号为
![c3-fig-5011.jpg](../images/c3-fig-5011.jpg)
![c3-fig-5011.jpg](img/c3-fig-5011.jpg)
在 Alyssa 的系统中,来自传感器的信号被表示为流`sense_data`并且流`zero_crossings`是相应的过零流。Alyssa 首先编写一个函数`sign_change_detector`,它将两个值作为参数,并比较这些值的符号以产生一个合适的 0、1 或–1。然后,她按如下方式构建零交叉流:
......@@ -4003,7 +4003,7 @@ const integ = pair(initial_value,
不幸的是,具有循环的系统的流模型可能需要使用超过目前所见的流编程模式的延迟。例如,[图 3.34](#c3-fig-0039) 所示为求解微分方程 dy/dt=f(y)的信号处理系统,其中 f 为给定函数。该图示出了将 f 应用于其输入信号的映射部件,该映射部件在反馈回路中以非常类似于模拟计算机电路的方式链接到积分器,该模拟计算机电路实际上用于求解这种方程。
![c3-fig-0034.jpg](../images/c3-fig-0034.jpg)
![c3-fig-0034.jpg](img/c3-fig-0034.jpg)
[图 3.34](#c3-fig-0039a) 一个求解方程 dy/dt=f(y)的“模拟计算机电路”。
......@@ -4074,11 +4074,11 @@ function integral(integrand, initial_value, dt) {
考虑设计一个信号处理系统来研究齐次二阶线性微分方程的问题
![c3-fig-5012.jpg](../images/c3-fig-5012.jpg)
![c3-fig-5012.jpg](img/c3-fig-5012.jpg)
建模为 y 的输出流由包含环路的网络生成。这是因为 d²y/dt²的值取决于 y 和 dy / dt 的值,而这两者都是通过对 d²y/dt^(2^(的积分来确定的我们要编码的图表如[图 3.35](#c3-fig-0040) 所示。编写一个函数`solve_2nd`,该函数将常量 a 、 b 和 dt 以及初始值 y [0] 和 dy [0] 作为参数,并生成连续值 y))
![c3-fig-0035.jpg](../images/c3-fig-0035.jpg)
![c3-fig-0035.jpg](img/c3-fig-0035.jpg)
[图 3.35](#c3-fig-0040a) 二阶线性微分方程解的信号流图。
......@@ -4090,7 +4090,7 @@ function integral(integrand, initial_value, dt) {
串联 RLC 电路由一个电阻、一个电容和一个电感串联而成,如图[图 3.36](#c3-fig-0041) 所示。如果 R 、 L 和 C 是电阻、电感和电容,那么这三个元件的电压( v )和电流( i )之间的关系由以下等式描述
![c3-fig-5013.jpg](../images/c3-fig-5013.jpg)
![c3-fig-5013.jpg](img/c3-fig-5013.jpg)
并且电路连接规定了关系
......@@ -4101,19 +4101,19 @@ vC = vL + vR
结合这些等式可以看出,电路的状态(由电容器两端的电压 v[C]和电感器中的电流 I[L]概括)由一对微分方程描述
![c3-fig-5014.jpg](../images/c3-fig-5014.jpg)
![c3-fig-5014.jpg](img/c3-fig-5014.jpg)
代表该微分方程系统的信号流图如图 3.37 所示。
![c3-fig-0036.jpg](../images/c3-fig-0036.jpg)
![c3-fig-0036.jpg](img/c3-fig-0036.jpg)
[图 3.36](#c3-fig-0041a) 一系列 RLC 电路。
![c3-fig-0037.jpg](../images/c3-fig-0037.jpg)
![c3-fig-0037.jpg](img/c3-fig-0037.jpg)
[图 3.37](#c3-fig-0042a) 串联 RLC 电路解决方案的信号流图。
编写一个函数`RLC`,将电路的参数 R 、 L 和 C 以及时间增量 dt 作为参数。以类似于练习 3.73 的`RC`函数的方式,`RLC`应该产生一个函数,该函数取状态变量 5 ![c3-fig-5015.jpg](../images/c3-fig-5015.jpg)和![c3-fig-5016.jpg](../images/c3-fig-5016.jpg)的初始值,并产生一对(使用`pair`)状态流 v [ C ] 和 i [ L ] 。使用`RLC`,生成模拟串联 RLC 电路行为的一对流,其中 R = 1 欧姆, C = 0.2 法拉, L = 1 亨利, dt = 0.1 秒,初始值![c3-fig-5016.jpg](../images/c3-fig-5016.jpg) = 0 安培,![c3-fig-5015.jpg](../images/c3-fig-5015.jpg) = 10 伏。
编写一个函数`RLC`,将电路的参数 R 、 L 和 C 以及时间增量 dt 作为参数。以类似于练习 3.73 的`RC`函数的方式,`RLC`应该产生一个函数,该函数取状态变量 5 ![c3-fig-5015.jpg](img/c3-fig-5015.jpg)和![c3-fig-5016.jpg](img/c3-fig-5016.jpg)的初始值,并产生一对(使用`pair`)状态流 v [ C ] 和 i [ L ] 。使用`RLC`,生成模拟串联 RLC 电路行为的一对流,其中 R = 1 欧姆, C = 0.2 法拉, L = 1 亨利, dt = 0.1 秒,初始值![c3-fig-5016.jpg](img/c3-fig-5016.jpg) = 0 安培,![c3-fig-5015.jpg](img/c3-fig-5015.jpg) = 10 伏。
##### 正常顺序评估
......@@ -4226,7 +4226,7 @@ function stream_withdraw(balance, amount_stream) {
另一方面,如果我们仔细观察,我们可以看到与时间相关的问题也悄悄进入功能模型。当我们希望设计交互系统时,尤其是对独立实体之间的交互进行建模时,会出现一个特别麻烦的领域。例如,再次考虑允许联合银行账户的银行系统的实现。在使用赋值和对象的传统系统中,我们将通过让 Peter 和 Paul 向同一个银行帐户对象发送他们的交易请求来模拟 Peter 和 Paul 共享一个帐户的事实,正如我们在 3.1.3 节中看到的。从流的角度来看,这里没有“对象”本身,我们已经指出,银行帐户可以被建模为一个流程,该流程对一个事务请求流进行操作以产生一个响应流。因此,我们可以通过将彼得的交易请求流与保罗的请求流合并,并将结果提供给银行账户流流程,来模拟彼得和保罗拥有联合银行账户的事实,如图[图 3.38](#c3-fig-0043) 所示。
![c3-fig-0038.jpg](../images/c3-fig-0038.jpg)
![c3-fig-0038.jpg](img/c3-fig-0038.jpg)
[图 3.38](#c3-fig-0043a) 一个联名银行账户,通过合并两个交易请求流来建模。
......@@ -4371,7 +4371,7 @@ v_prod(temp1,temp2,答案);
这使用了练习 3.50 中的函数`stream_map_2`
[65](#c3-fn-0065a) 最后这一点非常微妙,依赖于 p[n][+1]≤![c3-fig-5002.jpg](../images/c3-fig-5002.jpg)。(这里,p[k]表示第 k 个素数。诸如此类的估计很难确定。欧几里德关于素数有无穷多个的古老证明表明,p[n][+1]≤p[1]p[2]p[n]+1,直到 1851 年俄罗斯数学家 P. L .切比雪夫建立了这个结果最初是在 1845 年推测出来的,被称为伯特兰假说。一个证明可以在 Hardy 和 Wright 1960 年的第 22.3 节中找到。
[65](#c3-fn-0065a) 最后这一点非常微妙,依赖于 p[n][+1]≤![c3-fig-5002.jpg](img/c3-fig-5002.jpg)。(这里,p[k]表示第 k 个素数。诸如此类的估计很难确定。欧几里德关于素数有无穷多个的古老证明表明,p[n][+1]≤p[1]p[2]p[n]+1,直到 1851 年俄罗斯数学家 P. L .切比雪夫建立了这个结果最初是在 1845 年推测出来的,被称为伯特兰假说。一个证明可以在 Hardy 和 Wright 1960 年的第 22.3 节中找到。
这个练习展示了按需呼叫是如何与练习 3.27 中描述的普通记忆紧密相关的。在那个练习中,我们使用赋值来显式地构造一个本地表。我们的按需调用流优化有效地自动构建了这样一个表,将值存储在流的先前强制部分中。
......
......@@ -40,7 +40,7 @@
这两个规则描述了评估过程的本质,一个基本循环,在这个循环中,要在环境中评估的语句和表达式被简化为要应用于参数的函数,这些函数又被简化为要在新环境中评估的新语句和表达式,以此类推,直到我们深入到名称(其值在环境中被查找)以及直接应用的运算符和原始函数(参见[图 4.1](#c4-fig-0001) )。 [](#c4-fn-0004) 这个评估周期将通过评估器中两个关键函数`evaluate``apply`的相互作用来体现,这在 4.1.1 节中有描述(参见[图 4.1](#c4-fig-0001) )。
![c4-fig-0001.jpg](../images/c4-fig-0001.jpg)
![c4-fig-0001.jpg](img/c4-fig-0001.jpg)
[图 4.1](#c4-fig-0001a)`evaluate``apply`循环暴露了一种计算机语言的本质。
......@@ -282,7 +282,7 @@ list("sequence",
[图 4.2](#c4-fig-0002) 描绘了由语法谓词和选择器形成的抽象屏障,它们将评估器与程序的标记列表表示连接起来,而标记列表表示又通过`parse`与字符串表示分离。下面我们描述程序组件的解析,并列出相应的语法谓词和选择器,以及构造函数(如果需要的话)。
![c4-fig-0002.jpg](../images/c4-fig-0002.jpg)
![c4-fig-0002.jpg](img/c4-fig-0002.jpg)
[图 4.2](#c4-fig-0002a) 评估器中的语法抽象。
......@@ -1108,13 +1108,13 @@ function factorial(n) {
我们可以把这个程序看作是对一台机器的描述,这台机器包含递减、相乘和相等测试的部件,还有一个两位开关和另一台阶乘机器。(阶乘机器是无限的,因为它包含另一个阶乘机器。)[图 4.3](#c4-fig-0003) 是阶乘机器的流程图,显示了各部分是如何连接在一起的。
![c4-fig-0003.jpg](../images/c4-fig-0003.jpg)
![c4-fig-0003.jpg](img/c4-fig-0003.jpg)
[图 4.3](#c4-fig-0003a) 阶乘程序,被视为抽象机器。
同样,我们可以把评价者看作一台非常特殊的机器,它接受对机器的描述作为输入。给定该输入,评估器对自身进行配置以仿真所描述的机器。例如,如果我们将`factorial`的定义提供给评估者,如图[图 4.4](#c4-fig-0004) 所示,评估者将能够计算阶乘。
![c4-fig-0004.jpg](../images/c4-fig-0004.jpg)
![c4-fig-0004.jpg](img/c4-fig-0004.jpg)
[图 4.4](#c4-fig-0004a) 仿真阶乘机的评估器。
......@@ -3704,7 +3704,7 @@ job($x, list("computer", "programmer"))
在我们的系统中,一个查询获取一个输入帧流,并对该流中的每一帧执行上述匹配操作,如图[图 4.5](#c4-fig-0005) 所示。也就是说,对于输入流中的每个帧,查询通过匹配数据库中的断言来生成一个新的流,该流由该帧的所有扩展组成。所有这些流然后被组合成一个巨大的流,它包含输入流中每个帧的所有可能的扩展。这个流是查询的输出。
![c4-fig-0005.jpg](../images/c4-fig-0005.jpg)
![c4-fig-0005.jpg](img/c4-fig-0005.jpg)
[图 4.5](#c4-fig-0005a) 一个查询处理一个帧流。
......@@ -3733,13 +3733,13 @@ job($person, $x)
以与给定的`$x`绑定一致的方式。每个这样的匹配将产生一个包含`$x``$person`绑定的帧。两个查询的`and`可以看作是两个分量查询的串联组合,如图[图 4.6](#c4-fig-0006) 所示。通过第一个查询过滤器的帧被第二个查询过滤并进一步扩展。
![c4-fig-0006.jpg](../images/c4-fig-0006.jpg)
![c4-fig-0006.jpg](img/c4-fig-0006.jpg)
[图 4.6](#c4-fig-0006a) 两个查询的`and`组合是通过对连续的帧流进行操作产生的。
[图 4.7](#c4-fig-0007) 显示了计算两个查询的`or`的类似方法,作为两个组件查询的并行组合。每个查询分别扩展输入的帧流。这两个结果流然后被合并以产生最终的输出流。
![c4-fig-0007.jpg](../images/c4-fig-0007.jpg)
![c4-fig-0007.jpg](img/c4-fig-0007.jpg)
[图 4.7](#c4-fig-0007a) 两个查询的`or`组合是通过对帧流并行操作并合并结果产生的。
......@@ -4518,7 +4518,7 @@ list("job ",list("name "," $x "),list("computer "," wizard"))
查询系统函数,比如 4.4.4.5 部分的`add_rule_or_assertion`和 4.4.4.2 部分的`evaluate_query`,使用选择器和谓词,比如下面声明的`type``contents``is_rule``first_conjunct`,对特定于查询语言的表示进行操作。[图 4.8](#c4-fig-0008) 描述了查询系统使用的三个抽象障碍,以及转换函数`parse``unparse``convert_to_query_syntax`如何桥接它们。
![c4-fig-0008.jpg](../images/c4-fig-0008.jpg)
![c4-fig-0008.jpg](img/c4-fig-0008.jpg)
[图 4.8](#c4-fig-0008a) 查询系统中的语法抽象。
......
......@@ -29,7 +29,7 @@ function gcd(a, b) {
我们可以使用[图 5.1](#c5-fig-0001) 中所示的数据路径图来说明该机器所需的寄存器和操作。在该图中,寄存器(`a``b``t`)由矩形表示。向寄存器赋值的每种方式都由一个箭头表示,箭头后面有一个按钮(绘制为),从数据源指向寄存器。按下按钮时,该按钮允许源端的值“流入”指定的寄存器。每个按钮旁边的标签是我们用来指代该按钮的名称。这些名称是任意的,可以选择具有助记值的名称(例如,`a<-b`表示按下将寄存器`b`的内容分配给寄存器`a`的按钮)。一个寄存器的数据来源可以是另一个寄存器(如在`a<-b`赋值中),一个运算结果(如在`t<-r`赋值中),或一个常数(一个不能改变的内置值,在数据路径图中用包含常数的三角形表示)。
![c5-fig-0001.jpg](../images/c5-fig-0001.jpg)
![c5-fig-0001.jpg](img/c5-fig-0001.jpg)
[图 5.1](#c5-fig-0001a)GCD 机床的数据路径。
......@@ -37,7 +37,7 @@ function gcd(a, b) {
为了让数据路径实际计算 gcd,必须按正确的顺序按下按钮。我们将根据控制器图来描述这个序列,如图[图 5.2](#c5-fig-0002) 所示。控制器图的元素指示数据路径组件应该如何操作。控制器图中的矩形框标识要按下的数据路径按钮,箭头描述从一个步骤到下一个步骤的顺序。图中的菱形代表一个决定。根据菱形中标识的数据路径测试值,将遵循两个排序箭头中的一个。我们可以从物理类比的角度来解释控制器:把这个图想象成一个弹子在其中滚动的迷宫。当弹球滚进一个盒子时,它会按下由盒子命名的数据路径按钮。当弹球滚入一个决策节点时(比如对`b` = 0 的测试),它会离开由指示的测试结果所确定的路径上的节点。
![c5-fig-0002.jpg](../images/c5-fig-0002.jpg)
![c5-fig-0002.jpg](img/c5-fig-0002.jpg)
[图 5.2](#c5-fig-0002a)GCD 机的控制器。
......@@ -76,7 +76,7 @@ function factorial(n) {
[图 5.3](#c5-fig-0003) 显示了这样描述的 GCD 机器。这个例子仅仅暗示了这些描述的一般性,因为 GCD 机器是一个非常简单的例子:每个寄存器只有一个按钮,每个按钮和测试在控制器中只使用一次。
![c5-fig-0003.jpg](../images/c5-fig-0003.jpg)
![c5-fig-0003.jpg](img/c5-fig-0003.jpg)
[图 5.3](#c5-fig-0003a) 一台规格的 GCD 机。
......@@ -124,7 +124,7 @@ perform(list(op("display"), reg("a")))
[图 5.4](#c5-fig-0004) 显示了新 GCD 机器的数据路径和控制器。我们没有让机器在打印出答案后停下来,而是让它重新开始,这样它可以重复读取一对数字,计算它们的 GCD,并打印出结果。这个结构就像我们在第四章的解释器中使用的驱动循环。
![c5-fig-0004.jpg](../images/c5-fig-0004.jpg)
![c5-fig-0004.jpg](img/c5-fig-0004.jpg)
[图 5.4](#c5-fig-0004a) 读取输入并打印结果的 GCD 机器。
......@@ -150,11 +150,11 @@ assign("t", list(op("rem"), reg("a"), reg("b")))
在 GCD 中,控制器定义被包含一个循环的指令序列所取代,如图[图 5.6](#c5-fig-0006) 所示。
![c5-fig-0005.jpg](../images/c5-fig-0005.jpg)
![c5-fig-0005.jpg](img/c5-fig-0005.jpg)
[图 5.5](#c5-fig-0005a) 精心设计的 GCD 机床的数据路径和控制器。
![c5-fig-0006.jpg](../images/c5-fig-0006.jpg)
![c5-fig-0006.jpg](img/c5-fig-0006.jpg)
[图 5.6](#c5-fig-0006a) 图 5.5 中 GCD 机控制器指令序列。
......@@ -185,19 +185,19 @@ function sqrt(x) {
当设计一台执行计算的机器时,我们通常更喜欢安排组件由计算的不同部分共享,而不是复制组件。考虑一个包含两个 GCD 计算的机器——一个计算寄存器`a``b`内容的 GCD,另一个计算寄存器`c``d`内容的 GCD。我们可以从假设我们有一个原始的`gcd`操作开始,然后根据更多的原始操作扩展`gcd`的两个实例。[图 5.7](#c5-fig-0007) 只显示了最终机器数据路径的 GCD 部分,没有显示它们如何连接到机器的其余部分。该图还显示了机器控制器序列的相应部分。
![c5-fig-0007.jpg](../images/c5-fig-0007.jpg)
![c5-fig-0007.jpg](img/c5-fig-0007.jpg)
[图 5.7](#c5-fig-0007a) 带有两个 GCD 计算的机器的数据路径和控制器序列部分。
这台机器有两个余数操作箱和两个相等测试箱。如果复制的部件很复杂,就像余料箱一样,这将不是制造机器的经济方法。我们可以通过对两个 GCD 计算使用相同的组件来避免复制数据路径组件,只要这样做不会影响更大机器的其余计算。如果控制器到达`gcd_2`时不需要寄存器`a``b`中的值(或者如果这些值可以移动到其他寄存器进行保管),我们可以改变机器,使其在计算第二个和第一个 GCD 时使用寄存器`a``b`,而不是寄存器`c``d`。如果我们这样做,我们将获得如图 5.8 所示的控制器序列。
![c5-fig-0008.jpg](../images/c5-fig-0008.jpg)
![c5-fig-0008.jpg](img/c5-fig-0008.jpg)
[图 5.8](#c5-fig-0008a) 使用相同数据路径组件进行两种不同 GCD 计算的机器的控制器序列部分。
我们已经移除了重复的数据路径组件(因此数据路径再次如图[图 5.1](#c5-fig-0001) 所示),但是控制器现在有两个 GCD 序列,它们的不同之处仅在于入口点标签。最好是将这两个序列替换成一个单独的序列——一个`gcd` 子程序——在这个序列的末尾,我们分支回到主指令序列中的正确位置。我们可以这样完成:在转移到`gcd`之前,我们将一个区别值(比如 0 或 1)放入一个特殊的寄存器`continue`。在`gcd`子程序结束时,我们返回到`after_gcd_1``after_gcd_2`,这取决于`continue`寄存器的值。[图 5.9](#c5-fig-0009) 显示了生成的控制器序列的相关部分,其中仅包含一份`gcd`指令。
![c5-fig-0009.jpg](../images/c5-fig-0009.jpg)
![c5-fig-0009.jpg](img/c5-fig-0009.jpg)
[图 5.9](#c5-fig-0009a) 使用`continue`寄存器避免[图 5.8](#c5-fig-0008) 中的重复控制器序列。
......@@ -205,7 +205,7 @@ function sqrt(x) {
为了反映这种能力,我们将扩展 registermachine 语言的`assign`指令,以允许一个寄存器被指定为来自控制器序列的值 a 标签(作为一种特殊的常量)。我们还将扩展`go_to`指令,以允许在寄存器内容描述的入口点继续执行,而不仅仅是在常量标签描述的入口点继续执行。使用这些新的构造,我们可以用一个到存储在`continue`寄存器中的位置的分支来终止`gcd`子程序。这导致图 5.10 中[所示的控制器序列。](#c5-fig-0010)
![c5-fig-0010.jpg](../images/c5-fig-0010.jpg)
![c5-fig-0010.jpg](img/c5-fig-0010.jpg)
[图 5.10](#c5-fig-0010a)`continue`寄存器分配标签简化并概括了[图 5.9](#c5-fig-0009) 所示的策略。
......@@ -249,7 +249,7 @@ function gcd(a, b) {
[图 5.11](#c5-fig-0011) 显示了实现递归`factorial`功能的机器的数据路径和控制器。机器有一个堆栈和三个寄存器,称为`n``val``continue`。为了简化数据路径图,我们没有命名寄存器分配按钮,只命名了堆栈操作按钮(`sc``sn`保存寄存器,`rc``rn`恢复寄存器)。为了操作机器,我们将希望计算其阶乘的数字放入寄存器`n`并启动机器。当机器到达`fact_done`时,计算结束,答案将在`val`寄存器中找到。在控制器序列中,`n``continue`在每次递归调用之前保存,并在调用返回时恢复。通过分支到存储在`continue`中的位置来完成呼叫返回。当机器启动时,寄存器`continue`被初始化,以便最后一次返回将到达`fact_done`。保存阶乘计算结果的`val`寄存器在递归调用之前没有保存,因为在子例程返回之后`val`的旧内容不再有用。只需要新值,即子计算产生的值。
![c5-fig-0011.jpg](../images/c5-fig-0011.jpg)
![c5-fig-0011.jpg](img/c5-fig-0011.jpg)
[图 5.11](#c5-fig-0011a) 递归阶乘机。
......@@ -271,7 +271,7 @@ function fib(n) {
就像阶乘一样,我们可以用寄存器`n``val``continue`来实现递归斐波那契计算。该机器比用于阶乘的机器更复杂,因为在控制器序列中有两个地方我们需要执行递归调用——一次是计算 Fib(n–1),一次是计算 Fib(n–2)。为了设置这些调用,我们保存稍后需要其值的寄存器,将`n`寄存器设置为我们需要递归计算其 Fib 的数字(n–1 或 n–2),并将主序列中要返回的入口点分配给`continue`(分别为`afterfib_n_1``afterfib_n_2`)。我们接着去`fib_loop`。当我们从递归调用返回时,答案在`val`中。图 5.12 显示了该机器的控制器顺序。
![c5-fig-0012.jpg](../images/c5-fig-0012.jpg)
![c5-fig-0012.jpg](img/c5-fig-0012.jpg)
[图 5.12](#c5-fig-0012a) 用于计算斐波那契数列的机器控制器。
......@@ -502,7 +502,7 @@ function push(stack, value) {
`make_new_machine`函数,如图[图 5.13](#c5-fig-0013) 所示,构造了一个对象,其本地状态由一个堆栈、一个最初为空的指令序列、一个最初包含一个初始化堆栈操作的操作列表以及一个最初包含两个名为`flag``pc`(代表“程序计数器”)的注册表。内部函数`allocate_register`向注册表添加新条目,内部函数`lookup_register`在表中查找寄存器。
![c5-fig-0013.jpg](../images/c5-fig-0013.jpg)
![c5-fig-0013.jpg](img/c5-fig-0013.jpg)
[图 5.13](#c5-fig-0013a)`make_new_machine`功能实现了基本机型。
......@@ -1124,7 +1124,7 @@ cancel_all_breakpoints(machine)
我们可以使用向量来实现列表结构存储器所需的基本对结构。我们假设计算机内存分为两个向量:`the_heads``the_tails`。我们将如下表示列表结构:指向一对的指针是两个向量的索引。该对的`head`是指定索引的`the_heads`中的条目,该对的尾部是指定索引的`the_tails`中的条目。我们还需要一种对象而不是对的表示(比如数字和字符串),以及一种区分不同类型数据的方法。有许多方法可以实现这一点,但它们都归结为使用类型化指针,也就是说,扩展“指针”的概念,以包含关于数据类型的信息。 [](#c5-fn-0009) 数据类型使系统能够区分指向一对(由“对”数据类型和内存向量索引组成)的指针和指向其他类型数据(由其他数据类型和用于表示该类型数据的任何内容组成)的指针。如果两个数据对象的指针相同,则认为它们是相同的(`===`)。[图 5.14](#c5-fig-0014) 说明了使用这种方法来表示`list(list(1, 2), 3, 4)`,其盒指针图也已显示。我们使用字母前缀来表示数据类型信息。因此,指向具有索引 5 的对的指针被表示为`p5`,空列表由指针`e0`表示,而指向数字 4 的指针被表示为`n4`。在盒指针图中,我们在每一对的左下方指示了向量索引,该索引指定了该对的`head``tail`的存储位置。`the_heads``the_tails`中的空白位置可能包含其他列表结构的部分(这里不感兴趣)。
![c5-fig-0014.jpg](../images/c5-fig-0014.jpg)
![c5-fig-0014.jpg](img/c5-fig-0014.jpg)
[图 5.14](#c5-fig-0014a) 列表的盒指针和内存向量表示法`list(list(1, 2), 3, 4)`
......@@ -1269,7 +1269,7 @@ accumulate((x, y) => x + y,
当我们耗尽当前工作内存中的空闲单元时,也就是说,当一个`pair`操作试图将`free`指针递增到内存向量的末尾之外时,垃圾收集被触发。当垃圾收集过程完成时,`root`指针将指向新内存,所有可从`root`访问的对象将被移动到新内存,并且`free`指针将指示新内存中可分配新对的下一个位置。此外,工作内存和新内存的角色将互换——新的内存对将在新内存中构建,从`free`指示的位置开始,并且(先前的)工作内存将可用作下一次垃圾收集的新内存。[图 5.15](#c5-fig-0015) 显示了垃圾收集前后的内存安排。
![c5-fig-0015.jpg](../images/c5-fig-0015.jpg)
![c5-fig-0015.jpg](img/c5-fig-0015.jpg)
[图 5.15](#c5-fig-0015a) 垃圾收集进程对内存的重新配置。
......@@ -1374,7 +1374,7 @@ accumulate((x, y) => x + y,
在 5.1 节中,我们看到了如何将简单的 JavaScript 程序转换成注册机器的描述。我们现在将在一个更复杂的程序上执行这种转换,即 4 . 1 . 1–4 . 1 . 4 节的元循环求值器,它展示了如何用函数`evaluate``apply`来描述 JavaScript 解释器的行为。我们在本节开发的显式控制评估器展示了评估过程中使用的底层函数调用和参数传递机制是如何根据寄存器和堆栈上的操作来描述的。此外,显式控制评估器可以作为 JavaScript 解释器的实现,用与传统计算机的本机语言非常相似的语言编写。评估器可以由 5.2 节的寄存器机器模拟器执行。或者,它可以作为构建 JavaScript 评估器的机器语言实现的起点,甚至可以作为评估 JavaScript 程序的专用机器的起点。[图 5.16](#c5-fig-0016) 显示了这样一个硬件实现:一个充当 Scheme 评估器的硅片,Scheme 是本书最初版本中代替 JavaScript 使用的语言。芯片设计者从与本节描述的评估器相似的寄存器机器的数据路径和控制器规格开始,并使用设计自动化程序来构建集成电路布局。 [^(20)](#c5-fn-0020)
![c5-fig-0016.jpg](../images/c5-fig-0016.jpg)
![c5-fig-0016.jpg](img/c5-fig-0016.jpg)
[图 5.16](#c5-fig-0016a) 一种方案评估器的硅片实现。
......@@ -2468,7 +2468,7 @@ lambda 主体的这个简单转换是确保不显式返回的函数具有返回
`append_return_undefined`目前的设计有点粗糙:它总是在 lambda 主体后面附加一个`**return** undefined;`,即使在主体的每个执行路径中已经有一个 return 语句。重写`append_return_undefined`,以便它只在那些不包含 return 语句的路径末尾插入`**return** undefined;`。在下面的函数上测试您的解决方案,用任何表达式替换 e1 和 e2,用任何(非返回)语句替换 s1 和 s2。在`t`中,应在两个`(*)`处或仅在`(**)`处添加 return 语句。在`w``h`中,应在其中一个`(*)`处添加返回语句。在`m`中,不应添加返回语句。
![c5-fig-5001.jpg](../images/c5-fig-5001.jpg)
![c5-fig-5001.jpg](img/c5-fig-5001.jpg)
### 5.5.3 编制申请书和申报表
......@@ -2976,8 +2976,8 @@ return n === 1
错误分支的代码是另一个函数调用,其中函数是符号`"*"`的值,参数是`n`和另一个函数调用的结果(对`factorial`的调用)。这些调用中的每一个都建立了`fun``argl`以及它自己的原始和复合分支。[图 5.17](#c5-fig-0018) 显示了`factorial`函数声明的完整编译。注意,如上所示,谓词周围的`continue``env`的可能的`save``restore`实际上是生成的,因为这些寄存器被谓词中的函数调用修改,并且需要用于分支中的函数调用和`"return"`链接。
![c5-fig-0017a.jpg](../images/c5-fig-0017a.jpg)
![c5-fig-0017b.jpg](../images/c5-fig-0017b.jpg)
![c5-fig-0017a.jpg](img/c5-fig-0017a.jpg)
![c5-fig-0017b.jpg](img/c5-fig-0017b.jpg)
[图 5.17](#c5-fig-0018a) 编译声明的`factorial`功能。
......@@ -3016,8 +3016,8 @@ function factorial(n) {
编译了什么程序来产生如图[图 5.18](#c5-fig-0021) 所示的代码?
![c5-fig-0018a.jpg](../images/c5-fig-0018a.jpg)
![c5-fig-0018b.jpg](../images/c5-fig-0018b.jpg)
![c5-fig-0018a.jpg](img/c5-fig-0018a.jpg)
![c5-fig-0018b.jpg](img/c5-fig-0018b.jpg)
[图 5.18](#c5-fig-0021a) 编译器输出的一个例子。见练习 5.38。
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册