From 3fac9e3ad2a7a70d359f2e53d86c701eb4c6557f Mon Sep 17 00:00:00 2001 From: wizardforcel <562826179@qq.com> Date: Thu, 30 Mar 2023 17:07:10 +0800 Subject: [PATCH] 2023-03-30 17:07:10 --- docs/05.md | 20 +++---- docs/06.md | 106 +++++++++++++++++------------------ docs/07.md | 162 ++++++++++++++++++++++++++--------------------------- docs/08.md | 38 ++++++------- docs/09.md | 76 ++++++++++++------------- 5 files changed, 201 insertions(+), 201 deletions(-) diff --git a/docs/05.md b/docs/05.md index 10187d2..ace6b1e 100644 --- a/docs/05.md +++ b/docs/05.md @@ -183,11 +183,11 @@ circumference; (2 + 4 * 6) * (3 + 12); ``` -要求评估规则应用于四种不同的组合。我们可以通过用树的形式表示这种组合来获得这个过程的图片,如图[图 1.1](#c1-fig-0001) 所示。每个组合都由一个节点表示,节点的分支对应于该组合的运算符和操作数。终端节点(即没有分支的节点)代表运算符或数字。从树的角度来看求值,我们可以想象操作数的值向上渗透,从终端节点开始,然后在越来越高的级别上组合。总的来说,我们将会看到递归是一种非常强大的处理层次化、树状对象的技术。事实上,评估规则的“向上过滤值”形式是一种被称为树累积的通用过程的示例。 +要求评估规则应用于四种不同的组合。我们可以通过用树的形式表示这种组合来获得这个过程的图片,如图图 1.1:所示。每个组合都由一个节点表示,节点的分支对应于该组合的运算符和操作数。终端节点(即没有分支的节点)代表运算符或数字。从树的角度来看求值,我们可以想象操作数的值向上渗透,从终端节点开始,然后在越来越高的级别上组合。总的来说,我们将会看到递归是一种非常强大的处理层次化、树状对象的技术。事实上,评估规则的“向上过滤值”形式是一种被称为树累积的通用过程的示例。 ![c1-fig-0001.jpg](img/c1-fig-0001.jpg) -[图 1.1](#c1-fig-0001a) 树形表示,显示每个子表达式的值。 +图 1.1:树形表示,显示每个子表达式的值。 接下来,观察第一步的重复应用将我们带到了需要评估的点,不是组合,而是数字或名称等原始表达式。我们通过规定来处理原始情况 @@ -715,11 +715,11 @@ function sqrt_iter(guess, x) { 函数`sqrt`是由一组相互定义的函数定义的流程的第一个例子。注意`sqrt_iter`的声明是递归;也就是说,函数是根据它本身来定义的。能够根据函数本身来定义函数的想法可能会令人不安;这种“循环”定义怎么可能有意义,这似乎还不清楚,更不用说指定一个由计算机执行的明确定义的过程了。这将在第 1.2 节中详细讨论。但是首先让我们考虑一下`sqrt`这个例子所展示的其他一些要点。 -请注意,计算平方根的问题自然会分解成许多子问题:如何判断猜测是否足够好,如何改进猜测,等等。这些任务中的每一项都由单独的功能来完成。整个`sqrt`程序可以被视为一簇功能(如图[图 1.2](#c1-fig-0008) 所示),反映了问题分解成子问题。 +请注意,计算平方根的问题自然会分解成许多子问题:如何判断猜测是否足够好,如何改进猜测,等等。这些任务中的每一项都由单独的功能来完成。整个`sqrt`程序可以被视为一簇功能(如图图 1.2:所示),反映了问题分解成子问题。 ![c1-fig-0002.jpg](img/c1-fig-0002.jpg) -[图 1.2](#c1-fig-0008a)`sqrt`程序的功能分解。 +图 1.2:`sqrt`程序的功能分解。 这种分解策略的重要性不仅仅在于将程序分成几个部分。毕竟,我们可以把任何一个大程序分成几个部分——前十行,接下来十行,接下来十行,等等。相反,每个功能完成一个可识别的任务是至关重要的,这个任务可以作为定义其他功能的模块。例如,当我们根据`square`定义`is_good_enough`函数时,我们可以将`square`函数视为一个“黑盒”。我们现在并不关心函数如何计算结果,只关心函数计算平方的事实。如何计算平方的细节可以省略,稍后再考虑。事实上,就`is_good_enough`功能而言,`square`并不完全是一个功能,而是一个功能的抽象,即所谓的功能抽象。在这个抽象层次上,任何计算平方的函数都是一样好的。 @@ -865,11 +865,11 @@ function factorial(n) { } ``` -我们可以用 1.1.5 节的替代模型来观看动作计算 6 中的这个函数!,如图[图 1.3](#c1-fig-0009) 所示。 +我们可以用 1.1.5 节的替代模型来观看动作计算 6 中的这个函数!,如图图 1.3:所示。 ![c1-fig-0003.jpg](img/c1-fig-0003.jpg) -[图 1.3](#c1-fig-0009a) 一个用于计算 6 的线性递归过程!。 +图 1.3:一个用于计算 6 的线性递归过程!。 现在让我们从不同的角度来看阶乘的计算。我们可以描述一个计算规则。通过指定我们首先将 1 乘以 2,然后将结果乘以 3,然后乘以 4,等等,直到我们到达 n 。更正式的说法是,我们维护一个运行的产品,以及一个从 1 数到 n 的计数器。我们可以这样描述计算:计数器和乘积根据规则同时从一个步骤变化到下一个步骤 @@ -895,11 +895,11 @@ function fact_iter(product, counter, max_count) { } ``` -和前面一样,我们可以用代入模型来形象化计算 6 的过程!,如图[图 1.4](#c1-fig-0010) 所示。 +和前面一样,我们可以用代入模型来形象化计算 6 的过程!,如图图 1.4:所示。 ![c1-fig-0004.jpg](img/c1-fig-0004.jpg) -[图 1.4](#c1-fig-0010a) 用于计算 6 的线性迭代过程!。 +图 1.4:用于计算 6 的线性迭代过程!。 比较这两个过程。从一个角度来看,他们似乎没有什么不同。两者在相同的域上计算相同的数学函数,并且每一个都需要与 n 成比例的步骤来计算 n !。事实上,这两个过程甚至执行相同的乘法序列,获得相同的部分乘积序列。另一方面,当我们考虑这两个过程的“形状”时,我们发现它们的发展完全不同。 @@ -998,11 +998,11 @@ function fib(n) { } ``` -考虑一下这种计算的模式。为了计算`fib(5)`,我们计算`fib(4)`和`fib(3)`。为了计算`fib(4)`,我们计算`fib(3)`和`fib(2)`。一般来说,演化后的流程看起来像一棵树,如图[图 1.5](#c1-fig-0012) 所示。请注意,分支在每一层都分裂成两个(底部除外);这反映了一个事实,即`fib`函数每次被调用时都会调用自己两次。 +考虑一下这种计算的模式。为了计算`fib(5)`,我们计算`fib(4)`和`fib(3)`。为了计算`fib(4)`,我们计算`fib(3)`和`fib(2)`。一般来说,演化后的流程看起来像一棵树,如图图 1.5:所示。请注意,分支在每一层都分裂成两个(底部除外);这反映了一个事实,即`fib`函数每次被调用时都会调用自己两次。 ![c1-fig-0005.jpg](img/c1-fig-0005.jpg) -[图 1.5](#c1-fig-0012a) 计算`fib(5)`中生成的树递归过程。 +图 1.5:计算`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](img/c1-fig-5009.jpg)的整数,其中 diff --git a/docs/06.md b/docs/06.md index 1eb47c7..5f9d1b3 100644 --- a/docs/06.md +++ b/docs/06.md @@ -184,11 +184,11 @@ print_rat(add_rat(one_third, one_third)); 在继续更多复合数据和数据抽象的例子之前,让我们考虑一下有理数例子提出的一些问题。我们根据构造函数`make_rat`和选择器`numer`和`denom`定义了有理数运算。一般而言,数据抽象的基本思想是为每种类型的数据对象标识一组基本操作,根据这些操作,该类型的数据对象的所有操作将被表达,然后在操作数据时仅使用那些操作。 -我们可以想象有理数系统的结构,如图[图 2.1](#c2-fig-0002) 所示。水平线代表隔离系统不同“层次”的抽象障碍。在每一层,屏障将使用数据抽象的程序(上图)与实现数据抽象的程序(下图)分开。使用有理数的程序只根据有理数包提供的“公共使用”的函数来操纵它们:`add_rat`、`sub_rat`、`mul_rat`、`div_rat`和`equal_rat`。反过来,这些仅根据构造函数和选择器`make_rat`、`numer`和`denom`来实现,它们本身是成对实现的。只要可以通过使用`pair`、`head`和`tail`来操作 pairs,pairs 是如何实现的细节与其余的有理数包无关。实际上,每一层的功能都是定义抽象障碍和连接不同层的接口。这个简单的想法有很多优点。一个优点是它使程序更容易维护和修改。任何复杂的数据结构都可以用编程语言提供的原始数据结构以多种方式表示。当然,表示的选择会影响对其进行操作的程序;因此,如果表示在以后某个时间被改变,所有这样的程序可能必须相应地被修改。在大型程序的情况下,这项任务可能既费时又费钱,除非通过设计将对表示的依赖限制在很少的程序模块中。 +我们可以想象有理数系统的结构,如图图 2.1:所示。水平线代表隔离系统不同“层次”的抽象障碍。在每一层,屏障将使用数据抽象的程序(上图)与实现数据抽象的程序(下图)分开。使用有理数的程序只根据有理数包提供的“公共使用”的函数来操纵它们:`add_rat`、`sub_rat`、`mul_rat`、`div_rat`和`equal_rat`。反过来,这些仅根据构造函数和选择器`make_rat`、`numer`和`denom`来实现,它们本身是成对实现的。只要可以通过使用`pair`、`head`和`tail`来操作 pairs,pairs 是如何实现的细节与其余的有理数包无关。实际上,每一层的功能都是定义抽象障碍和连接不同层的接口。这个简单的想法有很多优点。一个优点是它使程序更容易维护和修改。任何复杂的数据结构都可以用编程语言提供的原始数据结构以多种方式表示。当然,表示的选择会影响对其进行操作的程序;因此,如果表示在以后某个时间被改变,所有这样的程序可能必须相应地被修改。在大型程序的情况下,这项任务可能既费时又费钱,除非通过设计将对表示的依赖限制在很少的程序模块中。 ![c2-fig-0001.jpg](img/c2-fig-0001.jpg) -[图 2.1](#c2-fig-0002a) 有理数包中的数据抽象壁垒。 +图 2.1:有理数包中的数据抽象壁垒。 例如,解决将有理数归约到最低项的问题的另一种方法是,每当我们访问有理数的部分时,而不是当我们构造它时,执行归约。这导致了不同的构造函数和选择器函数: @@ -425,17 +425,17 @@ Lem 抱怨说,Alyssa 的程序对这两种计算方式给出了不同的答案 ## 2.2 分层数据和闭合属性 -正如我们所看到的,pairs 提供了一种原始的“粘合剂”,我们可以用它来构造复合数据对象。[图 2.2](#c2-fig-0007) 显示了一种标准的可视化配对方式——在本例中,配对由`pair(1, 2)`组成。在这个被称为盒子和指针符号的表示中,每个复合对象被显示为一个盒子的指针。一对的盒子有两个部分,左边部分是头部,右边部分是尾部。 +正如我们所看到的,pairs 提供了一种原始的“粘合剂”,我们可以用它来构造复合数据对象。图 2.2:显示了一种标准的可视化配对方式——在本例中,配对由`pair(1, 2)`组成。在这个被称为盒子和指针符号的表示中,每个复合对象被显示为一个盒子的指针。一对的盒子有两个部分,左边部分是头部,右边部分是尾部。 ![c2-fig-0002.jpg](img/c2-fig-0002.jpg) -[图 2.2](#c2-fig-0007a)`pair(1, 2)`的盒指针表示。 +图 2.2:`pair(1, 2)`的盒指针表示。 -我们已经看到,`pair`不仅可以用来组合数字,也可以用来组合成对。(在做练习 2.2 和 2.3 时,你利用了这个事实,或者应该利用了这个事实。)因此,pairs 提供了一个通用的构建模块,我们可以从中构建各种数据结构。[图 2.3](#c2-fig-0008) 显示了使用配对组合数字 1、2、3 和 4 的两种方式。 +我们已经看到,`pair`不仅可以用来组合数字,也可以用来组合成对。(在做练习 2.2 和 2.3 时,你利用了这个事实,或者应该利用了这个事实。)因此,pairs 提供了一个通用的构建模块,我们可以从中构建各种数据结构。图 2.3:显示了使用配对组合数字 1、2、3 和 4 的两种方式。 ![c2-fig-0003.jpg](img/c2-fig-0003.jpg) -[图 2.3](#c2-fig-0008a) 两种方式组合 1、2、3、4 使用成对。 +图 2.3:两种方式组合 1、2、3、4 使用成对。 创建元素是对的对的能力是列表结构作为表示工具的重要性的本质。我们将这种能力称为`pair`的闭包属性。一般来说,如果组合数据对象的操作的结果本身可以使用相同的操作来组合,则该操作满足闭包属性。 [⁷](#c2-fn-0007) 封闭是任何组合方式中权力的关键,因为它允许我们创建层级结构——由零件组成的结构,而结构本身又由零件组成,如此等等。 @@ -447,7 +447,7 @@ Lem 抱怨说,Alyssa 的程序对这两种计算方式给出了不同的答案 ![c2-fig-0004.jpg](img/c2-fig-0004.jpg) -[图 2.4](#c2-fig-0009a) 把序列 1、2、3、4 表示为一串对子。 +图 2.4:把序列 1、2、3、4 表示为一串对子。 ```js pair(1, @@ -468,7 +468,7 @@ list(a[1], a[2], . . ., a[n]) pair(a[1], pair(a[2], pair(. . ., pair(a[n], null). . .))) ``` -我们的解释器使用我们称之为盒符号的盒指针图的文本表示来打印对。`pair(1, 2)`的结果打印为`[1, 2]`,[图 2.4](#c2-fig-0009) 中的数据对象打印为`[1, [2, [3, [4, **null**]]]]`: +我们的解释器使用我们称之为盒符号的盒指针图的文本表示来打印对。`pair(1, 2)`的结果打印为`[1, 2]`,图 2.4:中的数据对象打印为`[1, [2, [3, [4, **null**]]]]`: ```js const one_through_four = list(1, 2, 3, 4); @@ -800,17 +800,17 @@ for_each(x => display(x), list(57, 321, 88)); pair(list(1, 2), list(3, 4)); ``` -作为三个条目的列表,第一个条目本身就是一个列表,`[1, [2, **null**]]`。[图 2.5](#c2-fig-0010) 显示了这种结构的线对表示。 +作为三个条目的列表,第一个条目本身就是一个列表,`[1, [2, **null**]]`。图 2.5:显示了这种结构的线对表示。 ![c2-fig-0005.jpg](img/c2-fig-0005.jpg) -[图 2.5](#c2-fig-0010a) 由`pair(list(1, 2), list(3, 4))`构成的结构。 +图 2.5:由`pair(list(1, 2), list(3, 4))`构成的结构。 -另一种考虑元素是序列的序列的方式是树。序列的元素是树的分支,本身是序列的元素是子树。[图 2.6](#c2-fig-0011) 显示了图 2.5 中的树形结构。 +另一种考虑元素是序列的序列的方式是树。序列的元素是树的分支,本身是序列的元素是子树。图 2.6:显示了图 2.5 中的树形结构。 ![c2-fig-0006.jpg](img/c2-fig-0006.jpg) -[图 2.6](#c2-fig-0011a) 图 2.5 中的列表结构视为一棵树。 +图 2.6:图 2.5 中的列表结构视为一棵树。 递归是处理树结构的一种自然工具,因为我们通常可以将树上的操作简化为树枝上的操作,而树枝上的操作又依次简化为树枝上的操作,如此类推,直到到达树叶。例如,比较第 2.2.1 节的`length`函数和`count_leaves`函数,后者返回一棵树的总叶子数: @@ -864,7 +864,7 @@ function count_leaves(x) { ##### 练习 2.24 -假设我们评估表达式`list(1, list(2, list(3, 4)))`。给出解释器打印的结果,相应的盒指针结构,以及对树的解释(如图[图 2.6](#c2-fig-0011) )。 +假设我们评估表达式`list(1, list(2, list(3, 4)))`。给出解释器打印的结果,相应的盒指针结构,以及对树的解释(如图图 2.6: )。 ##### 练习 2.25 @@ -1087,11 +1087,11 @@ function even_fibs(n) { * 过滤它们,选择偶数的;和 * 使用`pair`累加结果,从空列表开始。 -信号处理工程师会发现很自然地将这些过程概念化为流经级联阶段的信号,每个阶段实现程序计划的一部分,如图[图 2.7](#c2-fig-0012) 所示。在`sum_odd_squares`中,我们从一个枚举器开始,它生成一个由给定树的叶子组成的“信号”。该信号通过一个滤波器,该滤波器除了奇数元素之外,其他元素都被滤除。产生的信号依次通过图,图是一个将`square`功能应用于每个元素的“传感器”。然后,映射的输出被馈送到一个累加器,该累加器使用`+`从初始 0 开始合并元素。`even_fibs`的计划是类似的。 +信号处理工程师会发现很自然地将这些过程概念化为流经级联阶段的信号,每个阶段实现程序计划的一部分,如图图 2.7:所示。在`sum_odd_squares`中,我们从一个枚举器开始,它生成一个由给定树的叶子组成的“信号”。该信号通过一个滤波器,该滤波器除了奇数元素之外,其他元素都被滤除。产生的信号依次通过图,图是一个将`square`功能应用于每个元素的“传感器”。然后,映射的输出被馈送到一个累加器,该累加器使用`+`从初始 0 开始合并元素。`even_fibs`的计划是类似的。 ![c2-fig-0007.jpg](img/c2-fig-0007.jpg) -[图 2.7](#c2-fig-0012a) 函数`sum_odd_squares`(上图)和`even_fibs`(下图)的信号流程图揭示了两个程序之间的共性。 +图 2.7:函数`sum_odd_squares`(上图)和`even_fibs`(下图)的信号流程图揭示了两个程序之间的共性。 不幸的是,上面的两个函数声明没有展示这种信号流结构。例如,如果我们检查`sum_odd_squares`函数,我们发现枚举部分由`is_null`和`is_pair`测试实现,部分由函数的树递归结构实现。类似地,这种积累部分出现在测试中,部分出现在递归中使用的加法中。一般来说,两种功能都没有与信号流描述中的元素相对应的独特部分。我们的两个函数以不同的方式分解计算,将枚举分布在程序中,并将其与映射、过滤和累积混合在一起。如果我们能够组织我们的程序,使信号流结构在我们编写的函数中显现出来,这将增加最终程序的概念清晰度。 @@ -1416,7 +1416,7 @@ function reverse(sequence) { 组织这种计算的一种自然方式是生成所有小于或等于 n 的正整数有序对的序列,过滤以选择那些和为素数的对,然后,对于通过过滤器的每一对( i , j ),生成三元组( i , j ,I【T72+】【t77) -这里有一个生成偶对序列的方法:对于每个整数 i n ,枚举整数 j < i ,对于每个这样的`i`和`j`生成`pair(i, j)`。在序列操作方面,我们沿着序列`enumerate_interval(1, n)`进行映射。对于这个序列中的每个 i ,我们沿着序列`enumerate_interval(1, i - 1)`进行映射。对于后面这个序列中的每个 j ,我们生成一对`list(i, j)`。这给了我们每个 i 的配对序列。组合所有 i 的所有序列(通过与`append`累加)产生所需的序列对: [^(17)](#c2-fn-0017) +这里有一个生成偶对序列的方法:对于每个整数`i < n`,枚举整数`j < i`,对于每个这样的`i`和`j`生成`pair(i, j)`。在序列操作方面,我们沿着序列`enumerate_interval(1, n)`进行映射。对于这个序列中的每个`i`,我们沿着序列`enumerate_interval(1, i - 1)`进行映射。对于后面这个序列中的每个`j`,我们生成一对`list(i, j)`。这给了我们每个`i`的偶对序列。组合所有`i`的所有序列(通过与`append`累加)产生所需的序列对: [^(17)](#c2-fn-0017) ```js accumulate(append, @@ -1498,7 +1498,7 @@ function remove(item, sequence) { ![c2-fig-0008.jpg](img/c2-fig-0008.jpg) -[图 2.8](#c2-fig-0015a) 八皇后谜题的一种解法。 +图 2.8:八皇后谜题的一种解法。 我们将该解决方案实现为函数`queens`,该函数返回将 n 个皇后放置在 n n 棋盘上的问题的所有解决方案的序列。函数`queens`有一个内部函数`queens_cols`,它返回将皇后放入棋盘前 k 列的所有方法的顺序。 @@ -1541,25 +1541,25 @@ flatmap(new_row => ![c2-fig-0009.jpg](img/c2-fig-0009.jpg) -[图 2.9](#c2-fig-0016a) 用图片语言生成的设计。 +图 2.9:用图片语言生成的设计。 ##### 图片语言 当我们在 1.1 节开始学习编程时,我们强调了通过关注语言的原语、组合方式和抽象方式来描述语言的重要性。我们将遵循这个框架。 -这种画面语言的优雅之处在于只有一种元素,叫做画家。画家绘制图像,该图像被移动和缩放以适合指定的平行四边形框架。例如,有一个我们称之为`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`图像相同的四帧绘制的。 +这种画面语言的优雅之处在于只有一种元素,叫做画家。画家绘制图像,该图像被移动和缩放以适合指定的平行四边形框架。例如,有一个我们称之为`wave`的原始画家,他画了一个粗糙的线条画,如图图 2.10:所示。图画的实际形状取决于画框——图 2.10:中的所有四幅图像都是由同一个`wave`画家绘制的,但是相对于四个不同的画框。画家可以比这更精细:名为`rogers`的原始画家为麻省理工学院的创始人威廉·巴顿·罗杰斯画了一幅画,如图图 2.11:所示。[^(21)](#c2-fn-0021)图 2.11:中的四幅图像是针对与图 2.10:中的`wave`图像相同的四帧绘制的。 ![c2-fig-0010.jpg](img/c2-fig-0010.jpg) -[图 2.10](#c2-fig-0017a) 由`wave`画师产生的图像,关于四个不同的帧。用虚线显示的帧不是图像的一部分。 +图 2.10:由`wave`画师产生的图像,关于四个不同的帧。用虚线显示的帧不是图像的一部分。 ![c2-fig-0011.jpg](img/c2-fig-0011.jpg) -[图 2.11](#c2-fig-0018a) 麻省理工学院创始人兼首任校长威廉·巴顿·罗杰斯的画像,与图 2.10[中的四个画框相对而画(原图由麻省理工学院博物馆提供)。](#c2-fig-0017) +图 2.11:麻省理工学院创始人兼首任校长威廉·巴顿·罗杰斯的画像,与图 2.10[中的四个画框相对而画(原图由麻省理工学院博物馆提供)。](#c2-fig-0017) 为了组合图像,我们使用各种操作从给定的画师构造新的画师。例如,`beside`操作采用两个画师,并生成一个新的复合画师,在帧的左半部分绘制第一个画师的图像,在帧的右半部分绘制第二个画师的图像。类似地,`below`采用两个画家并产生一个复合画家,该复合画家在第二个画家的图像下面绘制第一个画家的图像。一些操作转换单个画师以产生新的画师。例如,`flip_vert`取一个画师,产生一个颠倒画其图像的画师,`flip_horiz`产生一个颠倒画原画师图像的画师。 -[图 2.12](#c2-fig-0019) 显示了一个名为`wave4`的画家的图,该图从`wave`开始分两个阶段构建: +图 2.12:显示了一个名为`wave4`的画家的图,该图从`wave`开始分两个阶段构建: ```js const wave2 = beside(wave, flip_vert(wave)); @@ -1570,7 +1570,7 @@ const wave4 = below(wave2, wave2); ![c2-fig-0012.jpg](img/c2-fig-0012.jpg) -[图 2.12](#c2-fig-0019a) 创建复杂图形,从[图 2.10](#c2-fig-0017) 的`wave`画师开始。 +图 2.12:创建复杂图形,从图 2.10:的`wave`画师开始。 一旦我们可以组合画家,我们希望能够抽象出组合画家的典型模式。我们将把画图操作实现为 JavaScript 函数。这意味着在图片语言中我们不需要特殊的抽象机制:由于组合的方式是普通的 JavaScript 函数,我们自动地拥有了用函数可以做的任何画图操作的能力。例如,我们可以将`wave4`中的模式抽象为 @@ -1602,11 +1602,11 @@ function right_split(painter, n) { ![c2-fig-0013.jpg](img/c2-fig-0013.jpg) -[图 2.13](#c2-fig-0020a) 为`right_split`和`corner_split`的递归计划。 +图 2.13:为`right_split`和`corner_split`的递归计划。 ![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) +图 2.14:将递归运算`right_split`应用于画师`wave`和`rogers`。组合四个`corner_split`图形产生对称的`square_limit`,如图 2.9 中的[所示。](#c2-fig-0016) 我们可以通过向上和向右分支来生成平衡的模式(参见练习 2.44 和[图 2.13 和 2.14](#c2-fig-0020) ): @@ -1626,7 +1626,7 @@ function corner_split(painter, n) { } ``` -通过适当放置四个`corner_split`副本,我们得到一个名为`square_limit`的图案,其在`wave`和`rogers`中的应用如图[图 2.9](#c2-fig-0016) 所示: +通过适当放置四个`corner_split`副本,我们得到一个名为`square_limit`的图案,其在`wave`和`rogers`中的应用如图图 2.9:所示: ```js function square_limit(painter, n) { @@ -1691,11 +1691,11 @@ const up_split = split(below, beside); 在我们展示如何实现画家和他们的组合方式之前,我们必须首先考虑框架。一个帧可以用三个向量来描述——一个原点向量和两个边缘向量。原点向量指定框架的原点相对于平面中某个绝对原点的偏移量,边缘向量指定框架的角相对于其原点的偏移量。如果边缘垂直,框架将是矩形的。否则框架将是一个更一般的平行四边形。 -[图 2.15](#c2-fig-0022) 显示了一个帧及其相关向量。根据数据抽象,我们还不需要具体说明框架是如何表示的,只是说有一个构造函数`make_frame`,它接受三个向量并产生一个框架,以及三个相应的选择器`origin_frame`、`edge1_frame`和`edge2_frame`(见练习 2.47)。 +图 2.15:显示了一个帧及其相关向量。根据数据抽象,我们还不需要具体说明框架是如何表示的,只是说有一个构造函数`make_frame`,它接受三个向量并产生一个框架,以及三个相应的选择器`origin_frame`、`edge1_frame`和`edge2_frame`(见练习 2.47)。 ![c2-fig-0015.jpg](img/c2-fig-0015.jpg) -[图 2.15](#c2-fig-0022a) 一个帧由三个向量描述——一个原点和两条边。 +图 2.15:一个帧由三个向量描述——一个原点和两条边。 我们将使用单位正方形中的坐标(0 ≤ x , y ≤ 1)来指定图像。对于每一帧,我们关联一个帧坐标图,它将用于移动和缩放图像以适应帧。通过将矢量 **v** = ( x , y )映射到矢量和,地图将单位正方形转换为框架 @@ -1755,7 +1755,7 @@ function make_frame(origin, edge1, edge2) { 画师被表示为一个函数,给定一个帧作为参数,绘制一个特定的图像,移动和缩放以适合该帧。也就是说,如果`p`是画师,`f`是画框,那么我们通过以`f`为自变量调用`p`来产生`f`中`p`的形象。 -原始画师是如何实现的细节取决于图形系统的特定特征和要绘制的图像类型。例如,假设我们有一个函数`draw_line`,它在屏幕上的两个指定点之间画了一条线。然后我们可以为线条画创建画师,例如[图 2.10](#c2-fig-0017) 中的`wave`画师,来自如下的线段列表: [^(26)](#c2-fn-0026) +原始画师是如何实现的细节取决于图形系统的特定特征和要绘制的图像类型。例如,假设我们有一个函数`draw_line`,它在屏幕上的两个指定点之间画了一条线。然后我们可以为线条画创建画师,例如图 2.10:中的`wave`画师,来自如下的线段列表: [^(26)](#c2-fn-0026) ```js function segments_to_painter(segment_list) { @@ -2326,11 +2326,11 @@ function intersection_set(set1, set2) { ##### 作为二叉树的集合 -通过以树的形式排列集合元素,我们可以比有序列表表示做得更好。树的每个节点保存集合中的一个元素,称为该节点上的“条目”,以及到另外两个(可能是空的)节点中的每一个的链接。“左”链接指向比节点处的元素小的元素,“右”链接指向比节点处的元素大的元素。[图 2.16](#c2-fig-0026) 显示了一些代表集合{1,3,5,7,9,11}的树。同一集合可以由树以多种不同的方式来表示。对于有效的表示,我们唯一需要的是左子树中的所有元素都小于节点条目,而右子树中的所有元素都大于节点条目。 +通过以树的形式排列集合元素,我们可以比有序列表表示做得更好。树的每个节点保存集合中的一个元素,称为该节点上的“条目”,以及到另外两个(可能是空的)节点中的每一个的链接。“左”链接指向比节点处的元素小的元素,“右”链接指向比节点处的元素大的元素。图 2.16:显示了一些代表集合{1,3,5,7,9,11}的树。同一集合可以由树以多种不同的方式来表示。对于有效的表示,我们唯一需要的是左子树中的所有元素都小于节点条目,而右子树中的所有元素都大于节点条目。 ![c2-fig-0016.jpg](img/c2-fig-0016.jpg) -[图 2.16](#c2-fig-0026a) 表示集合{1,3,5,7,9,11}的各种二叉树。 +图 2.16:表示集合{1,3,5,7,9,11}的各种二叉树。 树表示的优点是:假设我们想要检查一个数 x 是否包含在一个集合中。我们首先将 x 与顶部节点中的条目进行比较。如果 x 小于这个,我们知道只需要搜索左边的子树;如果 x 更大,我们只需要搜索右边的子树。现在,如果树是“平衡的”,这些子树中的每一个都将是原来的一半大小。因此,在一个步骤中,我们已经将搜索大小为 n 的树的问题简化为搜索大小为 n /2 的树。由于树的大小在每一步都减半,我们应该预计搜索大小为 n 的树所需的步骤数随着θ(logn)的增加而增加。 [^(34)](#c2-fn-0034) @@ -2379,11 +2379,11 @@ function adjoin_set(x, set) { } ``` -上面声称搜索树可以在对数数量的步骤中执行,这是基于树是“平衡的”的假设,即每棵树的左右子树具有大约相同数量的元素,使得每个子树包含其父树的大约一半的元素。但是我们怎么能确定我们建造的树是平衡的呢?即使我们从平衡的树开始,用`adjoin_set`添加元素也可能产生不平衡的结果。由于新邻接元素的位置取决于该元素与集合中已有项目的比较情况,我们可以预期,如果我们“随机”添加元素,树将趋于平均平衡。但这不是保证。例如,如果我们从一个空集开始,并依次邻接数字 1 到 7,我们最终会得到一个高度不平衡的树,如图[图 2.17](#c2-fig-0027) 所示。在这棵树中,所有左边的子树都是空的,所以它比简单的有序列表没有优势。解决这个问题的一种方法是定义一种操作,将任意树转换成具有相同元素的平衡树。然后,我们可以在每隔几个`adjoin_set`操作之后执行这个转换,以保持我们的集合平衡。还有其他方法可以解决这个问题,其中大多数涉及设计新的数据结构,搜索和插入都可以在θ(logn 步中完成。 [^(36)](#c2-fn-0036) +上面声称搜索树可以在对数数量的步骤中执行,这是基于树是“平衡的”的假设,即每棵树的左右子树具有大约相同数量的元素,使得每个子树包含其父树的大约一半的元素。但是我们怎么能确定我们建造的树是平衡的呢?即使我们从平衡的树开始,用`adjoin_set`添加元素也可能产生不平衡的结果。由于新邻接元素的位置取决于该元素与集合中已有项目的比较情况,我们可以预期,如果我们“随机”添加元素,树将趋于平均平衡。但这不是保证。例如,如果我们从一个空集开始,并依次邻接数字 1 到 7,我们最终会得到一个高度不平衡的树,如图图 2.17:所示。在这棵树中,所有左边的子树都是空的,所以它比简单的有序列表没有优势。解决这个问题的一种方法是定义一种操作,将任意树转换成具有相同元素的平衡树。然后,我们可以在每隔几个`adjoin_set`操作之后执行这个转换,以保持我们的集合平衡。还有其他方法可以解决这个问题,其中大多数涉及设计新的数据结构,搜索和插入都可以在θ(logn 步中完成。 [^(36)](#c2-fn-0036) ![c2-fig-0017.jpg](img/c2-fig-0017.jpg) -[图 2.17](#c2-fig-0027a) 由邻接 1 到 7 依次产生的不平衡树。 +图 2.17:由邻接 1 到 7 依次产生的不平衡树。 ##### 练习 2.63 @@ -2411,7 +2411,7 @@ function tree_to_list_2(tree) { } ``` -1. 这两个函数对每棵树都产生相同的结果吗?如果不是,结果有什么不同?这两个函数为[图 2.16](#c2-fig-0026) 中的树产生了什么列表? +1. 这两个函数对每棵树都产生相同的结果吗?如果不是,结果有什么不同?这两个函数为图 2.16:中的树产生了什么列表? 2. b. 这两个函数在把一棵有 n 个元素的平衡树转换成一个链表所需的步骤数上,增长顺序是否相同?如果不是,哪个长得更慢? ##### 练习 2.64 @@ -2508,13 +2508,13 @@ BACADAEAFABBAAAGAH 一般来说,如果我们使用变长前缀码,利用要编码的消息中符号的相对频率,我们可以实现显著的节约。一种特殊的编码方法叫做霍夫曼编码法,以其发现者大卫·霍夫曼命名。霍夫曼码可以表示为二叉树,其叶子是被编码的符号。在树的每个非叶子节点处,有一个集合包含位于该节点下的叶子中的所有符号。此外,叶子上的每个符号被分配一个权重(这是它的相对频率),并且每个非叶子节点包含一个权重,该权重是位于它下面的叶子的所有权重之和。在编码或解码过程中不使用权重。我们将在下面看到它们是如何被用来帮助构建树的。 -[图 2.18](#c2-fig-0028) 显示了上面给出的 A 到 H 代码的霍夫曼树。叶子上的权重表示该树是为 A 出现的相对频率为 8、B 出现的相对频率为 3、其他字母出现的相对频率为 1 的消息而设计的。 +图 2.18:显示了上面给出的 A 到 H 代码的霍夫曼树。叶子上的权重表示该树是为 A 出现的相对频率为 8、B 出现的相对频率为 3、其他字母出现的相对频率为 1 的消息而设计的。 ![c2-fig-0018.jpg](img/c2-fig-0018.jpg) -[图 2.18](#c2-fig-0028a) 一棵霍夫曼编码树。 +图 2.18:一棵霍夫曼编码树。 -给定一个霍夫曼树,我们可以从根开始找到任何符号的编码,并向下移动,直到到达保存该符号的叶子。每次我们向下移动一个左分支,我们给代码加一个 0,每次向下移动一个右分支,我们加一个 1。(我们通过测试看哪个分支是符号的叶节点或者在它的集合中包含符号来决定跟随哪个分支。)例如,从[图 2.18](#c2-fig-0028) 中的树根开始,我们沿着一个右分支,然后一个左分支,然后一个右分支,然后一个右分支,到达 D 的叶子;因此,D 的代码是 1011。 +给定一个霍夫曼树,我们可以从根开始找到任何符号的编码,并向下移动,直到到达保存该符号的叶子。每次我们向下移动一个左分支,我们给代码加一个 0,每次向下移动一个右分支,我们加一个 1。(我们通过测试看哪个分支是符号的叶节点或者在它的集合中包含符号来决定跟随哪个分支。)例如,从图 2.18:中的树根开始,我们沿着一个右分支,然后一个左分支,然后一个右分支,然后一个右分支,到达 D 的叶子;因此,D 的代码是 1011。 为了使用霍夫曼树解码位序列,我们从根开始,并使用位序列的连续 0 和 1 来确定是向下移动左分支还是右分支。每当我们来到一片树叶,我们就在信息中生成一个新的符号,此时我们从树根开始寻找下一个符号。例如,假设我们得到了上面的树和序列 10001010。从根开始,我们向下移动右分支(因为字符串的第一位是 1),然后向下移动左分支(因为第二位是 0),然后向下移动左分支(因为第三位也是 0)。这将我们带到 B 的叶子,所以解码消息的第一个符号是 B。现在我们从根开始,我们向左移动,因为字符串中的下一位是 0。这将我们带到 a 的叶子。然后,我们用字符串 1010 的剩余部分再次从根开始,所以我们向右、向左、向右、向左移动并到达 c。因此,整个消息是 BAC。 @@ -2724,7 +2724,7 @@ Sha boom ## 2.4 抽象数据的多重表示 -我们引入了数据抽象,这是一种结构化系统的方法,使得程序的大部分可以独立于实现程序所操作的数据对象的选择而被指定。例如,我们在 2.1.1 节中看到了如何根据计算机语言构造复合数据的基本机制,将设计使用有理数的程序的任务与实现有理数的任务分开。关键思想是建立一个抽象屏障——在本例中,是有理数的选择器和构造器(`make_rat`、`numer`、`denom`)——将有理数的使用方式与它们在列表结构方面的底层表示隔离开来。类似的抽象障碍将执行有理数运算的函数(`add_rat`、`sub_rat`、`mul_rat`和`div_rat`)的细节与使用有理数的“高级”函数隔离开来。最终程序的结构如图[图 2.1](#c2-fig-0002) 所示。 +我们引入了数据抽象,这是一种结构化系统的方法,使得程序的大部分可以独立于实现程序所操作的数据对象的选择而被指定。例如,我们在 2.1.1 节中看到了如何根据计算机语言构造复合数据的基本机制,将设计使用有理数的程序的任务与实现有理数的任务分开。关键思想是建立一个抽象屏障——在本例中,是有理数的选择器和构造器(`make_rat`、`numer`、`denom`)——将有理数的使用方式与它们在列表结构方面的底层表示隔离开来。类似的抽象障碍将执行有理数运算的函数(`add_rat`、`sub_rat`、`mul_rat`和`div_rat`)的细节与使用有理数的“高级”函数隔离开来。最终程序的结构如图图 2.1:所示。 这些数据抽象障碍是控制复杂性的强大工具。通过隔离数据对象的底层表示,我们可以将设计一个大型程序的任务分成可以单独执行的更小的任务。但是这种数据抽象还不够强大,因为谈论数据对象的“底层表示”并不总是有意义的。 @@ -2738,7 +2738,7 @@ Sha boom ![c2-fig-0019.jpg](img/c2-fig-0019.jpg) -[图 2.19](#c2-fig-0029a) 复数系统中的数据抽象障碍。 +图 2.19:复数系统中的数据抽象障碍。 在 2.5 节中,我们将展示如何使用类型标签和数据导向风格来开发一个通用的算术包。这提供了一些函数(`add`、`mul`等等),可以用来操作各种“数字”,并且在需要一种新的数字时可以很容易地扩展。在 2.5.3 节中,我们将展示如何在执行符号代数的系统中使用泛型算法。 @@ -2746,16 +2746,16 @@ Sha boom 我们将开发一个对复数执行算术运算的系统,作为一个使用一般运算的程序的简单但不现实的例子。我们首先讨论作为有序对的复数的两种貌似合理的表示:矩形形式(实部和虚部)和极坐标形式(幅度和角度)。第 2.4.2 节将展示如何通过使用类型标签和泛型操作使两种表示在一个系统中共存。 -像有理数一样,复数自然表示为有序对。复数的集合可以被认为是具有两个正交轴的二维空间,即“实”轴和“虚”轴。(参见[图 2.20](#c2-fig-0030) 。)从这个角度来看,复数 z=x+iy(其中 I²=–1)可以认为是平面中实坐标为 x ,虚坐标为 y 的点。在这种表示中,复数的相加简化为坐标的相加: +像有理数一样,复数自然表示为有序对。复数的集合可以被认为是具有两个正交轴的二维空间,即“实”轴和“虚”轴。(参见图 2.20: 。)从这个角度来看,复数 z=x+iy(其中 I²=–1)可以认为是平面中实坐标为 x ,虚坐标为 y 的点。在这种表示中,复数的相加简化为坐标的相加: | 实部(z1+z2) | = | 实部(z1)+实部(z2) | | 虚部(z1+z2) | = | 虚部(z1)+虚部(z2) | ![c2-fig-0020.jpg](img/c2-fig-0020.jpg) -[图 2.20](#c2-fig-0030a) 复数为平面上的点。 +图 2.20:复数为平面上的点。 -当复数相乘时,更自然的想法是用极坐标形式来表示复数,如幅度和角度( r 和 A 在[图 2.20](#c2-fig-0030) )。两个复数的乘积是通过将一个复数拉伸另一个复数的长度,然后将其旋转另一个复数的角度而获得的向量: +当复数相乘时,更自然的想法是用极坐标形式来表示复数,如幅度和角度( r 和 A 在图 2.20: )。两个复数的乘积是通过将一个复数拉伸另一个复数的长度,然后将其旋转另一个复数的角度而获得的向量: | 星等(z1z2) | = | 量级(z1)量级(z2) | | 角度(z1z2) | = | 角度(z1)+角度(z2) | @@ -2977,7 +2977,7 @@ function make_from_mag_ang(r, a) { ![c2-fig-0021.jpg](img/c2-fig-0021.jpg) -[图 2.21](#c2-fig-0031a) 通用复数运算系统的结构。 +图 2.21:通用复数运算系统的结构。 因为每个数据对象都用它的类型来标记,所以选择器以通用的方式对数据进行操作。也就是说,每个选择器被定义为具有依赖于它所应用的特定数据类型的行为。请注意连接不同表示的一般机制:在给定的表示实现中(比如 Alyssa 的极坐标包),复数是一个无类型对(幅度、角度)。当一个通用选择器对多个`polar`类型进行操作时,它会去掉标签并将内容传递给 Alyssa 的代码。相反,当 Alyssa 构造一个通用的数字时,她用一个类型来标记它,以便它可以被更高级别的函数正确地识别。正如我们将在 2.5 节中看到的,当数据对象从一级传递到另一级时,这种剥离和附加标签的原则是一种重要的组织策略。 @@ -2993,7 +2993,7 @@ function make_from_mag_ang(r, a) { ![c2-fig-0022.jpg](img/c2-fig-0022.jpg) -[图 2.22](#c2-fig-0032a) 复数系统运算表。 +图 2.22:复数系统运算表。 数据导向编程是一种设计程序直接使用这种表的技术。之前,我们实现了将复数算术代码与两个表示包作为一组函数进行接口的机制,每个函数对类型执行显式调度。这里,我们将把接口实现为一个函数,它在表中查找操作名和参数类型的组合,以找到要应用的正确函数,然后将它应用于参数的内容。如果我们这样做了,那么为了向系统添加新的表示包,我们不需要改变任何现有的功能;我们只需要向表中添加新的条目。 @@ -3221,11 +3221,11 @@ function apply_generic(op, arg) { return head(arg)(op); } 在上一节中,我们看到了如何设计可以用多种方式表示数据对象的系统。关键思想是通过通用接口函数将指定数据操作的代码与几种表示联系起来。现在,我们将看到如何使用同样的思想,不仅定义在不同表示上通用的操作,而且定义在不同类型的参数上通用的操作。我们已经看到了几个不同的算术运算包:内置于我们语言中的原始算术(`+`、`-`、`*`、`/`)、2.1.1 节中的有理数算术(`add_rat`、`sub_rat`、`mul_rat`、`div_rat`)以及我们在 2.4.3 节中实现的复数算术。我们现在将使用数据导向技术构建一个算术运算包,它包含了我们已经构建的所有算术运算包。 -[图 2.23](#c2-fig-0033) 显示了我们将要构建的系统的结构。注意抽象障碍。从使用“数字”的人的角度来看,有一个函数`add`可以处理提供的任何数字。函数`add`是一个通用接口的一部分,它允许使用数字的程序统一访问独立的普通算术、有理算术和复杂算术包。任何单个算术包(如复杂包)本身都可以通过通用函数(如`add_complex`)访问,这些函数组合了为不同表示(如矩形和极坐标)设计的包。此外,该系统的结构是可加的,因此人们可以分别设计单独的算术包,然后将它们组合起来,形成一个通用的算术系统。 +图 2.23:显示了我们将要构建的系统的结构。注意抽象障碍。从使用“数字”的人的角度来看,有一个函数`add`可以处理提供的任何数字。函数`add`是一个通用接口的一部分,它允许使用数字的程序统一访问独立的普通算术、有理算术和复杂算术包。任何单个算术包(如复杂包)本身都可以通过通用函数(如`add_complex`)访问,这些函数组合了为不同表示(如矩形和极坐标)设计的包。此外,该系统的结构是可加的,因此人们可以分别设计单独的算术包,然后将它们组合起来,形成一个通用的算术系统。 ![c2-fig-0023.jpg](img/c2-fig-0023.jpg) -[图 2.23](#c2-fig-0033a) 通用算术系统。 +图 2.23:通用算术系统。 ### 2.5.1 通用算术运算 @@ -3375,11 +3375,11 @@ function make_complex_from_mag_ang(r, a){ } ``` -我们这里有一个两级标记系统。一个典型的复数,如矩形形式的 3 + 4 i ,将被表示为如图[图 2.24](#c2-fig-0034) 所示。外部标签(`"complex"`)用于将编号指向复合包装。一旦进入复杂包装,下一个标签(`"rectangular"`)用于将数字指向矩形包装。在一个大而复杂的系统中,可能有许多层次,每一层都通过一般操作与下一层连接。当数据对象被“向下”传递时,用于将它导向适当包的外部标签被剥离(通过应用`contents`),下一层标签(如果有)变得可见,用于进一步的分派。 +我们这里有一个两级标记系统。一个典型的复数,如矩形形式的 3 + 4 i ,将被表示为如图图 2.24:所示。外部标签(`"complex"`)用于将编号指向复合包装。一旦进入复杂包装,下一个标签(`"rectangular"`)用于将数字指向矩形包装。在一个大而复杂的系统中,可能有许多层次,每一层都通过一般操作与下一层连接。当数据对象被“向下”传递时,用于将它导向适当包的外部标签被剥离(通过应用`contents`),下一层标签(如果有)变得可见,用于进一步的分派。 ![c2-fig-0024.jpg](img/c2-fig-0024.jpg) -[图 2.24](#c2-fig-0034a) 以矩形形式表示 3 + 4i。 +图 2.24:以矩形形式表示 3 + 4i。 在上面的包中,我们使用了`add_rat`、`add_complex`和其他算术函数,与最初编写的完全一样。然而,一旦这些声明是不同安装函数的内部声明,它们就不再需要彼此不同的名字:我们可以在两个包中简单地将它们命名为`add`、`sub`、`mul`和`div`。 @@ -3482,11 +3482,11 @@ function apply_generic(op, args) { ##### 类型的层次结构 -上面介绍的强制方案依赖于类型对之间自然关系的存在。通常在不同类型之间的关系上有更多的“全局”结构。例如,假设我们正在构建一个通用的算术系统来处理整数、有理数、实数和复数。在这样一个系统中,把一个整数看成一种特殊的有理数是很自然的,这种有理数又是一种特殊的实数,而实数又是一种特殊的复数。我们实际拥有的是所谓的类型的层次结构,例如,整数是有理数的一个子类型(也就是说,任何可以应用于有理数的运算都可以自动应用于整数)。相反,我们说有理数形成了整数的一个超类型。这里我们有一个非常简单的层次结构,其中每个类型最多有一个超类型和一个子类型。这种结构称为塔,如图[图 2.25](#c2-fig-0035) 所示。 +上面介绍的强制方案依赖于类型对之间自然关系的存在。通常在不同类型之间的关系上有更多的“全局”结构。例如,假设我们正在构建一个通用的算术系统来处理整数、有理数、实数和复数。在这样一个系统中,把一个整数看成一种特殊的有理数是很自然的,这种有理数又是一种特殊的实数,而实数又是一种特殊的复数。我们实际拥有的是所谓的类型的层次结构,例如,整数是有理数的一个子类型(也就是说,任何可以应用于有理数的运算都可以自动应用于整数)。相反,我们说有理数形成了整数的一个超类型。这里我们有一个非常简单的层次结构,其中每个类型最多有一个超类型和一个子类型。这种结构称为塔,如图图 2.25:所示。 ![c2-fig-0025.jpg](img/c2-fig-0025.jpg) -[图 2.25](#c2-fig-0035a) 一座塔的类型。 +图 2.25:一座塔的类型。 如果我们有一个塔式结构,那么我们可以大大简化向层次结构中添加新类型的问题,因为我们只需要指定新类型如何嵌入到它上面的下一个超类型中,以及它如何成为它下面的超类型。例如,如果我们想把一个整数加到一个复数上,我们不需要显式定义一个特殊的强制函数`integer_to_complex`。而是定义整数如何转化为有理数,有理数如何转化为实数,实数如何转化为复数。然后,我们允许系统通过这些步骤将整数转换为复数,然后将两个复数相加。 @@ -3498,11 +3498,11 @@ function apply_generic(op, args) { ##### 等级制度的不足 -如我们所见,如果我们系统中的数据类型可以自然地排列在一个塔中,这就大大简化了处理不同类型上的一般操作的问题。不幸的是,通常情况并非如此。[图 2.26](#c2-fig-0036) 展示了一个更复杂的混合类型排列,这个显示了不同类型的几何图形之间的关系。我们看到,一般来说,一个类型可能有不止一个子类型。例如,三角形和四边形都是多边形的子类型。此外,一个类型可以有多个超类型。例如,等腰直角三角形可以被认为是等腰三角形或直角三角形。这个多重超类型的问题特别棘手,因为这意味着没有唯一的方法在层次结构中“提升”一个类型。找到“正确的”超类型,在其中对一个对象应用一个操作,可能需要在整个类型网络中对一个函数(如`apply_generic`)进行大量的搜索。因为一个类型通常有多个子类型,所以在类型层次结构中强制一个值“向下”也有类似的问题。在大型系统的设计中,处理大量相互关联的类型同时仍然保持模块性是非常困难的,这也是当前研究的一个领域。 [^(49)](#c2-fn-0049) +如我们所见,如果我们系统中的数据类型可以自然地排列在一个塔中,这就大大简化了处理不同类型上的一般操作的问题。不幸的是,通常情况并非如此。图 2.26:展示了一个更复杂的混合类型排列,这个显示了不同类型的几何图形之间的关系。我们看到,一般来说,一个类型可能有不止一个子类型。例如,三角形和四边形都是多边形的子类型。此外,一个类型可以有多个超类型。例如,等腰直角三角形可以被认为是等腰三角形或直角三角形。这个多重超类型的问题特别棘手,因为这意味着没有唯一的方法在层次结构中“提升”一个类型。找到“正确的”超类型,在其中对一个对象应用一个操作,可能需要在整个类型网络中对一个函数(如`apply_generic`)进行大量的搜索。因为一个类型通常有多个子类型,所以在类型层次结构中强制一个值“向下”也有类似的问题。在大型系统的设计中,处理大量相互关联的类型同时仍然保持模块性是非常困难的,这也是当前研究的一个领域。 [^(49)](#c2-fn-0049) ![c2-fig-0026.jpg](img/c2-fig-0026.jpg) -[图 2.26](#c2-fig-0036a) 几何图形类型之间的关系。 +图 2.26:几何图形类型之间的关系。 ##### 练习 2.81 @@ -4001,7 +4001,7 @@ identity,flip _ vert); 函数`segments_to_painter`使用下面练习 2.48 中描述的线段表示法。它还使用了练习 2.23 中描述的`for_each`功能。 -[27](#c2-fn-0027a) 例如,[图 2.11](#c2-fig-0018) 的`rogers`油漆工就是从一张灰度图像中构造出来的。对于给定帧中的每个点,`rogers`画师确定图像中在帧坐标映射下映射到它的点,并相应地对其进行着色。通过允许不同类型的绘制者,我们利用了 2.1.3 节中讨论的抽象数据的概念,我们认为有理数表示可以是满足适当条件的任何东西。在这里,我们利用了一个事实,即画师可以以任何方式实现,只要它在指定的框架中绘制东西。第 2.1.3 节还展示了如何将线对实现为函数。画家是我们数据函数表示的第二个例子。 +[27](#c2-fn-0027a) 例如,图 2.11:的`rogers`油漆工就是从一张灰度图像中构造出来的。对于给定帧中的每个点,`rogers`画师确定图像中在帧坐标映射下映射到它的点,并相应地对其进行着色。通过允许不同类型的绘制者,我们利用了 2.1.3 节中讨论的抽象数据的概念,我们认为有理数表示可以是满足适当条件的任何东西。在这里,我们利用了一个事实,即画师可以以任何方式实现,只要它在指定的框架中绘制东西。第 2.1.3 节还展示了如何将线对实现为函数。画家是我们数据函数表示的第二个例子。 [28](#c2-fn-0028a) 功能`rotate90`仅适用于方形框架的纯旋转,因为它还会拉伸和收缩图像以适应旋转后的框架。 diff --git a/docs/07.md b/docs/07.md index 62fe37c..9d916b7 100644 --- a/docs/07.md +++ b/docs/07.md @@ -568,11 +568,11 @@ 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 的绑定。 +图 3.1:显示了一个由三个框架组成的简单环境结构,标记为 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](img/c3-fig-0001.jpg) -[图 3.1](#c3-fig-0001a) 一个简单的环境结构。 +图 3.1:一个简单的环境结构。 环境对求值过程至关重要,因为它决定了表达式求值的上下文。事实上,可以说编程语言中的表达式本身没有任何意义。更确切地说,一个表达式只有在被评估的环境中才有意义。即使对像`display(1)`这样简单的表达式的解释也依赖于这样一种理解,即在名称`display`引用显示值的原始函数的上下文中操作。因此,在我们的评估模型中,我们总是说评估一个关于某些环境的表达式。为了描述与解释器的交互,我们将假设有一个全局环境,由单个框架(没有封闭环境)组成,该框架包括与原语函数相关联的名称的值。例如,`display`是基本显示函数的名称的想法是通过说名称`display`在全局环境中被绑定到基本显示函数来捕捉的。 @@ -604,21 +604,21 @@ const square = x => x * x; 该函数计算`x => x * x`并将`square`绑定到结果值,所有这些都在程序环境中完成。 -[图 3.2](#c3-fig-0002) 显示了评估该声明语句的结果。全局环境包含程序环境。为了减少混乱,在这个图之后,我们将不显示全局环境(因为它总是相同的),但是从程序环境向上的指针提醒我们它的存在。函数对象是一对,其代码指定函数有一个参数`x`和一个函数体`**return** x * x;`。函数的环境部分是一个指向程序环境的指针,因为 lambda 表达式就是在这个环境中被求值以产生函数的。程序框架中添加了一个新的绑定,它将函数对象与名称`square`相关联。 +图 3.2:显示了评估该声明语句的结果。全局环境包含程序环境。为了减少混乱,在这个图之后,我们将不显示全局环境(因为它总是相同的),但是从程序环境向上的指针提醒我们它的存在。函数对象是一对,其代码指定函数有一个参数`x`和一个函数体`**return** x * x;`。函数的环境部分是一个指向程序环境的指针,因为 lambda 表达式就是在这个环境中被求值以产生函数的。程序框架中添加了一个新的绑定,它将函数对象与名称`square`相关联。 ![c3-fig-0002.jpg](img/c3-fig-0002.jpg) -[图 3.2](#c3-fig-0002a) 在程序环境中评估`**function** square(x) { **return** x * x; }`产生的环境结构。 +图 3.2:在程序环境中评估`**function** square(x) { **return** x * x; }`产生的环境结构。 -一般来说,`**const**`、`**function**`、`**let**`给帧添加绑定。常量上禁止赋值,所以我们的环境模型需要区分引用常量的名称和引用变量的名称。我们通过在名字后面的冒号后面写一个等号来表示名字是一个常量。我们认为函数声明等同于常数声明; [^(14)](#c3-fn-0014) 观察[图 3.2](#c3-fig-0002) 中冒号后的等号。 +一般来说,`**const**`、`**function**`、`**let**`给帧添加绑定。常量上禁止赋值,所以我们的环境模型需要区分引用常量的名称和引用变量的名称。我们通过在名字后面的冒号后面写一个等号来表示名字是一个常量。我们认为函数声明等同于常数声明; [^(14)](#c3-fn-0014) 观察图 3.2:中冒号后的等号。 现在我们已经看到了函数是如何创建的,我们可以描述函数是如何应用的。环境模型指定:要将函数应用于参数,请创建一个新环境,该环境包含一个将参数绑定到参数值的框架。这个框架的封闭环境是由函数指定的环境。现在,在这个新环境中,评估函数体。 -为了说明这个规则是如何遵循的,[图 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。 +为了说明这个规则是如何遵循的,图 3.3:举例说明了在程序环境中对表达式`square(5)`求值所创建的环境结构,其中`square`是在图 3.2:中生成的函数。应用该函数会创建一个新的环境,在图中标记为 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](img/c3-fig-0003.jpg) -[图 3.3](#c3-fig-0003a) 在程序环境中评估`square(5)`创建的环境。 +图 3.3:在程序环境中评估`square(5)`创建的环境。 函数应用的环境模型可以概括为两个规则: @@ -645,13 +645,13 @@ function f(a) { } ``` -我们可以使用环境模型来分析同一个示例。[图 3.4](#c3-fig-0004) 显示了通过评估程序环境中`f`、`square`和`sum_of_squares`的定义而创建的三个功能对象。每个函数对象都由一些代码和一个指向程序环境的指针组成。 +我们可以使用环境模型来分析同一个示例。图 3.4:显示了通过评估程序环境中`f`、`square`和`sum_of_squares`的定义而创建的三个功能对象。每个函数对象都由一些代码和一个指向程序环境的指针组成。 ![c3-fig-0004.jpg](img/c3-fig-0004.jpg) -[图 3.4](#c3-fig-0004a) 程序框中的功能对象。 +图 3.4:程序框中的功能对象。 -在[图 3.5](#c3-fig-0005) 中,我们看到了通过评估表达式`f(5)`创建的环境结构。对`f`的调用创建了一个新的环境,E1,从一个帧开始,其中`f`的参数`a`被绑定到参数 5。在 E1,我们评价`f`的身体: +在图 3.5:中,我们看到了通过评估表达式`f(5)`创建的环境结构。对`f`的调用创建了一个新的环境,E1,从一个帧开始,其中`f`的参数`a`被绑定到参数 5。在 E1,我们评价`f`的身体: ```js return sum_of_squares(a + 1, a * 2); @@ -659,9 +659,9 @@ return sum_of_squares(a + 1, a * 2); ![c3-fig-0005.jpg](img/c3-fig-0005.jpg) -[图 3.5](#c3-fig-0005a) 使用[图 3.4](#c3-fig-0004) 中的函数评估`f(5)`创建的环境。 +图 3.5:使用图 3.4:中的函数评估`f(5)`创建的环境。 -为了评估 return 语句,我们首先评估 return 表达式的子表达式。第一个子表达式`sum_of_squares`的值是一个函数对象。(注意这个值是如何找到的:我们首先查看 E1 的第一帧,它不包含`sum_of_squares`的绑定。然后我们进入封闭环境,即程序环境,找到[图 3.4](#c3-fig-0004) 所示的绑定。)通过应用原语操作`+`和`*`来评估两个组合`a + 1`和`a * 2`以分别获得 6 和 10,来评估另外两个子表达式。 +为了评估 return 语句,我们首先评估 return 表达式的子表达式。第一个子表达式`sum_of_squares`的值是一个函数对象。(注意这个值是如何找到的:我们首先查看 E1 的第一帧,它不包含`sum_of_squares`的绑定。然后我们进入封闭环境,即程序环境,找到图 3.4:所示的绑定。)通过应用原语操作`+`和`*`来评估两个组合`a + 1`和`a * 2`以分别获得 6 和 10,来评估另外两个子表达式。 现在我们将函数对象`sum_of_squares`应用于参数 6 和 10。这导致了一个新的环境 E2,其中参数`x`和`y`被绑定到自变量。在 E2 内,我们评估该语句 @@ -734,11 +734,11 @@ W1(50); 50 ``` -[图 3.6](#c3-fig-0006) 显示了在程序环境中声明`make_withdraw`函数的结果。这产生了一个包含指向程序环境的指针的函数对象。到目前为止,这与我们已经看到的例子没有什么不同,只是函数体中的返回表达式本身是一个 lambda 表达式。 +图 3.6:显示了在程序环境中声明`make_withdraw`函数的结果。这产生了一个包含指向程序环境的指针的函数对象。到目前为止,这与我们已经看到的例子没有什么不同,只是函数体中的返回表达式本身是一个 lambda 表达式。 ![c3-fig-0006.jpg](img/c3-fig-0006.jpg) -[图 3.6](#c3-fig-0006a) 在程序环境中定义`make_withdraw`的结果。 +图 3.6:在程序环境中定义`make_withdraw`的结果。 当我们将函数`make_withdraw`应用于一个论点时,计算的有趣部分发生了: @@ -746,11 +746,11 @@ W1(50); const W1 = make_withdraw(100); ``` -像往常一样,我们首先建立一个环境 E1,其中参数`balance`被绑定到参数 100。在这个环境中,我们评估`make_withdraw`的主体,即返回表达式是 lambda 表达式的返回语句。对这个 lambda 表达式的求值构造了一个新的 function 对象,其代码由 lambda 表达式指定,其环境是 E1,即在其中对 lambda 表达式求值以产生函数的环境。结果函数对象是调用`make_withdraw`返回的值。这在程序环境中被绑定到`W1`,因为常量声明本身在程序环境中被评估。[图 3.7](#c3-fig-0007) 显示了最终的环境结构。 +像往常一样,我们首先建立一个环境 E1,其中参数`balance`被绑定到参数 100。在这个环境中,我们评估`make_withdraw`的主体,即返回表达式是 lambda 表达式的返回语句。对这个 lambda 表达式的求值构造了一个新的 function 对象,其代码由 lambda 表达式指定,其环境是 E1,即在其中对 lambda 表达式求值以产生函数的环境。结果函数对象是调用`make_withdraw`返回的值。这在程序环境中被绑定到`W1`,因为常量声明本身在程序环境中被评估。图 3.7:显示了最终的环境结构。 ![c3-fig-0007.jpg](img/c3-fig-0007.jpg) -[图 3.7](#c3-fig-0007a) 评估`**const** W1 = make_withdraw(100);`的结果。 +图 3.7:评估`**const** W1 = make_withdraw(100);`的结果。 现在我们可以分析当`W1`应用于一个论点时会发生什么: @@ -770,17 +770,17 @@ if (balance >= amount) { } ``` -由此产生的环境结构如图[图 3.8](#c3-fig-0008) 所示。被求值的表达式同时引用了`amount`和`balance`。变量`amount`将在环境中的第一帧中找到,而`balance`将通过跟随包围环境指针到 E1 找到。 +由此产生的环境结构如图图 3.8:所示。被求值的表达式同时引用了`amount`和`balance`。变量`amount`将在环境中的第一帧中找到,而`balance`将通过跟随包围环境指针到 E1 找到。 ![c3-fig-0008.jpg](img/c3-fig-0008.jpg) -[图 3.8](#c3-fig-0008a) 应用功能对象`W1`创建的环境。 +图 3.8:应用功能对象`W1`创建的环境。 -当赋值被执行时,`balance`在 E1 的绑定被改变。在完成对`W1`的调用时,`balance`为 50,包含`balance`的框架仍然被函数对象`W1`指向。绑定`amount`的框架(我们在其中执行了改变`balance`的代码)不再相关,因为构建它的函数调用已经终止,并且没有从环境的其他部分指向该框架的指针。下次调用`W1`时,这将构建一个绑定`amount`的新框架,其封闭环境是 E1。我们看到 E1 作为“位置”来保存函数对象`W1`的本地状态变量。[图 3.9](#c3-fig-0009) 显示了调用`W1`后的情况。 +当赋值被执行时,`balance`在 E1 的绑定被改变。在完成对`W1`的调用时,`balance`为 50,包含`balance`的框架仍然被函数对象`W1`指向。绑定`amount`的框架(我们在其中执行了改变`balance`的代码)不再相关,因为构建它的函数调用已经终止,并且没有从环境的其他部分指向该框架的指针。下次调用`W1`时,这将构建一个绑定`amount`的新框架,其封闭环境是 E1。我们看到 E1 作为“位置”来保存函数对象`W1`的本地状态变量。图 3.9:显示了调用`W1`后的情况。 ![c3-fig-0009.jpg](img/c3-fig-0009.jpg) -[图 3.9](#c3-fig-0009a) 环境调用到`W1`后。 +图 3.9:环境调用到`W1`后。 观察当我们通过再次调用`make_withdraw`创建第二个“撤回”对象时会发生什么: @@ -788,11 +788,11 @@ if (balance >= amount) { 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`。因此,对一个对象的本地状态的更改不会影响另一个对象。 +这就产生了图 3.10:的环境结构,说明`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](img/c3-fig-0010.jpg) -[图 3.10](#c3-fig-0010a) 使用`**const** W2 = make_withdraw(100);`创建第二个对象。 +图 3.10:使用`**const** W2 = make_withdraw(100);`创建第二个对象。 ##### 练习 3.10 @@ -847,11 +847,11 @@ function sqrt(x) { } ``` -现在我们可以使用环境模型来看看为什么这些内部声明的行为符合预期。[图 3.11](#c3-fig-0011) 显示了在表达式`sqrt(2)`的求值中,内部函数`is_good_enough`在`guess`等于 1 时第一次被调用的点。 +现在我们可以使用环境模型来看看为什么这些内部声明的行为符合预期。图 3.11:显示了在表达式`sqrt(2)`的求值中,内部函数`is_good_enough`在`guess`等于 1 时第一次被调用的点。 ![c3-fig-0011.jpg](img/c3-fig-0011.jpg) -[图 3.11](#c3-fig-0011a)`sqrt`功能同内部声明。 +图 3.11:`sqrt`功能同内部声明。 观察环境的结构。名称`sqrt`在程序环境中被绑定到一个函数对象,该函数对象的关联环境是程序环境。当调用`sqrt`时,形成了一个新的环境,E1,从属于程序环境,其中参数`x`被绑定到 2。然后在 E1 对`sqrt`的车身进行了评估。该主体是一个具有局部函数声明的块,因此 E1 用这些声明的新框架进行了扩展,产生了新的环境 E2。然后在 E2 中评估块体。因为正文中的第一条语句是 @@ -861,7 +861,7 @@ function is_good_enough(guess) { } ``` -评估这个声明在环境 E2 中创建了函数`is_good_enough`。更准确地说,E2 的第一帧中的名称`is_good_enough`被绑定到一个函数对象,该函数对象的关联环境是 E2。类似地,`improve`和`sqrt_iter`被定义为 E2 的函数。为简明起见,[图 3.11](#c3-fig-0011) 只显示了`is_good_enough`的功能对象。 +评估这个声明在环境 E2 中创建了函数`is_good_enough`。更准确地说,E2 的第一帧中的名称`is_good_enough`被绑定到一个函数对象,该函数对象的关联环境是 E2。类似地,`improve`和`sqrt_iter`被定义为 E2 的函数。为简明起见,图 3.11:只显示了`is_good_enough`的功能对象。 在局部函数被定义之后,表达式`sqrt_iter(1)`被评估,仍然在环境 E2 中。于是 E2 里绑定到`sqrt_iter`的函数对象被调用,参数为 1。这创建了一个环境 E3,其中`sqrt_iter`的参数`guess`被绑定到 1。函数`sqrt_iter`依次调用`is_good_enough`,用`guess`(来自 E3)的值作为`is_good_ enough`的参数。这建立了另一个环境,E4,其中`guess`(`is_good_enough`的参数)被绑定到 1。虽然`sqrt_iter`和`is_good_enough`都有一个名为`guess`的参数,但这是位于不同帧中的两个不同的局部变量。同样,E3 和 E4 都有 E2 作为它们的封闭环境,因为`sqrt_iter`和`is_good_enough`函数都有 E2 作为它们的环境部分。这样做的一个结果是出现在`is_ good_enough`主体中的名字`x`将引用出现在 E1 的`x`的绑定,即调用原始`sqrt`函数的`x`的值。 @@ -939,7 +939,7 @@ function f(x) { } ``` -在调用`f`的过程中,当`is_even`被调用时,环境图看起来类似于调用`sqrt_iter`时的[图 3.11](#c3-fig-0011) 中的环境图。函数`is_even`和`is_odd`在 E2 中被绑定到指向 E2 的函数对象,作为评估这些函数调用的环境。因此`is_even`体中的`is_odd`指的是正确的函数。虽然`is_odd`是在`is_even`之后定义的,但这与在`sqrt_iter`的主体中名称`improve`和名称`sqrt_iter`本身引用正确的函数没有什么不同。 +在调用`f`的过程中,当`is_even`被调用时,环境图看起来类似于调用`sqrt_iter`时的图 3.11:中的环境图。函数`is_even`和`is_odd`在 E2 中被绑定到指向 E2 的函数对象,作为评估这些函数调用的环境。因此`is_even`体中的`is_odd`指的是正确的函数。虽然`is_odd`是在`is_even`之后定义的,但这与在`sqrt_iter`的主体中名称`improve`和名称`sqrt_iter`本身引用正确的函数没有什么不同。 有了处理块内声明的方法,我们可以在顶层重新访问名字的声明。在 3.2.1 节中,我们看到在顶层声明的名字被添加到程序框架中。更好的解释是,整个程序放在一个隐式块中,在全局环境中进行求值。然后,上面描述的对块的处理处理顶层:全局环境由一个框架扩展,该框架包含隐式块中声明的所有名称的绑定。该框架是程序框架,而产生的环境是程序环境。 @@ -963,31 +963,31 @@ set_balance(account, new-value) 成对的基本变异器是`set_head`和`set_tail`。函数`set_head`有两个参数,第一个必须是一对。它修改这一对,用指向第二个参数`set_head`的指针替换`head`指针。 [^(19)](#c3-fn-0019) -举个例子,假设`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) +举个例子,假设`x`被绑定到`list(list("a", "b"), "c", "d")`,`y`被绑定到`list("e", "f")`,如图图 3.12:所示。对表达式`set_head(x, y)`求值会修改`x`绑定到的对,用`y`的值替换它的`head`。操作结果如图图 3.13:所示。结构`x`已经修改,现在等同于`list(list("e", "f"), "c", "d")`。由被替换的指针标识的代表列表`list("a", "b")`的对现在从原始结构中分离出来。 [^(20)](#c3-fn-0020) ![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")`。 +图 3.12:列出了`x`:`list(list("a", "b"), "c", "d")``y`:`list("e", "f")`。 ![c3-fig-0013.jpg](img/c3-fig-0013.jpg) -[图 3.13](#c3-fig-0013a) 图 3.12 中`set_head(x, y)`对列表的影响。 +图 3.13:图 3.12 中`set_head(x, y)`对列表的影响。 -将[图 3.13](#c3-fig-0013) 与[图 3.14](#c3-fig-0014) 进行比较,说明执行的结果 +将图 3.13:与图 3.14:进行比较,说明执行的结果 ```js 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")`,现在脱离了结构。 +将`x`和`y`绑定到图 3.12 的原列表中。名称`z`现在被绑定到由`pair`操作创建的新对;`x`绑定的列表不变。`set_tail`的操作与`set_head`类似。唯一不同的是,替换的是指针对的`tail`指针,而不是`head`指针。执行`set_tail(x, y)`对图 3.12:列表的影响如图图 3.15:所示。这里`x`的`tail`指针已经被指向`list("e", "f")`的指针所取代。还有,曾经是`x`的`tail`的列表`list("c", "d")`,现在脱离了结构。 ![c3-fig-0014.jpg](img/c3-fig-0014.jpg) -[图 3.14](#c3-fig-0014a) 图 3.12 中`**const** z = pair(y, tail(x));`对列表的影响。 +图 3.14:图 3.12 中`**const** z = pair(y, tail(x));`对列表的影响。 ![c3-fig-0015.jpg](img/c3-fig-0015.jpg) -[图 3.15](#c3-fig-0015a) 图 3.12 中`set_tail(x, y)`对列表的影响。 +图 3.15:图 3.12 中`set_tail(x, y)`对列表的影响。 函数`pair`通过创建新的对来构建新的列表结构,而`set_ head`和`set_tail`修改现有的对。事实上,我们可以用两个变异函数和一个函数`get_new_pair`来实现`pair`,该函数返回一个不属于任何现有列表结构的新对。我们获得新对,设置其指向指定对象的`head`和`tail`指针,并返回新对作为`pair`的结果。 [^(21)](#c3-fn-0021) @@ -1118,13 +1118,13 @@ const x = list("a", "b"); const z1 = pair(x, x); ``` -如图[图 3.16](#c3-fig-0016) 所示,`z1`为一对,其`head`和`tail`均指向同一对`x`。由`z1`的`head`和`tail`共享`x`是`pair`实现的直接方式的结果。一般来说,使用`pair`来构建列表将会产生一个互连的对结构,其中许多单独的对被许多不同的结构共享。 +如图图 3.16:所示,`z1`为一对,其`head`和`tail`均指向同一对`x`。由`z1`的`head`和`tail`共享`x`是`pair`实现的直接方式的结果。一般来说,使用`pair`来构建列表将会产生一个互连的对结构,其中许多单独的对被许多不同的结构共享。 ![c3-fig-0016.jpg](img/c3-fig-0016.jpg) -[图 3.16](#c3-fig-0016a) 由`pair(x, x)`组成的列表`z1`。 +图 3.16:由`pair(x, x)`组成的列表`z1`。 -与[图 3.16](#c3-fig-0016) 相比,[图 3.17](#c3-fig-0017) 显示了由创建的结构 +与图 3.16:相比,图 3.17:显示了由创建的结构 ```js const z2 = pair(list("a", "b"), list("a", "b")); @@ -1134,7 +1134,7 @@ const z2 = pair(list("a", "b"), list("a", "b")); ![c3-fig-0017.jpg](img/c3-fig-0017.jpg) -[图 3.17](#c3-fig-0017a) 由`pair(list("a", "b"), list("a", "b"))`组成的列表`z2`。 +图 3.17:由`pair(list("a", "b"), list("a", "b"))`组成的列表`z2`。 当被认为是一个列表时,`z1`和`z2`都表示“同一个”列表: @@ -1270,11 +1270,11 @@ head(x); 变异子`set_head`和`set_tail`使我们能够使用成对来构造数据结构,这些数据结构不能单独用`pair`、`head`和`tail`来构建。本节展示了如何使用对来表示一个称为队列的数据结构。3.3.3 节将展示如何表示称为表的数据结构。 -队列是从一端(称为队列的后)插入项目,从另一端(队列的前)删除项目的序列。[图 3.18](#c3-fig-0018) 显示了一个最初为空的队列,其中插入了物品`a`和`b`。然后移除`a`,插入`c`和`d`,移除`b`。因为项目总是按照它们被插入的顺序被移除,所以队列有时被称为先进先出缓冲器。 +队列是从一端(称为队列的后)插入项目,从另一端(队列的前)删除项目的序列。图 3.18:显示了一个最初为空的队列,其中插入了物品`a`和`b`。然后移除`a`,插入`c`和`d`,移除`b`。因为项目总是按照它们被插入的顺序被移除,所以队列有时被称为先进先出缓冲器。 ![c3-fig-0018.jpg](img/c3-fig-0018.jpg) -[图 3.18](#c3-fig-0018a) 队列操作。 +图 3.18:队列操作。 就数据抽象而言,我们可以将队列视为由以下一组操作定义的: @@ -1310,11 +1310,11 @@ head(x); 列表表示的困难在于需要扫描以找到列表的结尾。我们需要扫描的原因是,虽然将列表表示为一串对的标准方式很容易为我们提供指向列表开头的指针,但它没有提供指向列表结尾的容易访问的指针。避免这一缺点的修改是将队列表示为一个列表,以及一个指示列表中最后一对的附加指针。这样,当我们插入一个条目时,我们可以参考后面的指针,从而避免扫描列表。 -然后,队列被表示为一对指针,`front_ptr`和`rear_ptr`,它们分别指示普通列表中的第一对和最后一对。因为我们希望队列是一个可识别的对象,所以我们可以使用`pair`来组合这两个指针。因此,队列本身将是两个指针的`pair`。[图 3.19](#c3-fig-0019) 说明了这种表示。 +然后,队列被表示为一对指针,`front_ptr`和`rear_ptr`,它们分别指示普通列表中的第一对和最后一对。因为我们希望队列是一个可识别的对象,所以我们可以使用`pair`来组合这两个指针。因此,队列本身将是两个指针的`pair`。图 3.19:说明了这种表示。 ![c3-fig-0019.jpg](img/c3-fig-0019.jpg) -[图 3.19](#c3-fig-0019a) 实现一个队列作为一个有前后指针的链表。 +图 3.19:实现一个队列作为一个有前后指针的链表。 为了定义队列操作,我们使用以下函数,这些函数使我们能够选择和修改队列的前后指针: @@ -1365,9 +1365,9 @@ function insert_queue(queue, item) { ![c3-fig-0020.jpg](img/c3-fig-0020.jpg) -[图 3.20](#c3-fig-0020a) 在[图 3.19](#c3-fig-0019) 的队列上使用`insert_queue(q, "d")`的结果。 +图 3.20:在图 3.19:的队列上使用`insert_queue(q, "d")`的结果。 -要删除队列前面的项目,我们只需修改前面的指针,使其现在指向队列中的第二个项目,这可以通过跟随第一个项目的`tail`指针找到(参见[图 3.21](#c3-fig-0021)):[^(25)](#c3-fn-0025) +要删除队列前面的项目,我们只需修改前面的指针,使其现在指向队列中的第二个项目,这可以通过跟随第一个项目的`tail`指针找到(参见图 3.21:):[^(25)](#c3-fn-0025) ```js function delete_queue(queue) { @@ -1382,7 +1382,7 @@ function delete_queue(queue) { ![c3-fig-0021.jpg](img/c3-fig-0021.jpg) -[图 3.21](#c3-fig-0021a) 在[图 3.20](#c3-fig-0020) 的队列上使用`delete_queue(q)`的结果。 +图 3.21:在图 3.20:的队列上使用`delete_queue(q)`的结果。 ##### 练习 3.21 @@ -1430,7 +1430,7 @@ function make_queue() { 当我们在第 2 章中学习表示集合的各种方法时,我们在 2.3.3 节中提到了维护由标识键索引的记录表的任务。在 2.4.3 节的数据导向编程的实现中,我们广泛使用了二维表,其中的信息使用两个键来存储和检索。这里我们看到如何将表构建为可变列表结构。 -我们首先考虑一个一维表,其中每个值都存储在一个键下。我们将该表实现为一个记录列表,每个记录都被实现为一个由一个键和相关值组成的对。这些记录成对粘在一起形成一个列表,其`head`指向连续的记录。这些胶合对被称为工作台的支柱。为了在向表中添加新记录时有一个可以改变的地方,我们将表构建为一个标题为的列表。有标题的列表在开头有一个特殊的主干对,它保存一个虚拟的“记录”——在这个例子中是任意选择的字符串`"*table*"`。[图 3.22](#c3-fig-0022) 显示了表格的盒指针图 +我们首先考虑一个一维表,其中每个值都存储在一个键下。我们将该表实现为一个记录列表,每个记录都被实现为一个由一个键和相关值组成的对。这些记录成对粘在一起形成一个列表,其`head`指向连续的记录。这些胶合对被称为工作台的支柱。为了在向表中添加新记录时有一个可以改变的地方,我们将表构建为一个标题为的列表。有标题的列表在开头有一个特殊的主干对,它保存一个虚拟的“记录”——在这个例子中是任意选择的字符串`"*table*"`。图 3.22:显示了表格的盒指针图 ```js a: 1 @@ -1440,7 +1440,7 @@ c: 3 ![c3-fig-0022.jpg](img/c3-fig-0022.jpg) -[图 3.22](#c3-fig-0022a) 以表头列表表示的表格。 +图 3.22:以表头列表表示的表格。 为了从表中提取信息,我们使用了`lookup`函数,该函数将一个键作为参数,并返回相关的值(如果该键下没有存储值,则返回`undefined`)。函数`lookup`是根据`assoc`操作定义的,它需要一个键和一列记录作为参数。注意`assoc`不会看到虚拟记录。函数`assoc`返回给定键作为其`head`的记录。 [^(27)](#c3-fn-0027) 然后函数`lookup`检查`assoc`返回的结果记录是否不是`undefined`,并返回该记录的值(T12)。 @@ -1485,7 +1485,7 @@ function make_table() { ##### 二维表格 -在二维表中,每个值由两个键索引。我们可以将这样的表构造为一维表,其中每个键标识一个子表。[图 3.23](#c3-fig-0023) 显示了表格的盒指针图 +在二维表中,每个值由两个键索引。我们可以将这样的表构造为一维表,其中每个键标识一个子表。图 3.23:显示了表格的盒指针图 ```js "math": @@ -1501,7 +1501,7 @@ function make_table() { ![c3-fig-0023.jpg](img/c3-fig-0023.jpg) -[图 3.23](#c3-fig-0023a) 一个二维表格。 +图 3.23:一个二维表格。 当我们查找一个条目时,我们使用第一个键来标识正确的子表。然后,我们使用第二个键来标识子表中的记录。 @@ -1660,17 +1660,17 @@ 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。 +我们的电路计算模型将由与构成电路的基本元件相对应的对象组成。有根导线,承载个数字信号。一个数字信号在任何时候都可能只有两个可能值 0 和 1 中的一个。还有各种类型的数字功能盒,将携带输入信号的导线连接到其他输出导线。这种盒产生从它们的输入信号计算的输出信号。输出信号延迟的时间取决于功能盒的类型。例如,反相器是反转其输入的原始功能盒。如果反相器的输入信号变为 0,那么一个反相器延迟之后,反相器会将其输出信号变为 1。如果反相器的输入信号变为 1,那么一个反相器延迟之后,反相器会将其输出信号变为 0。我们象征性地绘制一个逆变器,如图 3.24 中的[所示。一个与门,也如图](#c3-fig-0024)图 3.24:所示,是一个具有两个输入和一个输出的原始功能盒。它将其输出信号驱动到输入的逻辑与值。也就是说,如果它的两个输入信号都变为 1,那么一个与门延迟时间之后,与门将迫使它的输出信号为 1;否则输出将为 0。一个或门是一个类似的双输入原始功能盒,它驱动其输出信号为输入的逻辑或值。也就是说,如果至少一个输入信号为 1,则输出将变为 1;否则输出将变成 0。 ![c3-fig-0024.jpg](img/c3-fig-0024.jpg) -[图 3.24](#c3-fig-0024a) 数字逻辑仿真器中的原始函数。 +图 3.24:数字逻辑仿真器中的原始函数。 -我们可以将原函数连接在一起,构造更复杂的函数。为此,我们将一些功能框的输出连接到其他功能框的输入。例如[图 3.25](#c3-fig-0025) 所示的半加法器电路由一个或门、两个与门和一个反相器组成。它有两个输入信号, A 和 B ,有两个输出信号, S 和 C 。当 A 和 B 中恰好有一个为 1 时 S 变为 1,当 A 和 B 都为 1 时 C 变为 1。从图中可以看出,由于存在延迟,输出可能会在不同的时间生成。数字电路设计中的许多困难都源于这一事实。 +我们可以将原函数连接在一起,构造更复杂的函数。为此,我们将一些功能框的输出连接到其他功能框的输入。例如图 3.25:所示的半加法器电路由一个或门、两个与门和一个反相器组成。它有两个输入信号, A 和 B ,有两个输出信号, S 和 C 。当 A 和 B 中恰好有一个为 1 时 S 变为 1,当 A 和 B 都为 1 时 C 变为 1。从图中可以看出,由于存在延迟,输出可能会在不同的时间生成。数字电路设计中的许多困难都源于这一事实。 ![c3-fig-0025.jpg](img/c3-fig-0025.jpg) -[图 3.25](#c3-fig-0025a) 一种半加法器电路。 +图 3.25:一种半加法器电路。 我们现在将构建一个程序来模拟我们想要研究的数字逻辑电路。该程序将构建模拟电线的计算对象,这些电线将“容纳”信号。功能盒将由在信号之间实施正确关系的功能来建模。 @@ -1733,7 +1733,7 @@ function full_adder(a, b, c_in, sum, c_out) { ![c3-fig-0026.jpg](img/c3-fig-0026.jpg) -[图 3.26](#c3-fig-0026a) 一种全加器电路。 +图 3.26:一种全加器电路。 本质上,我们的模拟器为我们提供了构建电路语言的工具。如果我们采用我们在 1.1 节中研究 JavaScript 时所用的语言的一般观点,我们可以说基本功能盒形成了语言的基本元素,连接盒一起提供了一种组合的方式,而将连接模式指定为功能作为一种抽象的方式。 @@ -1802,11 +1802,11 @@ function and_gate(a1, a2, output) { ##### 练习 3.30 -[图 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 位纹波进位加法器获得完整输出所需的延迟是多少,用与门、或门和反相器的延迟表示?] +图 3.27:显示了一个纹波进位加法器由 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](img/c3-fig-0027.jpg) -[图 3.27](#c3-fig-0027a) 一个用于 n 位数的纹波进位加法器。 +图 3.27:一个用于 n 位数的纹波进位加法器。 ##### 代表电线 @@ -1965,7 +1965,7 @@ probe("carry", carry); "carry 0, new value = 0" ``` -接下来,我们连接半加法器电路中的导线(如图[图 3.25](#c3-fig-0025) ),将`input_1`上的信号设置为 1,并运行模拟: +接下来,我们连接半加法器电路中的导线(如图图 3.25: ),将`input_1`上的信号设置为 1,并运行模拟: ```js half_adder(input_1, input_2, sum, carry); @@ -2126,11 +2126,11 @@ dAE = FL 9C = 5(F – 32) ``` -这样的约束可以认为是一个由原语加法器、乘法器和常量约束组成的网络([图 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]** +这样的约束可以认为是一个由原语加法器、乘法器和常量约束组成的网络(图 3.28: )。在图中,我们看到左侧的乘法器盒有三个端子,分别标为 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](img/c3-fig-0028.jpg) -[图 3.28](#c3-fig-0028a) 关系 9C = 5(F–32)表示为约束网络。 +图 3.28:关系 9C = 5(F–32)表示为约束网络。 这种网络的计算过程如下:当连接器被赋予一个值(由用户或由它所链接的约束框赋予)时,它唤醒所有与其相关联的约束(除了刚刚唤醒它的约束),以通知它们它具有一个值。每个被唤醒的约束框然后轮询其连接器,以查看是否有足够的信息来确定连接器的值。如果是这样,盒子设置连接器,然后唤醒所有相关的约束,依此类推。例如,在摄氏和华氏之间的转换中, w 、 x 和 y 立即被常量框分别设置为 9、5 和 32。连接器唤醒乘法器和加法器,它们确定没有足够的信息来继续。如果用户(或网络的其他部分)将 C 设置为一个值(比如 25),最左边的乘法器将被唤醒,它将 u 设置为 25 ^ 9 = 225。然后 u 唤醒第二个乘法器,其将 v 设置为 45,并且 v 唤醒加法器,其将 F 设置为 77。 @@ -2164,7 +2164,7 @@ function celsius_fahrenheit_converter(c, f) { } ``` -该函数创建内部连接器`u`、`v`、`w`、`x`和`y`,并使用原语约束构造函数`adder`、`multiplier`和`constant`将它们链接起来,如图[图 3.28](#c3-fig-0028) 所示。正如 3.3.4 节中的数字电路模拟器一样,用函数来表达这些原始元素的组合自动为我们的语言提供了一种对复合对象进行抽象的方法。 +该函数创建内部连接器`u`、`v`、`w`、`x`和`y`,并使用原语约束构造函数`adder`、`multiplier`和`constant`将它们链接起来,如图图 3.28:所示。正如 3.3.4 节中的数字电路模拟器一样,用函数来表达这些原始元素的组合自动为我们的语言提供了一种对复合对象进行抽象的方法。 为了观察运行中的网络,我们可以在连接器`C`和`F`上放置探针,使用类似于我们在第 3.3.4 节中用来监控电线的`probe`功能。在连接器上放置探针将导致每当连接器被赋予一个值时打印一条消息: @@ -2603,11 +2603,11 @@ balance = balance - amount; 作为每次取款过程的一部分执行。这包括三个步骤:(1)访问`balance`变量的值;(2)计算新的余额;(3)将`balance`设置为这个新值。如果彼得和保罗的提取同时执行这个语句,那么这两个提取可能会交错访问`balance`的顺序,并将其设置为新值。 -[图 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) +图 3.29:中的时序图描绘了一系列事件,其中`balance`从 100 开始,彼得退出 10,保罗退出 25,然而`balance`的最终值是 75。如图所示,出现这种异常的原因是,保罗将 75 分配给`balance`是在假设要递减的`balance`的值为 100 的情况下进行的。然而,当彼得把`balance`改成 90 时,这个假设就失效了。这对银行系统来说是一个灾难性的失败,因为系统中的货币总量没有得到保存。在交易之前,总金额为 100 美元。后来,彼得有 10 美元,保罗有 25 美元,银行有 75 美元。 [^(41)](#c3-fn-0041) ![c3-fig-0029.jpg](img/c3-fig-0029.jpg) -[图 3.29](#c3-fig-0029a) 时序图显示了两次银行取款中事件顺序的交错如何导致不正确的最终余额。 +图 3.29:时序图显示了两次银行取款中事件顺序的交错如何导致不正确的最终余额。 这里说明的一般现象是几个线程可能共享一个公共状态变量。使这变得复杂的是,可能有多个线程同时试图操纵共享状态。以银行账户为例,在每笔交易中,每个客户都应该能够表现得好像其他客户不存在一样。当客户以依赖于余额的方式改变余额时,他们必须能够假设,就在改变的时刻之前,余额仍然是他们所认为的那样。 @@ -2615,13 +2615,13 @@ balance = balance - amount; 上面的例子代表了潜移默化进入并发程序的细微错误。这种复杂性的根源在于不同线程之间共享的变量的赋值。我们已经知道,在编写使用赋值的程序时必须小心,因为计算的结果取决于赋值发生的顺序。对于并发线程,我们必须特别小心赋值,因为我们可能无法控制不同线程所做的赋值的顺序。如果几个这样的变化可能同时发生(比如两个储户访问一个联合账户),我们需要一些方法来确保我们的系统行为正确。例如,在从联合银行账户提款的情况下,我们必须确保资金得到保存。为了让并发程序正确运行,我们可能需要对并发执行设置一些限制。 -对并发性的一个可能的限制是规定不能同时发生两个改变任何共享状态变量的操作。这是一个极其严格的要求。对于分布式银行,它要求系统设计者确保一次只能进行一项交易。这既低效又过于保守。[图 3.30](#c3-fig-0030) 显示了彼得和保罗共享一个银行账户,而保罗也有一个私人账户。该图显示了从共享账户中的两次提款(一次由 Peter 提取,一次由 Paul 提取)以及向 Paul 的私人账户中的一次存款。 [^(43)](#c3-fn-0043) 从共享账户的两次提款必须不能并发(因为两者都访问和更新同一个账户),保罗的存款和提款必须不能并发(因为两者都访问和更新保罗钱包中的金额)。但是,允许保罗向他的私人账户存款与彼得从共享账户提款同时进行应该没有问题。 +对并发性的一个可能的限制是规定不能同时发生两个改变任何共享状态变量的操作。这是一个极其严格的要求。对于分布式银行,它要求系统设计者确保一次只能进行一项交易。这既低效又过于保守。图 3.30:显示了彼得和保罗共享一个银行账户,而保罗也有一个私人账户。该图显示了从共享账户中的两次提款(一次由 Peter 提取,一次由 Paul 提取)以及向 Paul 的私人账户中的一次存款。 [^(43)](#c3-fn-0043) 从共享账户的两次提款必须不能并发(因为两者都访问和更新同一个账户),保罗的存款和提款必须不能并发(因为两者都访问和更新保罗钱包中的金额)。但是,允许保罗向他的私人账户存款与彼得从共享账户提款同时进行应该没有问题。 ![c3-fig-0030.jpg](img/c3-fig-0030.jpg) -[图 3.30](#c3-fig-0030a) 银行 1 的联名账户和银行 2 的私人账户同时存取款。 +图 3.30:银行 1 的联名账户和银行 2 的私人账户同时存取款。 -对并发性不太严格的限制将确保并发系统产生相同的结果,就好像线程以某种顺序连续运行一样。这一要求有两个重要方面。首先,它不要求线程实际上按顺序运行,而只是产生与它们按顺序运行的结果相同的结果。对于[图 3.30](#c3-fig-0030) 中的示例,银行账户系统的设计者可以安全地允许 Paul 的存款和 Peter 的取款同时发生,因为最终结果将与这两个操作顺序发生的结果相同。第二,一个并发程序可能产生不止一个可能的“正确”结果,因为我们只要求结果与某个顺序相同。例如,假设彼得和保罗的联名账户开始时有 100 美元,彼得存入 40 美元,而保罗同时取出账户中的一半资金。那么顺序执行可能导致账户余额为 70 美元或 90 美元(参见练习 3.38)。 [^(44)](#c3-fn-0044) +对并发性不太严格的限制将确保并发系统产生相同的结果,就好像线程以某种顺序连续运行一样。这一要求有两个重要方面。首先,它不要求线程实际上按顺序运行,而只是产生与它们按顺序运行的结果相同的结果。对于图 3.30:中的示例,银行账户系统的设计者可以安全地允许 Paul 的存款和 Peter 的取款同时发生,因为最终结果将与这两个操作顺序发生的结果相同。第二,一个并发程序可能产生不止一个可能的“正确”结果,因为我们只要求结果与某个顺序相同。例如,假设彼得和保罗的联名账户开始时有 100 美元,彼得存入 40 美元,而保罗同时取出账户中的一半资金。那么顺序执行可能导致账户余额为 70 美元或 90 美元(参见练习 3.38)。 [^(44)](#c3-fn-0044) 对于并发程序的正确执行,仍然有较弱的要求。一个模拟扩散(比如说,一个物体中的热流)的程序可能由大量的线程组成,每一个线程代表一个小的空间,它们同时更新它们的值。每个线程重复地将其值更改为其自身值和其邻居值的平均值。该算法收敛到正确的答案,而与运算的完成顺序无关;不需要对共享值的并发使用进行任何限制。 @@ -2726,7 +2726,7 @@ function make_account(balance) { } ``` -通过这种实现,两个线程不能同时从一个帐户中取款或向一个帐户中存款。这消除了[图 3.29](#c3-fig-0029) 中所示的错误来源,其中 Peter 在 Paul 访问余额以计算新值的时间和 Paul 实际执行分配的时间之间更改了账户余额。另一方面,每个帐户都有自己的序列化程序,因此不同帐户的存取可以同时进行。 +通过这种实现,两个线程不能同时从一个帐户中取款或向一个帐户中存款。这消除了图 3.29:中所示的错误来源,其中 Peter 在 Paul 访问余额以计算新值的时间和 Paul 实际执行分配的时间之间更改了账户余额。另一方面,每个帐户都有自己的序列化程序,因此不同帐户的存取可以同时进行。 ##### 练习 3.39 @@ -3009,7 +3009,7 @@ function test_and_set(cell) { *`test_and_set`的实际实现取决于我们的系统如何运行并发线程的细节。例如,我们可能使用时间分片机制在顺序处理器上执行并发线程,该机制在线程间循环,允许每个线程在中断之前运行一小段时间,然后继续运行下一个线程。在这种情况下,`test_and_set`可以通过在测试和设置期间禁用时间片来工作。或者,多处理计算机直接在硬件中提供支持原子操作的指令。 [^(51)](#c3-fn-0051)* *##### 练习 3.46 -假设我们使用文本中所示的普通函数实现了`test_and_set`,而没有试图使操作原子化。画一个类似于[图 3.29](#c3-fig-0029) 中的时序图来演示互斥体的实现是如何因允许两个线程同时获取互斥体而失败的。 +假设我们使用文本中所示的普通函数实现了`test_and_set`,而没有试图使操作原子化。画一个类似于图 3.29:中的时序图来演示互斥体的实现是如何因允许两个线程同时获取互斥体而失败的。 ##### 练习 3.47 @@ -3372,11 +3372,11 @@ stream_ref(primes, 50); 233 ``` -思考由`sieve`建立的信号处理系统是很有趣的,如图[图 3.31](#c3-fig-0031) 中的“亨德森图”所示。 [^(63)](#c3-fn-0063) 输入流输入到一个“un `pair` er”中,该“un`pair`er”将流的第一个元素与流的其余部分分开。第一个元素用于构建一个整除滤波器,其余的元素通过该滤波器,滤波器的输出被馈送到另一个筛箱。然后,原始的第一元件被连接到内部筛的输出,以形成输出流。因此,不仅流是无限的,信号处理器也是无限的,因为筛子中包含一个筛子。 +思考由`sieve`建立的信号处理系统是很有趣的,如图图 3.31:中的“亨德森图”所示。 [^(63)](#c3-fn-0063) 输入流输入到一个“un `pair` er”中,该“un`pair`er”将流的第一个元素与流的其余部分分开。第一个元素用于构建一个整除滤波器,其余的元素通过该滤波器,滤波器的输出被馈送到另一个筛箱。然后,原始的第一元件被连接到内部筛的输出,以形成输出流。因此,不仅流是无限的,信号处理器也是无限的,因为筛子中包含一个筛子。 ![c3-fig-0031.jpg](img/c3-fig-0031.jpg) -[图 3.31](#c3-fig-0031a) 把素筛看成一个信号处理系统。每条实线代表一个正在传输的值流。从`head`到`pair`和`filter`的虚线表示这是单个值而不是流。 +图 3.31:把素筛看成一个信号处理系统。每条实线代表一个正在传输的值流。从`head`到`pair`和`filter`的虚线表示这是单个值而不是流。 ##### 隐式定义流 @@ -3927,19 +3927,19 @@ function integral(integrand, initial_value, dt) { } ``` -[图 3.32](#c3-fig-0036) 是对应于`integral`功能的信号处理系统图。输入流由 dt 缩放并通过加法器,加法器的输出通过相同的加法器返回。`integ`定义中的自参考通过反馈回路反映在图中,反馈回路将加法器的输出连接到其中一个输入。 +图 3.32:是对应于`integral`功能的信号处理系统图。输入流由 dt 缩放并通过加法器,加法器的输出通过相同的加法器返回。`integ`定义中的自参考通过反馈回路反映在图中,反馈回路将加法器的输出连接到其中一个输入。 ![c3-fig-0032.jpg](img/c3-fig-0032.jpg) -[图 3.32](#c3-fig-0036a)`integral`功能视为信号处理系统。 +图 3.32:`integral`功能视为信号处理系统。 ##### 练习 3.73 -我们可以使用流来模拟电路,以表示一系列时间内的电流或电压值。例如,假设我们有一个由电阻 R 的电阻器和电容 C 的电容器串联而成的 RC 电路。电路对注入电流 i 的电压响应 v 由[图 3.33](#c3-fig-0037) 中的公式确定,其结构如随附的信号流程图所示。 +我们可以使用流来模拟电路,以表示一系列时间内的电流或电压值。例如,假设我们有一个由电阻 R 的电阻器和电容 C 的电容器串联而成的 RC 电路。电路对注入电流 i 的电压响应 v 由图 3.33:中的公式确定,其结构如随附的信号流程图所示。 ![c3-fig-0033.jpg](img/c3-fig-0033.jpg) -[图 3.33](#c3-fig-0037a) 一个 RC 电路和相关的信号流程图。 +图 3.33:一个 RC 电路和相关的信号流程图。 写一个函数`RC`来模拟这个电路。`RC`应将 R 、 C 和 dt 的值作为输入,并应返回一个函数,该函数将代表电流 i 的流和电容器电压 v0 的初始值作为输入,并产生电压流 v 作为输出。例如,您应该能够使用`RC`来模拟一个 RC 电路,其中 R = 5 欧姆, C = 1 法拉,并且通过评估`**const** RC1 = RC(5, 1, 0.5)`获得 0.5 秒的时间步长。这将`RC1`定义为一个函数,该函数采用代表电流时序的流和初始电容器电压,并产生电压输出流。 @@ -4001,11 +4001,11 @@ const integ = pair(initial_value, 解释器处理这种隐式定义的能力取决于在 lambda 表达式中包装对`add_streams`的调用所导致的延迟。如果没有这个延迟,解释器就不能在评估对`add_streams`的调用之前构造`integ`,这需要已经定义了`integ`。一般来说,这样的延迟对于使用流来模拟包含循环的信号处理系统是至关重要的。没有延迟,我们的模型将不得不公式化,以便任何信号处理组件的输入都将在产生输出之前得到充分评估。这将禁止循环。 -不幸的是,具有循环的系统的流模型可能需要使用超过目前所见的流编程模式的延迟。例如,[图 3.34](#c3-fig-0039) 所示为求解微分方程 dy/dt=f(y)的信号处理系统,其中 f 为给定函数。该图示出了将 f 应用于其输入信号的映射部件,该映射部件在反馈回路中以非常类似于模拟计算机电路的方式链接到积分器,该模拟计算机电路实际上用于求解这种方程。 +不幸的是,具有循环的系统的流模型可能需要使用超过目前所见的流编程模式的延迟。例如,图 3.34:所示为求解微分方程 dy/dt=f(y)的信号处理系统,其中 f 为给定函数。该图示出了将 f 应用于其输入信号的映射部件,该映射部件在反馈回路中以非常类似于模拟计算机电路的方式链接到积分器,该模拟计算机电路实际上用于求解这种方程。 ![c3-fig-0034.jpg](img/c3-fig-0034.jpg) -[图 3.34](#c3-fig-0039a) 一个求解方程 dy/dt=f(y)的“模拟计算机电路”。 +图 3.34:一个求解方程 dy/dt=f(y)的“模拟计算机电路”。 假设给我们一个初始值 y0 用于 y ,我们可以尝试使用函数来建模这个系统 @@ -4076,11 +4076,11 @@ function integral(integrand, initial_value, dt) { ![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)) +建模为 y 的输出流由包含环路的网络生成。这是因为 d²y/dt²的值取决于 y 和 dy / dt 的值,而这两者都是通过对 d²y/dt^(2^(的积分来确定的我们要编码的图表如图 3.35:所示。编写一个函数`solve_2nd`,该函数将常量 a 、 b 和 dt 以及初始值 y [0] 和 dy [0] 作为参数,并生成连续值 y)) ![c3-fig-0035.jpg](img/c3-fig-0035.jpg) -[图 3.35](#c3-fig-0040a) 二阶线性微分方程解的信号流图。 +图 3.35:二阶线性微分方程解的信号流图。 ##### 练习 3.79 @@ -4088,7 +4088,7 @@ function integral(integrand, initial_value, dt) { ##### 练习 3.80 -串联 RLC 电路由一个电阻、一个电容和一个电感串联而成,如图[图 3.36](#c3-fig-0041) 所示。如果 R 、 L 和 C 是电阻、电感和电容,那么这三个元件的电压( v )和电流( i )之间的关系由以下等式描述 +串联 RLC 电路由一个电阻、一个电容和一个电感串联而成,如图图 3.36:所示。如果 R 、 L 和 C 是电阻、电感和电容,那么这三个元件的电压( v )和电流( i )之间的关系由以下等式描述 ![c3-fig-5013.jpg](img/c3-fig-5013.jpg) @@ -4107,11 +4107,11 @@ vC = vL + vR ![c3-fig-0036.jpg](img/c3-fig-0036.jpg) -[图 3.36](#c3-fig-0041a) 一系列 RLC 电路。 +图 3.36:一系列 RLC 电路。 ![c3-fig-0037.jpg](img/c3-fig-0037.jpg) -[图 3.37](#c3-fig-0042a) 串联 RLC 电路解决方案的信号流图。 +图 3.37:串联 RLC 电路解决方案的信号流图。 编写一个函数`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 伏。 @@ -4224,11 +4224,11 @@ function stream_withdraw(balance, amount_stream) { 用对象建模是强大而直观的,很大程度上是因为这符合与我们所在的世界互动的感觉。然而,正如我们在本章中反复看到的,这些模型提出了约束事件顺序和同步多个过程的棘手问题。避免这些问题的可能性刺激了函数式编程语言的发展,它不包括任何赋值或可变数据的规定。在这种语言中,所有函数都实现了其参数的明确定义的数学函数,其行为不会改变。函数方法对于处理并发系统非常有吸引力。 [^(75)](#c3-fn-0075) -另一方面,如果我们仔细观察,我们可以看到与时间相关的问题也悄悄进入功能模型。当我们希望设计交互系统时,尤其是对独立实体之间的交互进行建模时,会出现一个特别麻烦的领域。例如,再次考虑允许联合银行账户的银行系统的实现。在使用赋值和对象的传统系统中,我们将通过让 Peter 和 Paul 向同一个银行帐户对象发送他们的交易请求来模拟 Peter 和 Paul 共享一个帐户的事实,正如我们在 3.1.3 节中看到的。从流的角度来看,这里没有“对象”本身,我们已经指出,银行帐户可以被建模为一个流程,该流程对一个事务请求流进行操作以产生一个响应流。因此,我们可以通过将彼得的交易请求流与保罗的请求流合并,并将结果提供给银行账户流流程,来模拟彼得和保罗拥有联合银行账户的事实,如图[图 3.38](#c3-fig-0043) 所示。 +另一方面,如果我们仔细观察,我们可以看到与时间相关的问题也悄悄进入功能模型。当我们希望设计交互系统时,尤其是对独立实体之间的交互进行建模时,会出现一个特别麻烦的领域。例如,再次考虑允许联合银行账户的银行系统的实现。在使用赋值和对象的传统系统中,我们将通过让 Peter 和 Paul 向同一个银行帐户对象发送他们的交易请求来模拟 Peter 和 Paul 共享一个帐户的事实,正如我们在 3.1.3 节中看到的。从流的角度来看,这里没有“对象”本身,我们已经指出,银行帐户可以被建模为一个流程,该流程对一个事务请求流进行操作以产生一个响应流。因此,我们可以通过将彼得的交易请求流与保罗的请求流合并,并将结果提供给银行账户流流程,来模拟彼得和保罗拥有联合银行账户的事实,如图图 3.38:所示。 ![c3-fig-0038.jpg](img/c3-fig-0038.jpg) -[图 3.38](#c3-fig-0043a) 一个联名银行账户,通过合并两个交易请求流来建模。 +图 3.38:一个联名银行账户,通过合并两个交易请求流来建模。 这个公式的问题在于合并的概念。通过简单地交替接受彼得的一个请求和保罗的一个请求来合并这两个流是不行的。假设 Paul 很少访问该帐户。我们很难强迫 Peter 在 Paul 发出第二笔交易之前等待 Paul 访问帐户。然而,这样的合并被实现时,它必须以某种方式交错两个事务流,这种方式受到 Peter 和 Paul 所理解的“实时”的约束,也就是说,如果 Peter 和 Paul 相遇,他们可以同意某些事务在会议之前被处理,而其他事务在会议之后被处理。 [^(76)](#c3-fn-0076) 这正是我们在第 3.4.1 节中必须处理的约束,我们发现需要引入显式同步,以确保在带状态的对象的并发处理中事件的“正确”顺序。因此,在试图支持函数式风格时,合并来自不同代理的输入的需要再次引入了函数式风格想要消除的相同问题。 diff --git a/docs/08.md b/docs/08.md index a233fe7..b5099b5 100644 --- a/docs/08.md +++ b/docs/08.md @@ -38,11 +38,11 @@ 1. 1。若要评估函数应用程序,请评估子表达式,然后将函数子表达式的值应用于参数子表达式的值。 2. 2。要将复合函数应用于一组参数,请在新环境中评估函数体。要构建这个环境,请将函数对象的环境部分扩展一个框架,在该框架中,函数的参数被绑定到该函数所应用到的参数。 -这两个规则描述了评估过程的本质,一个基本循环,在这个循环中,要在环境中评估的语句和表达式被简化为要应用于参数的函数,这些函数又被简化为要在新环境中评估的新语句和表达式,以此类推,直到我们深入到名称(其值在环境中被查找)以及直接应用的运算符和原始函数(参见[图 4.1](#c4-fig-0001) )。 [⁴](#c4-fn-0004) 这个评估周期将通过评估器中两个关键函数`evaluate`和`apply`的相互作用来体现,这在 4.1.1 节中有描述(参见[图 4.1](#c4-fig-0001) )。 +这两个规则描述了评估过程的本质,一个基本循环,在这个循环中,要在环境中评估的语句和表达式被简化为要应用于参数的函数,这些函数又被简化为要在新环境中评估的新语句和表达式,以此类推,直到我们深入到名称(其值在环境中被查找)以及直接应用的运算符和原始函数(参见图 4.1: )。 [⁴](#c4-fn-0004) 这个评估周期将通过评估器中两个关键函数`evaluate`和`apply`的相互作用来体现,这在 4.1.1 节中有描述(参见图 4.1: )。 ![c4-fig-0001.jpg](img/c4-fig-0001.jpg) -[图 4.1](#c4-fig-0001a)`evaluate`—`apply`循环暴露了一种计算机语言的本质。 +图 4.1:`evaluate`—`apply`循环暴露了一种计算机语言的本质。 评估器的实现将取决于定义要评估的语句和表达式的语法的函数。我们将使用数据抽象来使评估器独立于语言的表示。例如,我们使用抽象谓词`is_assignment`来测试赋值,并使用抽象选择器`assignment_symbol`和`assignment_value_expression`来访问赋值的各个部分,而不是选择用一个以名字开头后跟`=`的字符串来表示赋值。第 4.1.2 节中介绍的数据抽象层将允许评估者保持独立于具体的语法问题,例如解释语言的关键字,以及表示程序组件的数据结构的选择。第 4.1.3 节中还描述了一些操作,这些操作指定了功能和环境的表示。例如,`make_function`构造复合函数,`lookup_symbol_value`访问名称的值,`apply_primitive_function`将一个原始函数应用于给定的参数列表。 @@ -280,11 +280,11 @@ list("sequence", 评估员让人想起第 2.3.2 节中讨论的符号微分程序。两个程序都处理符号数据。在这两个程序中,对对象进行操作的结果是通过对对象的各个部分进行递归操作并根据对象的类型组合结果来确定的。在这两个程序中,我们都使用了数据抽象来将操作的一般规则与对象如何表示的细节分离开来。在微分程序中,这意味着同一个微分函数可以处理前缀形式、中缀形式或其他形式的代数表达式。对于评估者来说,这意味着被评估语言的语法完全由`parse`和对`parse`产生的标记列表进行分类和提取的函数决定。 -[图 4.2](#c4-fig-0002) 描绘了由语法谓词和选择器形成的抽象屏障,它们将评估器与程序的标记列表表示连接起来,而标记列表表示又通过`parse`与字符串表示分离。下面我们描述程序组件的解析,并列出相应的语法谓词和选择器,以及构造函数(如果需要的话)。 +图 4.2:描绘了由语法谓词和选择器形成的抽象屏障,它们将评估器与程序的标记列表表示连接起来,而标记列表表示又通过`parse`与字符串表示分离。下面我们描述程序组件的解析,并列出相应的语法谓词和选择器,以及构造函数(如果需要的话)。 ![c4-fig-0002.jpg](img/c4-fig-0002.jpg) -[图 4.2](#c4-fig-0002a) 评估器中的语法抽象。 +图 4.2:评估器中的语法抽象。 ##### 文字表达 @@ -1106,17 +1106,17 @@ function factorial(n) { } ``` -我们可以把这个程序看作是对一台机器的描述,这台机器包含递减、相乘和相等测试的部件,还有一个两位开关和另一台阶乘机器。(阶乘机器是无限的,因为它包含另一个阶乘机器。)[图 4.3](#c4-fig-0003) 是阶乘机器的流程图,显示了各部分是如何连接在一起的。 +我们可以把这个程序看作是对一台机器的描述,这台机器包含递减、相乘和相等测试的部件,还有一个两位开关和另一台阶乘机器。(阶乘机器是无限的,因为它包含另一个阶乘机器。)图 4.3:是阶乘机器的流程图,显示了各部分是如何连接在一起的。 ![c4-fig-0003.jpg](img/c4-fig-0003.jpg) -[图 4.3](#c4-fig-0003a) 阶乘程序,被视为抽象机器。 +图 4.3:阶乘程序,被视为抽象机器。 -同样,我们可以把评价者看作一台非常特殊的机器,它接受对机器的描述作为输入。给定该输入,评估器对自身进行配置以仿真所描述的机器。例如,如果我们将`factorial`的定义提供给评估者,如图[图 4.4](#c4-fig-0004) 所示,评估者将能够计算阶乘。 +同样,我们可以把评价者看作一台非常特殊的机器,它接受对机器的描述作为输入。给定该输入,评估器对自身进行配置以仿真所描述的机器。例如,如果我们将`factorial`的定义提供给评估者,如图图 4.4:所示,评估者将能够计算阶乘。 ![c4-fig-0004.jpg](img/c4-fig-0004.jpg) -[图 4.4](#c4-fig-0004a) 仿真阶乘机的评估器。 +图 4.4:仿真阶乘机的评估器。 从这个角度来看,我们的评估者被看作是一台通用机器。当这些被描述为 JavaScript 程序时,它模仿其他机器。 [^(19)](#c4-fn-0019) 这是惊人的。试着想象一个用于电路的模拟评估器。这将是一个电路,它将一个编码计划的信号作为输入,用于其他电路,如滤波器。给定该输入,电路评估器将表现得像具有相同描述的滤波器。这样一个通用电路复杂得几乎无法想象。值得注意的是,程序评估器是一个相当简单的程序。 [^(20)](#c4-fn-0020) @@ -3702,11 +3702,11 @@ job($x, list("computer", "programmer")) 通过使用流来组织针对帧的模式测试。给定一个帧,匹配过程逐个通过数据库条目。对于每一个数据库条目,匹配器产生一个特殊的符号表示匹配失败,或者产生一个帧的扩展。所有数据库条目的结果被收集到一个流中,该流通过一个过滤器来剔除失败。结果是通过匹配数据库中的某个断言来扩展给定帧的所有帧的流。 [^(61)](#c4-fn-0061) -在我们的系统中,一个查询获取一个输入帧流,并对该流中的每一帧执行上述匹配操作,如图[图 4.5](#c4-fig-0005) 所示。也就是说,对于输入流中的每个帧,查询通过匹配数据库中的断言来生成一个新的流,该流由该帧的所有扩展组成。所有这些流然后被组合成一个巨大的流,它包含输入流中每个帧的所有可能的扩展。这个流是查询的输出。 +在我们的系统中,一个查询获取一个输入帧流,并对该流中的每一帧执行上述匹配操作,如图图 4.5:所示。也就是说,对于输入流中的每个帧,查询通过匹配数据库中的断言来生成一个新的流,该流由该帧的所有扩展组成。所有这些流然后被组合成一个巨大的流,它包含输入流中每个帧的所有可能的扩展。这个流是查询的输出。 ![c4-fig-0005.jpg](img/c4-fig-0005.jpg) -[图 4.5](#c4-fig-0005a) 一个查询处理一个帧流。 +图 4.5:一个查询处理一个帧流。 为了回答一个简单的查询,我们使用包含单个空帧的输入流的查询。产生的输出流包含对空框架的所有扩展(即我们的查询的所有答案)。然后,这个帧流用于生成原始查询模式的副本流,其中变量由每个帧中的值实例化,这就是最终打印的流。 @@ -3731,17 +3731,17 @@ can_do_job($x, list("computer", "programmer", "trainee")) job($person, $x) ``` -以与给定的`$x`绑定一致的方式。每个这样的匹配将产生一个包含`$x`和`$person`绑定的帧。两个查询的`and`可以看作是两个分量查询的串联组合,如图[图 4.6](#c4-fig-0006) 所示。通过第一个查询过滤器的帧被第二个查询过滤并进一步扩展。 +以与给定的`$x`绑定一致的方式。每个这样的匹配将产生一个包含`$x`和`$person`绑定的帧。两个查询的`and`可以看作是两个分量查询的串联组合,如图图 4.6:所示。通过第一个查询过滤器的帧被第二个查询过滤并进一步扩展。 ![c4-fig-0006.jpg](img/c4-fig-0006.jpg) -[图 4.6](#c4-fig-0006a) 两个查询的`and`组合是通过对连续的帧流进行操作产生的。 +图 4.6:两个查询的`and`组合是通过对连续的帧流进行操作产生的。 -[图 4.7](#c4-fig-0007) 显示了计算两个查询的`or`的类似方法,作为两个组件查询的并行组合。每个查询分别扩展输入的帧流。这两个结果流然后被合并以产生最终的输出流。 +图 4.7:显示了计算两个查询的`or`的类似方法,作为两个组件查询的并行组合。每个查询分别扩展输入的帧流。这两个结果流然后被合并以产生最终的输出流。 ![c4-fig-0007.jpg](img/c4-fig-0007.jpg) -[图 4.7](#c4-fig-0007a) 两个查询的`or`组合是通过对帧流并行操作并合并结果产生的。 +图 4.7:两个查询的`or`组合是通过对帧流并行操作并合并结果产生的。 即使从这个高层次的描述来看,复合查询的处理也很慢。例如,由于一个查询可能为每个输入帧产生一个以上的输出帧,并且`and`中的每个查询都从上一个查询中获得其输入帧,在最坏的情况下,`and`查询可能不得不执行与查询数量成指数关系的匹配(参见练习 4.73)。虽然只处理简单查询的系统非常实用,但处理复杂查询却非常困难。 [^(63)](#c4-fn-0063) @@ -3853,7 +3853,7 @@ rule(lives_near($person_1, $person_2), ##### 查询计算器和驱动程序循环 -尽管潜在的匹配操作很复杂,但系统的组织很像任何语言的评估器。协调匹配操作的函数称为`evaluate_query`,它的作用类似于 JavaScript 的`evaluate`函数。函数`evaluate_query`将一个查询和一个帧流作为输入。它的输出是一个帧流,对应于查询模式的成功匹配,扩展了输入流中的一些帧,如图[图 4.5](#c4-fig-0005) 所示。和`evaluate`一样,`evaluate_query`对不同类型的表达式(查询)进行分类,并为每种类型分配适当的函数。每个语法形式(`and`、`or`、`not`和`javascript_predicate`)都有一个函数,一个函数用于简单查询。 +尽管潜在的匹配操作很复杂,但系统的组织很像任何语言的评估器。协调匹配操作的函数称为`evaluate_query`,它的作用类似于 JavaScript 的`evaluate`函数。函数`evaluate_query`将一个查询和一个帧流作为输入。它的输出是一个帧流,对应于查询模式的成功匹配,扩展了输入流中的一些帧,如图图 4.5:所示。和`evaluate`一样,`evaluate_query`对不同类型的表达式(查询)进行分类,并为每种类型分配适当的函数。每个语法形式(`and`、`or`、`not`和`javascript_predicate`)都有一个函数,一个函数用于简单查询。 驱动程序循环类似于本章中其他赋值器的`driver_loop`函数,读取用户输入的查询。对于每个查询,它使用查询和由单个空帧组成的流调用`evaluate_query`。这将产生所有可能匹配的流(空框架的所有可能扩展)。对于结果流中的每个帧,它使用在帧中找到的变量值实例化原始查询。然后打印这个实例化的查询流。 [^(68)](#c4-fn-0068) @@ -4122,7 +4122,7 @@ put("and", "evaluate_query", conjoin); 设置`evaluate_query`在遇到`and`时分派给`conjoin`。 -我们类似地处理`or`查询,如图[图 4.7](#c4-fig-0007) 所示。使用 4.4.4.6 部分的`interleave_delayed`函数分别计算并合并`or`的各种析取项的输出流。(参见练习 4.68 和 4.69。) +我们类似地处理`or`查询,如图图 4.7:所示。使用 4.4.4.6 部分的`interleave_delayed`函数分别计算并合并`or`的各种析取项的输出流。(参见练习 4.68 和 4.69。) ```js function disjoin(disjuncts, frame_stream) { @@ -4516,11 +4516,11 @@ convert_to_query_syntax(parse('job($x, list("computer", "wizard"));')); 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`如何桥接它们。 +查询系统函数,比如 4.4.4.5 部分的`add_rule_or_assertion`和 4.4.4.2 部分的`evaluate_query`,使用选择器和谓词,比如下面声明的`type`、`contents`、`is_rule`和`first_conjunct`,对特定于查询语言的表示进行操作。图 4.8:描述了查询系统使用的三个抽象障碍,以及转换函数`parse`、`unparse`和`convert_to_query_syntax`如何桥接它们。 ![c4-fig-0008.jpg](img/c4-fig-0008.jpg) -[图 4.8](#c4-fig-0008a) 查询系统中的语法抽象。 +图 4.8:查询系统中的语法抽象。 ##### 处理模式变量 @@ -4945,7 +4945,7 @@ put("unique", "evaluate_query", uniquely_asserted); ##### 练习 4.73 -我们将`and`实现为一系列查询的组合([图 4.6](#c4-fig-0006) )是优雅的,但它是低效的,因为在处理`and`的第二个查询时,我们必须扫描数据库以获得第一个查询产生的每一帧。如果数据库有 N 个元素,并且一个典型的查询产生与 N 成比例的输出帧的数量(比如说 N / k ),那么为第一个查询产生的每一帧扫描数据库将需要 N²/k 调用模式匹配器。另一种方法是分别处理`and`的两个子句,然后寻找所有兼容的输出帧对。如果每个查询产生 N / k 个输出帧,那么这意味着我们必须执行 N²/k²兼容性检查——比我们当前方法中所需的匹配数量少了 k 个因子。 +我们将`and`实现为一系列查询的组合(图 4.6: )是优雅的,但它是低效的,因为在处理`and`的第二个查询时,我们必须扫描数据库以获得第一个查询产生的每一帧。如果数据库有 N 个元素,并且一个典型的查询产生与 N 成比例的输出帧的数量(比如说 N / k ),那么为第一个查询产生的每一帧扫描数据库将需要 N²/k 调用模式匹配器。另一种方法是分别处理`and`的两个子句,然后寻找所有兼容的输出帧对。如果每个查询产生 N / k 个输出帧,那么这意味着我们必须执行 N²/k²兼容性检查——比我们当前方法中所需的匹配数量少了 k 个因子。 设计一个使用这种策略的`and`实现。您必须实现一个函数,该函数将两个帧作为输入,检查帧中的绑定是否兼容,如果兼容,则生成一个合并两组绑定的帧。这个操作类似于统一。 diff --git a/docs/09.md b/docs/09.md index eb4c82a..9532fbd 100644 --- a/docs/09.md +++ b/docs/09.md @@ -27,19 +27,19 @@ function gcd(a, b) { 执行这种算法的机器必须记录两个数字, a 和 b ,所以让我们假设这些数字存储在两个具有这些名字的寄存器中。所需的基本操作是测试寄存器`b`的内容是否为零,并计算寄存器`a`的内容除以寄存器`b`的内容的余数。余数运算是一个复杂的过程,但是现在假设我们有一个计算余数的原始设备。在 GCD 算法的每个周期,寄存器`a`的内容必须被寄存器`b`的内容替换,而`b`的内容必须被`a`的旧内容除以`b`的旧内容的余数替换。如果这些替换可以同时进行,这将是很方便的,但是在我们的寄存器机器模型中,我们将假设在每个步骤中只有一个寄存器可以被分配一个新值。为了完成替换,我们的机器将使用第三个“临时”寄存器,我们称之为`t`。(首先将余数放入`t`,然后将`b`的内容放入`a`,最后将`t`中存储的余数放入`b`。) -我们可以使用[图 5.1](#c5-fig-0001) 中所示的数据路径图来说明该机器所需的寄存器和操作。在该图中,寄存器(`a`、`b`和`t`)由矩形表示。向寄存器赋值的每种方式都由一个箭头表示,箭头后面有一个按钮(绘制为),从数据源指向寄存器。按下按钮时,该按钮允许源端的值“流入”指定的寄存器。每个按钮旁边的标签是我们用来指代该按钮的名称。这些名称是任意的,可以选择具有助记值的名称(例如,`a<-b`表示按下将寄存器`b`的内容分配给寄存器`a`的按钮)。一个寄存器的数据来源可以是另一个寄存器(如在`a<-b`赋值中),一个运算结果(如在`t<-r`赋值中),或一个常数(一个不能改变的内置值,在数据路径图中用包含常数的三角形表示)。 +我们可以使用图 5.1:中所示的数据路径图来说明该机器所需的寄存器和操作。在该图中,寄存器(`a`、`b`和`t`)由矩形表示。向寄存器赋值的每种方式都由一个箭头表示,箭头后面有一个按钮(绘制为),从数据源指向寄存器。按下按钮时,该按钮允许源端的值“流入”指定的寄存器。每个按钮旁边的标签是我们用来指代该按钮的名称。这些名称是任意的,可以选择具有助记值的名称(例如,`a<-b`表示按下将寄存器`b`的内容分配给寄存器`a`的按钮)。一个寄存器的数据来源可以是另一个寄存器(如在`a<-b`赋值中),一个运算结果(如在`t<-r`赋值中),或一个常数(一个不能改变的内置值,在数据路径图中用包含常数的三角形表示)。 ![c5-fig-0001.jpg](img/c5-fig-0001.jpg) -[图 5.1](#c5-fig-0001a)GCD 机床的数据路径。 +图 5.1:GCD 机床的数据路径。 -根据常数和寄存器内容计算值的操作在数据路径图中用包含操作名称的梯形表示。例如,[图 5.1](#c5-fig-0001) 中标有`rem`的方框表示计算其所连接的寄存器`a`和`b`的剩余内容的操作。箭头(无按钮)从输入寄存器和常量指向方框,箭头将运算的输出值连接到寄存器。测试由包含测试名称的圆圈表示。比如我们的 GCD 机器有一个操作,测试寄存器`b`的内容是否为零。测试也有来自其输入寄存器和常数的箭头,但是它没有输出箭头;它的值由控制器使用,而不是由数据路径使用。总的来说,数据路径图显示了机器所需的寄存器和操作,以及它们必须如何连接。如果我们把箭头看作电线,把按钮看作开关,那么数据路径图就很像由电子元件构成的机器的接线图。 +根据常数和寄存器内容计算值的操作在数据路径图中用包含操作名称的梯形表示。例如,图 5.1:中标有`rem`的方框表示计算其所连接的寄存器`a`和`b`的剩余内容的操作。箭头(无按钮)从输入寄存器和常量指向方框,箭头将运算的输出值连接到寄存器。测试由包含测试名称的圆圈表示。比如我们的 GCD 机器有一个操作,测试寄存器`b`的内容是否为零。测试也有来自其输入寄存器和常数的箭头,但是它没有输出箭头;它的值由控制器使用,而不是由数据路径使用。总的来说,数据路径图显示了机器所需的寄存器和操作,以及它们必须如何连接。如果我们把箭头看作电线,把按钮看作开关,那么数据路径图就很像由电子元件构成的机器的接线图。 -为了让数据路径实际计算 gcd,必须按正确的顺序按下按钮。我们将根据控制器图来描述这个序列,如图[图 5.2](#c5-fig-0002) 所示。控制器图的元素指示数据路径组件应该如何操作。控制器图中的矩形框标识要按下的数据路径按钮,箭头描述从一个步骤到下一个步骤的顺序。图中的菱形代表一个决定。根据菱形中标识的数据路径测试值,将遵循两个排序箭头中的一个。我们可以从物理类比的角度来解释控制器:把这个图想象成一个弹子在其中滚动的迷宫。当弹球滚进一个盒子时,它会按下由盒子命名的数据路径按钮。当弹球滚入一个决策节点时(比如对`b` = 0 的测试),它会离开由指示的测试结果所确定的路径上的节点。 +为了让数据路径实际计算 gcd,必须按正确的顺序按下按钮。我们将根据控制器图来描述这个序列,如图图 5.2:所示。控制器图的元素指示数据路径组件应该如何操作。控制器图中的矩形框标识要按下的数据路径按钮,箭头描述从一个步骤到下一个步骤的顺序。图中的菱形代表一个决定。根据菱形中标识的数据路径测试值,将遵循两个排序箭头中的一个。我们可以从物理类比的角度来解释控制器:把这个图想象成一个弹子在其中滚动的迷宫。当弹球滚进一个盒子时,它会按下由盒子命名的数据路径按钮。当弹球滚入一个决策节点时(比如对`b` = 0 的测试),它会离开由指示的测试结果所确定的路径上的节点。 ![c5-fig-0002.jpg](img/c5-fig-0002.jpg) -[图 5.2](#c5-fig-0002a)GCD 机的控制器。 +图 5.2:GCD 机的控制器。 总之,数据路径和控制器完整地描述了一台计算 gcd 的机器。在将数字放入寄存器`a`和`b`后,我们在标记为`start`的地方启动控制器(滚动的弹球)。当控制器到达`done`时,我们将在寄存器`a`中找到 GCD 的值。 @@ -74,11 +74,11 @@ function factorial(n) { 机器从控制器指令序列的开始处开始,并在执行到序列末尾时停止。除了当分支改变控制流时,指令按照它们被列出的顺序执行。 -[图 5.3](#c5-fig-0003) 显示了这样描述的 GCD 机器。这个例子仅仅暗示了这些描述的一般性,因为 GCD 机器是一个非常简单的例子:每个寄存器只有一个按钮,每个按钮和测试在控制器中只使用一次。 +图 5.3:显示了这样描述的 GCD 机器。这个例子仅仅暗示了这些描述的一般性,因为 GCD 机器是一个非常简单的例子:每个寄存器只有一个按钮,每个按钮和测试在控制器中只使用一次。 ![c5-fig-0003.jpg](img/c5-fig-0003.jpg) -[图 5.3](#c5-fig-0003a) 一台规格的 GCD 机。 +图 5.3:一台规格的 GCD 机。 遗憾的是,这样的描述很难读懂。为了理解控制器指令,我们必须不断地参考按钮名称和操作名称的定义,为了理解按钮的功能,我们可能需要参考操作名称的定义。因此,我们将转换我们的符号来组合来自数据路径和控制器描述的信息,这样我们就可以一起看到它们。 @@ -122,11 +122,11 @@ controller( perform(list(op("display"), reg("a"))) ``` -[图 5.4](#c5-fig-0004) 显示了新 GCD 机器的数据路径和控制器。我们没有让机器在打印出答案后停下来,而是让它重新开始,这样它可以重复读取一对数字,计算它们的 GCD,并打印出结果。这个结构就像我们在第四章的解释器中使用的驱动循环。 +图 5.4:显示了新 GCD 机器的数据路径和控制器。我们没有让机器在打印出答案后停下来,而是让它重新开始,这样它可以重复读取一对数字,计算它们的 GCD,并打印出结果。这个结构就像我们在第四章的解释器中使用的驱动循环。 ![c5-fig-0004.jpg](img/c5-fig-0004.jpg) -[图 5.4](#c5-fig-0004a) 读取输入并打印结果的 GCD 机器。 +图 5.4:读取输入并打印结果的 GCD 机器。 ### 5.1.2 机械设计中的抽象 @@ -142,21 +142,21 @@ function remainder(n, d) { } ``` -因此,我们可以用减法运算和比较测试来代替 GCD 机器数据路径中的余数运算。[图 5.5](#c5-fig-0005) 显示了加工机器的数据路径和控制器。指令 +因此,我们可以用减法运算和比较测试来代替 GCD 机器数据路径中的余数运算。图 5.5:显示了加工机器的数据路径和控制器。指令 ```js assign("t", list(op("rem"), reg("a"), reg("b"))) ``` -在 GCD 中,控制器定义被包含一个循环的指令序列所取代,如图[图 5.6](#c5-fig-0006) 所示。 +在 GCD 中,控制器定义被包含一个循环的指令序列所取代,如图图 5.6:所示。 ![c5-fig-0005.jpg](img/c5-fig-0005.jpg) -[图 5.5](#c5-fig-0005a) 精心设计的 GCD 机床的数据路径和控制器。 +图 5.5:精心设计的 GCD 机床的数据路径和控制器。 ![c5-fig-0006.jpg](img/c5-fig-0006.jpg) -[图 5.6](#c5-fig-0006a) 图 5.5 中 GCD 机控制器指令序列。 +图 5.6:图 5.5 中 GCD 机控制器指令序列。 ##### 练习 5.3 @@ -183,23 +183,23 @@ function sqrt(x) { ### 5.1.3 子程序 -当设计一台执行计算的机器时,我们通常更喜欢安排组件由计算的不同部分共享,而不是复制组件。考虑一个包含两个 GCD 计算的机器——一个计算寄存器`a`和`b`内容的 GCD,另一个计算寄存器`c`和`d`内容的 GCD。我们可以从假设我们有一个原始的`gcd`操作开始,然后根据更多的原始操作扩展`gcd`的两个实例。[图 5.7](#c5-fig-0007) 只显示了最终机器数据路径的 GCD 部分,没有显示它们如何连接到机器的其余部分。该图还显示了机器控制器序列的相应部分。 +当设计一台执行计算的机器时,我们通常更喜欢安排组件由计算的不同部分共享,而不是复制组件。考虑一个包含两个 GCD 计算的机器——一个计算寄存器`a`和`b`内容的 GCD,另一个计算寄存器`c`和`d`内容的 GCD。我们可以从假设我们有一个原始的`gcd`操作开始,然后根据更多的原始操作扩展`gcd`的两个实例。图 5.7:只显示了最终机器数据路径的 GCD 部分,没有显示它们如何连接到机器的其余部分。该图还显示了机器控制器序列的相应部分。 ![c5-fig-0007.jpg](img/c5-fig-0007.jpg) -[图 5.7](#c5-fig-0007a) 带有两个 GCD 计算的机器的数据路径和控制器序列部分。 +图 5.7:带有两个 GCD 计算的机器的数据路径和控制器序列部分。 这台机器有两个余数操作箱和两个相等测试箱。如果复制的部件很复杂,就像余料箱一样,这将不是制造机器的经济方法。我们可以通过对两个 GCD 计算使用相同的组件来避免复制数据路径组件,只要这样做不会影响更大机器的其余计算。如果控制器到达`gcd_2`时不需要寄存器`a`和`b`中的值(或者如果这些值可以移动到其他寄存器进行保管),我们可以改变机器,使其在计算第二个和第一个 GCD 时使用寄存器`a`和`b`,而不是寄存器`c`和`d`。如果我们这样做,我们将获得如图 5.8 所示的控制器序列。 ![c5-fig-0008.jpg](img/c5-fig-0008.jpg) -[图 5.8](#c5-fig-0008a) 使用相同数据路径组件进行两种不同 GCD 计算的机器的控制器序列部分。 +图 5.8:使用相同数据路径组件进行两种不同 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`指令。 +我们已经移除了重复的数据路径组件(因此数据路径再次如图图 5.1:所示),但是控制器现在有两个 GCD 序列,它们的不同之处仅在于入口点标签。最好是将这两个序列替换成一个单独的序列——一个`gcd` 子程序——在这个序列的末尾,我们分支回到主指令序列中的正确位置。我们可以这样完成:在转移到`gcd`之前,我们将一个区别值(比如 0 或 1)放入一个特殊的寄存器`continue`。在`gcd`子程序结束时,我们返回到`after_gcd_1`或`after_gcd_2`,这取决于`continue`寄存器的值。图 5.9:显示了生成的控制器序列的相关部分,其中仅包含一份`gcd`指令。 ![c5-fig-0009.jpg](img/c5-fig-0009.jpg) -[图 5.9](#c5-fig-0009a) 使用`continue`寄存器避免[图 5.8](#c5-fig-0008) 中的重复控制器序列。 +图 5.9:使用`continue`寄存器避免图 5.8:中的重复控制器序列。 对于处理小问题来说,这是一种合理的方法,但是如果在控制器序列中有许多 GCD 计算的实例,这将会很尴尬。为了决定在`gcd`子例程之后在哪里继续执行,我们需要测试所有使用`gcd`的地方的数据路径和控制器中的分支指令。实现子例程的一个更强大的方法是让`continue`寄存器保存控制器序列中入口点的标签,当子例程结束时,应该在该入口点继续执行。实现这种策略需要在数据路径和寄存器机器的控制器之间有一种新的连接:必须有一种方法在控制器序列中给寄存器分配一个标签,使得这个值可以从寄存器中取出并用于在指定的入口点继续执行。 @@ -207,7 +207,7 @@ function sqrt(x) { ![c5-fig-0010.jpg](img/c5-fig-0010.jpg) -[图 5.10](#c5-fig-0010a) 给`continue`寄存器分配标签简化并概括了[图 5.9](#c5-fig-0009) 所示的策略。 +图 5.10:给`continue`寄存器分配标签简化并概括了图 5.9:所示的策略。 具有一个以上子程序的机器可以使用多个延续寄存器(如`gcd_continue`、`factorial_continue`),或者我们可以让所有子程序共享一个`continue`寄存器。共享更经济,但是如果我们有一个子程序(`sub1`)调用另一个子程序(`sub2`),我们必须小心。除非`sub1`在设置`continue`以调用`sub2`之前将`continue`的内容保存在其他寄存器中,否则`sub1`将不知道调用完成后该去哪里。下一节中开发的处理递归的机制也为嵌套子例程调用的问题提供了更好的解决方案。 @@ -247,11 +247,11 @@ function gcd(a, b) { 借助堆栈,我们可以为每个阶乘子问题重用阶乘机器的数据路径的单个副本。在重用操作数据路径的控制器序列时,也存在类似的设计问题。为了重新执行阶乘计算,控制器不能像迭代过程那样简单地循环回到起点,因为在求解(n–1)之后!子问题机器仍然必须将结果乘以 n 。控制器必须暂停其对 n 的计算!,求解(n–1)!子问题,然后继续它的 n 的计算!。阶乘计算的这种观点建议使用第 5.1.3 节中描述的子程序机制,它让控制器使用一个`continue`寄存器转移到序列中解决子问题的部分,然后从主问题停止的地方继续。因此,我们可以创建一个阶乘子例程,返回到存储在`continue`寄存器中的入口点。在每个子程序调用前后,我们保存和恢复`continue`,就像我们保存和恢复`n`寄存器一样,因为阶乘计算的每个“级别”都将使用相同的`continue`寄存器。也就是说,当阶乘子例程为子问题调用自己时,它必须在`continue`中放入一个新值,但是为了返回到调用它来解决子问题的地方,它将需要旧值。 -[图 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`的旧内容不再有用。只需要新值,即子计算产生的值。 +图 5.11:显示了实现递归`factorial`功能的机器的数据路径和控制器。机器有一个堆栈和三个寄存器,称为`n`、`val`和`continue`。为了简化数据路径图,我们没有命名寄存器分配按钮,只命名了堆栈操作按钮(`sc`和`sn`保存寄存器,`rc`和`rn`恢复寄存器)。为了操作机器,我们将希望计算其阶乘的数字放入寄存器`n`并启动机器。当机器到达`fact_done`时,计算结束,答案将在`val`寄存器中找到。在控制器序列中,`n`和`continue`在每次递归调用之前保存,并在调用返回时恢复。通过分支到存储在`continue`中的位置来完成呼叫返回。当机器启动时,寄存器`continue`被初始化,以便最后一次返回将到达`fact_done`。保存阶乘计算结果的`val`寄存器在递归调用之前没有保存,因为在子例程返回之后`val`的旧内容不再有用。只需要新值,即子计算产生的值。 ![c5-fig-0011.jpg](img/c5-fig-0011.jpg) -[图 5.11](#c5-fig-0011a) 递归阶乘机。 +图 5.11:递归阶乘机。 虽然原则上阶乘计算需要一个无限的机器,但图 5.11 中的机器实际上是有限的,除了堆栈,它可能是无限的。然而,堆栈的任何特定物理实现都是有限大小的,这将限制机器可以处理的递归调用的深度。factorial 的实现说明了将递归算法实现为由栈扩充的普通寄存器机器的一般策略。当遇到递归子问题时,我们在堆栈上保存子问题解决后需要其当前值的寄存器,解决递归子问题,然后恢复保存的寄存器并继续执行主问题。必须始终保存`continue`寄存器。是否有其他寄存器需要保存取决于特定的机器,因为不是所有的递归计算都需要在子问题求解过程中修改的寄存器的初始值(见练习 5.4)。 @@ -273,7 +273,7 @@ function fib(n) { ![c5-fig-0012.jpg](img/c5-fig-0012.jpg) -[图 5.12](#c5-fig-0012a) 用于计算斐波那契数列的机器控制器。 +图 5.12:用于计算斐波那契数列的机器控制器。 ##### 练习 5.4 @@ -500,11 +500,11 @@ function push(stack, value) { ##### 基本机器 -`make_new_machine`函数,如图[图 5.13](#c5-fig-0013) 所示,构造了一个对象,其本地状态由一个堆栈、一个最初为空的指令序列、一个最初包含一个初始化堆栈操作的操作列表以及一个最初包含两个名为`flag`和`pc`(代表“程序计数器”)的注册表。内部函数`allocate_register`向注册表添加新条目,内部函数`lookup_register`在表中查找寄存器。 +`make_new_machine`函数,如图图 5.13:所示,构造了一个对象,其本地状态由一个堆栈、一个最初为空的指令序列、一个最初包含一个初始化堆栈操作的操作列表以及一个最初包含两个名为`flag`和`pc`(代表“程序计数器”)的注册表。内部函数`allocate_register`向注册表添加新条目,内部函数`lookup_register`在表中查找寄存器。 ![c5-fig-0013.jpg](img/c5-fig-0013.jpg) -[图 5.13](#c5-fig-0013a)`make_new_machine`功能实现了基本机型。 +图 5.13:`make_new_machine`功能实现了基本机型。 `flag`寄存器用于控制模拟机器中的分支。我们的`test`指令将`flag`的内容设置为测试结果(真或假)。我们的`branch`指令通过检查`flag`的内容来决定是否分支。 @@ -968,7 +968,7 @@ restore(y); `restore`的含义有几种合理的可能: -1. a. `restore(y)`将保存在堆栈上的最后一个值放入`y`中,不管该值来自哪个寄存器。这是我们的模拟器的行为方式。展示如何利用此行为从第 5.1.4 节的斐波那契机器中删除一条指令([图 5.12](#c5-fig-0012) )。 +1. a. `restore(y)`将保存在堆栈上的最后一个值放入`y`中,不管该值来自哪个寄存器。这是我们的模拟器的行为方式。展示如何利用此行为从第 5.1.4 节的斐波那契机器中删除一条指令(图 5.12: )。 2. b. `restore(y)`将保存在堆栈上的最后一个值放入`y`,但前提是该值是从`y`保存的;否则,它会发出错误信号。修改模拟器,使其以这种方式运行。您必须更改`save`以将寄存器名称与值一起放入堆栈。 3. c. 修改模拟器,使其以这种方式运行。您将不得不为每个寄存器关联一个单独的堆栈。你应该让`initialize_stack`操作初始化所有的寄存器堆栈。 @@ -981,7 +981,7 @@ restore(y); * `save` d 或`restore` d 寄存器的列表(无副本); * 对于每个寄存器,一个其被分配的源的列表(没有重复)(例如,图 5.11 的[的阶乘机器中寄存器`val`的源是`constant(1)`和`list(op("*"), reg("n"), reg("val"))`)。](#c5-fig-0011) -将消息传递接口扩展到机器,以提供对这些新信息的访问。为了测试你的分析器,从[图 5.12](#c5-fig-0012) 中定义斐波那契机,并检查你构建的列表。 +将消息传递接口扩展到机器,以提供对这些新信息的访问。为了测试你的分析器,从图 5.12:中定义斐波那契机,并检查你构建的列表。 ##### 练习 5.12 @@ -1053,7 +1053,7 @@ function make_stack() { ##### 练习 5.13 -测量推动次数和计算 n 所需的最大堆叠深度!对于 n 的各种小值,使用[图 5.11](#c5-fig-0011) 所示的阶乘机。根据您的数据,根据 n 确定用于计算 n 的推送操作总数和最大堆栈深度的公式!对于任意 n1。注意,每个都是 n 的线性函数,因此由两个常数决定。为了打印统计数据,您必须用初始化堆栈和打印统计数据的指令来扩充阶乘机器。您可能还想修改机器,使其重复读取 n 的值,计算阶乘,并打印结果(就像我们在[图 5.4](#c5-fig-0004) 中对 GCD 机器所做的那样),这样您就不必重复调用`get_register_contents`、`set_register_contents`和`start`。 +测量推动次数和计算 n 所需的最大堆叠深度!对于 n 的各种小值,使用图 5.11:所示的阶乘机。根据您的数据,根据 n 确定用于计算 n 的推送操作总数和最大堆栈深度的公式!对于任意 n1。注意,每个都是 n 的线性函数,因此由两个常数决定。为了打印统计数据,您必须用初始化堆栈和打印统计数据的指令来扩充阶乘机器。您可能还想修改机器,使其重复读取 n 的值,计算阶乘,并打印结果(就像我们在图 5.4:中对 GCD 机器所做的那样),这样您就不必重复调用`get_register_contents`、`set_register_contents`和`start`。 ##### 练习 5.14 @@ -1122,11 +1122,11 @@ 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`中的空白位置可能包含其他列表结构的部分(这里不感兴趣)。 +我们可以使用向量来实现列表结构存储器所需的基本对结构。我们假设计算机内存分为两个向量:`the_heads`和`the_tails`。我们将如下表示列表结构:指向一对的指针是两个向量的索引。该对的`head`是指定索引的`the_heads`中的条目,该对的尾部是指定索引的`the_tails`中的条目。我们还需要一种对象而不是对的表示(比如数字和字符串),以及一种区分不同类型数据的方法。有许多方法可以实现这一点,但它们都归结为使用类型化指针,也就是说,扩展“指针”的概念,以包含关于数据类型的信息。 [⁹](#c5-fn-0009) 数据类型使系统能够区分指向一对(由“对”数据类型和内存向量索引组成)的指针和指向其他类型数据(由其他数据类型和用于表示该类型数据的任何内容组成)的指针。如果两个数据对象的指针相同,则认为它们是相同的(`===`)。图 5.14:说明了使用这种方法来表示`list(list(1, 2), 3, 4)`,其盒指针图也已显示。我们使用字母前缀来表示数据类型信息。因此,指向具有索引 5 的对的指针被表示为`p5`,空列表由指针`e0`表示,而指向数字 4 的指针被表示为`n4`。在盒指针图中,我们在每一对的左下方指示了向量索引,该索引指定了该对的`head`和`tail`的存储位置。`the_heads`和`the_tails`中的空白位置可能包含其他列表结构的部分(这里不感兴趣)。 ![c5-fig-0014.jpg](img/c5-fig-0014.jpg) -[图 5.14](#c5-fig-0014a) 列表的盒指针和内存向量表示法`list(list(1, 2), 3, 4)`。 +图 5.14:列表的盒指针和内存向量表示法`list(list(1, 2), 3, 4)`。 一个指向数字的指针,比如`n4`,可能由指示数字数据的类型和数字 4 的实际表示组成。 [^(10)](#c5-fn-0010) 为了处理太大而无法在分配给单个指针的固定空间中表示的数字,我们可以使用不同的 bignum 数据类型,为此指针指定一个列表,在其中存储数字的各个部分。 [^(11)](#c5-fn-0011) @@ -1218,7 +1218,7 @@ assign("the_stack", constant(null)) ##### 练习 5.19 -画出由产生的列表结构的盒指针表示法和内存向量表示法(如图[图 5.14](#c5-fig-0014) +画出由产生的列表结构的盒指针表示法和内存向量表示法(如图图 5.14: ```js const x = pair(1, 2); @@ -1267,11 +1267,11 @@ accumulate((x, y) => x + y, 我们现在用寄存器机器语言来更详细地描述停止复制算法。我们将假设有一个名为`root`的寄存器,它包含一个指向最终指向所有可访问数据的结构的指针。这可以通过在开始垃圾收集之前将所有机器寄存器的内容存储在由`root`指向的预分配列表中来安排。 [^(17)](#c5-fn-0017) 我们还假设,除了当前的工作内存之外,还有空闲内存可供我们复制有用的数据。当前的工作内存由向量组成,向量的基址在名为`the_heads`和`the_tails`的寄存器中,空闲内存在名为`new_heads`和`new_tails`的寄存器中。 -当我们耗尽当前工作内存中的空闲单元时,也就是说,当一个`pair`操作试图将`free`指针递增到内存向量的末尾之外时,垃圾收集被触发。当垃圾收集过程完成时,`root`指针将指向新内存,所有可从`root`访问的对象将被移动到新内存,并且`free`指针将指示新内存中可分配新对的下一个位置。此外,工作内存和新内存的角色将互换——新的内存对将在新内存中构建,从`free`指示的位置开始,并且(先前的)工作内存将可用作下一次垃圾收集的新内存。[图 5.15](#c5-fig-0015) 显示了垃圾收集前后的内存安排。 +当我们耗尽当前工作内存中的空闲单元时,也就是说,当一个`pair`操作试图将`free`指针递增到内存向量的末尾之外时,垃圾收集被触发。当垃圾收集过程完成时,`root`指针将指向新内存,所有可从`root`访问的对象将被移动到新内存,并且`free`指针将指示新内存中可分配新对的下一个位置。此外,工作内存和新内存的角色将互换——新的内存对将在新内存中构建,从`free`指示的位置开始,并且(先前的)工作内存将可用作下一次垃圾收集的新内存。图 5.15:显示了垃圾收集前后的内存安排。 ![c5-fig-0015.jpg](img/c5-fig-0015.jpg) -[图 5.15](#c5-fig-0015a) 垃圾收集进程对内存的重新配置。 +图 5.15:垃圾收集进程对内存的重新配置。 垃圾收集过程的状态通过维护两个指针来控制:`free`和`scan`。这些被初始化为指向新内存的开始。该算法首先将由`root`指向的配对重新定位到新内存的开头。该对被复制,`root`指针被调整以指向新位置,并且`free`指针递增。此外,还会标记该对的旧位置,以表明其内容已被移动。标记过程如下:在`head`位置,我们放置一个特殊标签,表明这是一个已经移动的物体。(这样的物体传统上被称为心碎。) [^(18)](#c5-fn-0018) 在`tail`位置我们放置一个转发地址,它指向对象被移动到的位置。 @@ -1372,11 +1372,11 @@ accumulate((x, y) => x + y, ## 5.4 显式控制评估器 -在 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) +在 5.1 节中,我们看到了如何将简单的 JavaScript 程序转换成注册机器的描述。我们现在将在一个更复杂的程序上执行这种转换,即 4 . 1 . 1–4 . 1 . 4 节的元循环求值器,它展示了如何用函数`evaluate`和`apply`来描述 JavaScript 解释器的行为。我们在本节开发的显式控制评估器展示了评估过程中使用的底层函数调用和参数传递机制是如何根据寄存器和堆栈上的操作来描述的。此外,显式控制评估器可以作为 JavaScript 解释器的实现,用与传统计算机的本机语言非常相似的语言编写。评估器可以由 5.2 节的寄存器机器模拟器执行。或者,它可以作为构建 JavaScript 评估器的机器语言实现的起点,甚至可以作为评估 JavaScript 程序的专用机器的起点。图 5.16:显示了这样一个硬件实现:一个充当 Scheme 评估器的硅片,Scheme 是本书最初版本中代替 JavaScript 使用的语言。芯片设计者从与本节描述的评估器相似的寄存器机器的数据路径和控制器规格开始,并使用设计自动化程序来构建集成电路布局。 [^(20)](#c5-fn-0020) ![c5-fig-0016.jpg](img/c5-fig-0016.jpg) -[图 5.16](#c5-fig-0016a) 一种方案评估器的硅片实现。 +图 5.16:一种方案评估器的硅片实现。 ##### 寄存器和操作 @@ -2974,12 +2974,12 @@ return n === 1 go_to(reg("continue")), ``` -错误分支的代码是另一个函数调用,其中函数是符号`"*"`的值,参数是`n`和另一个函数调用的结果(对`factorial`的调用)。这些调用中的每一个都建立了`fun`和`argl`以及它自己的原始和复合分支。[图 5.17](#c5-fig-0018) 显示了`factorial`函数声明的完整编译。注意,如上所示,谓词周围的`continue`和`env`的可能的`save`和`restore`实际上是生成的,因为这些寄存器被谓词中的函数调用修改,并且需要用于分支中的函数调用和`"return"`链接。 +错误分支的代码是另一个函数调用,其中函数是符号`"*"`的值,参数是`n`和另一个函数调用的结果(对`factorial`的调用)。这些调用中的每一个都建立了`fun`和`argl`以及它自己的原始和复合分支。图 5.17:显示了`factorial`函数声明的完整编译。注意,如上所示,谓词周围的`continue`和`env`的可能的`save`和`restore`实际上是生成的,因为这些寄存器被谓词中的函数调用修改,并且需要用于分支中的函数调用和`"return"`链接。 ![c5-fig-0017a.jpg](img/c5-fig-0017a.jpg) ![c5-fig-0017b.jpg](img/c5-fig-0017b.jpg) -[图 5.17](#c5-fig-0018a) 编译声明的`factorial`功能。 +图 5.17:编译声明的`factorial`功能。 ##### 练习 5.36 @@ -3014,12 +3014,12 @@ function factorial(n) { ##### 练习 5.38 -编译了什么程序来产生如图[图 5.18](#c5-fig-0021) 所示的代码? +编译了什么程序来产生如图图 5.18:所示的代码? ![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。 +图 5.18:编译器输出的一个例子。见练习 5.38。 ##### 练习 5.39 -- GitLab