提交 3fac9e3a 编写于 作者: W wizardforcel

2023-03-30 17:07:10

上级 6d55d01e
......@@ -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)的整数,其中
......
此差异已折叠。
此差异已折叠。
......@@ -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`实现。您必须实现一个函数,该函数将两个帧作为输入,检查帧中的绑定是否兼容,如果兼容,则生成一个合并两组绑定的帧。这个操作类似于统一。
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册