提交 816d9790 编写于 作者: W wizardforcel

2023-03-31 11:29:14

上级 2f83c6cb
此差异已折叠。
......@@ -212,7 +212,7 @@ function denom(x) {
##### 练习 2.2
考虑在平面中表示线段的问题。每个线段被表示为一对点:起点和终点。声明一个构造函数`make_segment`和选择器`start_segment``end_segment`,它们根据点来定义段的表示。此外,一个点可以表示为一对数字:x 坐标和`y`坐标。相应地,指定一个构造函数`make_point`和定义这个表示的选择器`x_point``y_point`。最后,使用您的选择器和构造函数,声明一个函数`midpoint_segment`,它将一条线段作为参数,并返回它的中点(其坐标是端点坐标的平均值的点)。要尝试您的函数,您需要一种打印点的方法:
考虑在平面中表示线段的问题。每个线段被表示为一对点:起点和终点。声明一个构造函数`make_segment`和选择器`start_segment``end_segment`,它们根据点来定义段的表示。此外,一个点可以表示为一对数字:`x`坐标和`y`坐标。相应地,指定一个构造函数`make_point`和定义这个表示的选择器`x_point``y_point`。最后,使用您的选择器和构造函数,声明一个函数`midpoint_segment`,它将一条线段作为参数,并返回它的中点(其坐标是端点坐标的平均值的点)。要尝试您的函数,您需要一种打印点的方法:
```js
function print_point(p) {
......@@ -1697,7 +1697,7 @@ const up_split = split(below, beside);
图 2.15:一个帧由三个向量描述——一个原点和两条边。
我们将使用单位正方形中的坐标(0 ≤ x , y ≤ 1)来指定图像。对于每一帧,我们关联一个帧坐标图,它将用于移动和缩放图像以适应帧。通过将矢量 **v** = ( x , y )映射到矢量和,地图将单位正方形转换为框架
我们将使用单位正方形中的坐标(0 ≤`x`, y ≤ 1)来指定图像。对于每一帧,我们关联一个帧坐标图,它将用于移动和缩放图像以适应帧。通过将矢量 **v** = (`x`, y )映射到矢量和,地图将单位正方形转换为框架
```js
Origin(Frame) + x · Edge1 (Frame) + y · Edge2 (Frame)
......@@ -1733,7 +1733,7 @@ origin_frame(a_frame);
| (x1,y1)+(x2,y2) | = | (x[1]+x[2],y[1]+y[2]) |
| (x1,y1)–(x2,y2) | = | (x[1]–x[2],y[1]–y[2]) |
| s ( x , y ) | = | ( sx , sy ) |
| s (`x`, y ) | = | ( sx , sy ) |
##### 练习 2.47
......@@ -2002,7 +2002,7 @@ JavaScript 解释器读取双引号`"`后的字符,直到找到另一个双引
### 2.3.2 示例:符号微分
作为符号操作的说明和数据抽象的进一步说明,考虑执行代数表达式的符号微分的函数的设计。我们希望函数将一个代数表达式和一个变量作为自变量,并返回表达式对变量的导数。例如,如果函数的参数是 ax²+bx+c 和 x ,那么函数应该返回 2 ax + b 。符号微分在编程语言 Lisp 中具有特殊的历史意义。这是开发一种用于符号操作的计算机语言背后的激励例子之一。此外,它标志着导致符号数学工作的强大系统开发的研究路线的开始,这些系统今天被应用数学家和物理学家例行使用。
作为符号操作的说明和数据抽象的进一步说明,考虑执行代数表达式的符号微分的函数的设计。我们希望函数将一个代数表达式和一个变量作为自变量,并返回表达式对变量的导数。例如,如果函数的参数是 ax²+bx+`c``x`,那么函数应该返回 2 ax +`b`。符号微分在编程语言 Lisp 中具有特殊的历史意义。这是开发一种用于符号操作的计算机语言背后的激励例子之一。此外,它标志着导致符号数学工作的强大系统开发的研究路线的开始,这些系统今天被应用数学家和物理学家例行使用。
在开发符号微分程序时,我们将遵循我们在开发 2.1.1 节的有理数系统时所遵循的相同的数据抽象策略。也就是说,我们将首先定义一个微分算法,该算法对诸如“和”、“积”和“变量”之类的抽象对象进行运算,而不用担心如何表示这些对象。只有在此之后,我们才会解决表示问题。
......@@ -2129,7 +2129,7 @@ list("+", list("*", list("*", "x", "y"), list("+", 1, 0)),
![c2-fig-5009.jpg](img/c2-fig-5009.jpg)
但是我们希望程序知道 x 0 = 0,1 y = y ,0 + y = y 。第二个例子的答案应该是简单的`y`。如第三个例子所示,当表达式很复杂时,这就成了一个严重的问题。
但是我们希望程序知道 x 0 = 0,1 y =`y`,0 + y =`y`。第二个例子的答案应该是简单的`y`。如第三个例子所示,当表达式很复杂时,这就成了一个严重的问题。
我们的困难很像我们在有理数实现中遇到的困难:我们没有将答案简化为最简单的形式。为了减少有理数,我们只需要改变实现的构造函数和选择器。我们可以在这里采用类似的策略。我们根本不会改变`deriv`。相反,我们将更改`make_sum`,以便如果两个被加数都是数字,`make_sum`将把它们相加并返回它们的和。同样,如果其中一个被加数为 0,那么`make_sum`将返回另一个被加数。
......@@ -2746,7 +2746,7 @@ Sha boom
我们将开发一个对复数执行算术运算的系统,作为一个使用一般运算的程序的简单但不现实的例子。我们首先讨论作为有序对的复数的两种貌似合理的表示:矩形形式(实部和虚部)和极坐标形式(幅度和角度)。第 2.4.2 节将展示如何通过使用类型标签和泛型操作使两种表示在一个系统中共存。
像有理数一样,复数自然表示为有序对。复数的集合可以被认为是具有两个正交轴的二维空间,即“实”轴和“虚”轴。(参见图 2.20: 。)从这个角度来看,复数 z=x+iy(其中 I²=–1)可以认为是平面中实坐标为 x ,虚坐标为`y`的点。在这种表示中,复数的相加简化为坐标的相加:
像有理数一样,复数自然表示为有序对。复数的集合可以被认为是具有两个正交轴的二维空间,即“实”轴和“虚”轴。(参见图 2.20: 。)从这个角度来看,复数 z=x+iy(其中 I²=–1)可以认为是平面中实坐标为`x`,虚坐标为`y`的点。在这种表示中,复数的相加简化为坐标的相加:
| 实部(z1+z2) | = | 实部(z1)+实部(z2) |
| 虚部(z1+z2) | = | 虚部(z1)+虚部(z2) |
......@@ -2802,9 +2802,9 @@ function div_complex(z1, z2) {
为了使不同的选择具体化,想象有两个程序员,Ben Bitdiddle 和 Alyssa P. Hacker,他们独立地设计复数系统的表示。Ben 选择用矩形来表示复数。有了这个选择,选择一个复数的实部和虚部就简单了,就像用给定的实部和虚部构造一个复数一样。为了找到幅度和角度,或者用给定的幅度和角度构造一个复数,他使用了三角关系
| x = r cos A | r = ![c2-fig-5011.jpg](img/c2-fig-5011.jpg) |
| y=rsinA | A = arctan( y , x ) |
| y=rsinA | A = arctan(`y`, x ) |
其将实部和虚部( x 、 y )与幅度和角度( r 、 A )相关联。因此,本的表示由以下选择器和构造器给出:
其将实部和虚部(`x`、 y )与幅度和角度( r 、 A )相关联。因此,本的表示由以下选择器和构造器给出:
```js
function real_part(z) { return head(z); }
......@@ -3701,7 +3701,7 @@ function mul_term_by_all_terms(t1, L) {
(y + 1)x2 + (y2 + 1)x + (y 1) · (y 2)x + (y3 + 7)
```
原因是当系统试图合并系数时,会通过`add``mul`进行调度。由于系数本身是多项式(在 y ,这些将使用`add_poly``mul_poly`组合。结果是一种“数据导向递归”,例如,调用`mul_poly`将导致递归调用`mul_poly`,以便乘以系数。如果系数的系数本身是多项式(可能用于表示三个变量的多项式),数据方向将确保系统将遵循另一层的递归调用,依此类推,通过数据结构规定的尽可能多的层。 [^(54)](#c2-fn-0054)
原因是当系统试图合并系数时,会通过`add``mul`进行调度。由于系数本身是多项式(在`y`,这些将使用`add_poly``mul_poly`组合。结果是一种“数据导向递归”,例如,调用`mul_poly`将导致递归调用`mul_poly`,以便乘以系数。如果系数的系数本身是多项式(可能用于表示三个变量的多项式),数据方向将确保系统将遵循另一层的递归调用,依此类推,通过数据结构规定的尽可能多的层。 [^(54)](#c2-fn-0054)
##### 表示术语列表
......@@ -3968,7 +3968,7 @@ David Turner 向我们展示了这种嵌套映射的方法,他的语言 KRC
我们在这里将一对表示为两个元素的列表,而不是普通的一对。这样,“对”( i , j )就表示为`list(i, j)`,而不是`pair(i, j)`
[18](#c2-fn-0018a) 集合 S–x 是 S 的所有元素的集合,不包括 x
[18](#c2-fn-0018a) 集合 S–`x`是 S 的所有元素的集合,不包括`x`
[19](#c2-fn-0019a)JavaScript 程序中的字符序列 // 用于引入注释。解释器会忽略从 // 到行尾的所有内容。在这本书里,我们没有使用很多注释;我们试图通过使用描述性的名称来使我们的程序自文档化。
......@@ -4031,7 +4031,7 @@ identity,flip _ vert);
在实际的计算系统中,由于矩形和极坐标形式之间的转换存在舍入误差,所以大多数时候矩形形式比极坐标形式更好。这就是为什么复数的例子是不现实的。尽管如此,它为使用一般操作的系统设计提供了一个清晰的说明,并为本章后面要开发的更具体的系统提供了一个很好的介绍。
[40](#c2-fn-0040a) 这里提到的反正切函数,由 JavaScript 的`math_atan2`函数计算,定义为带两个参数`y` x ,返回正切为 y /`x`的角度。自变量的符号决定角度的象限。
[40](#c2-fn-0040a) 这里提到的反正切函数,由 JavaScript 的`math_atan2`函数计算,定义为带两个参数`y``x`,返回正切为 y /`x`的角度。自变量的符号决定角度的象限。
名称`undefined`在任何 JavaScript 实现中都是预先声明的,除了引用原始值之外不应该使用。
......@@ -4070,7 +4070,7 @@ apply _ in _ underlying _ JavaScript(sum _ of _ squares,list(1,3))
虽然我们假设术语表是有序的,但是我们已经实现了`adjoin_term`来简单地将新术语连接到现有术语表的前面。只要我们保证使用`adjoin_term`的函数(比如`add_terms`)总是用比列表中出现的更高阶的术语调用它,我们就可以摆脱这种情况。如果我们不想做这样的保证,我们可以实现`adjoin_term`类似于集合的有序列表表示的`adjoin_set`构造函数(练习 2.61)。
欧几里德算法适用于多项式的事实在代数中是形式化的,即多项式形成一种称为欧几里德环的代数域。欧几里德环是一个允许加法、减法和交换乘法的域,以及一种给环的每个元素`x`分配一个正整数“measure”m(x)的方式,该正整数的性质是 m(xy)m(x)对于任何非零的`x``y`给定任意一个`x` y ,存在一个 q 使得 y=qx+r 和 r = 0 或 m(r)<m( 从抽象的角度来看,这是证明欧几里德算法行得通所需要的。对于整数的定义域,一个整数的测度 m 就是该整数本身的绝对值。对于多项式的定义域,多项式的度量是它的次数。
欧几里德算法适用于多项式的事实在代数中是形式化的,即多项式形成一种称为欧几里德环的代数域。欧几里德环是一个允许加法、减法和交换乘法的域,以及一种给环的每个元素`x`分配一个正整数“measure”m(x)的方式,该正整数的性质是 m(xy)m(x)对于任何非零的`x``y`给定任意一个`x``y`,存在一个 q 使得 y=qx+r 和 r = 0 或 m(r)<m( 从抽象的角度来看,这是证明欧几里德算法行得通所需要的。对于整数的定义域,一个整数的测度`e`就是该整数本身的绝对值。对于多项式的定义域,多项式的度量是它的次数。
在 JavaScript 中,整数的除法可以产生有限精度的小数,因此我们可能无法得到有效的除数。
......
......@@ -331,7 +331,7 @@ function random_gcd_test(trials, initial_x) {
##### 练习 3.5
蒙特卡洛积分是一种通过蒙特卡洛模拟的方式估算定积分的方法。考虑计算由谓词 P ( x , y )描述的空间区域的面积,对于该区域中的点( x , y )为真,对于不在该区域中的点为假。例如,包含在以(5,7)为中心的半径为 3 的圆内的区域由测试是否(x–5)²+(y–7)²3²的谓词来描述。要估计由这种谓词描述的区域的面积,首先要选择一个包含该区域的矩形。例如,对角位于(2,4)和(8,10)的矩形包含上面的圆。期望的积分是位于该区域中的矩形部分的面积。我们可以通过随机选取位于矩形中的点( x , y )来估计积分,并对每个点测试 P ( x , y )以确定该点是否位于该区域中。如果我们用许多点来尝试,那么落在该区域中的点的分数应该给出位于该区域中的矩形的比例的估计。因此,用这个分数乘以整个矩形的面积应该可以得到积分的估计值。
蒙特卡洛积分是一种通过蒙特卡洛模拟的方式估算定积分的方法。考虑计算由谓词 P (`x`, y )描述的空间区域的面积,对于该区域中的点(`x`, y )为真,对于不在该区域中的点为假。例如,包含在以(5,7)为中心的半径为 3 的圆内的区域由测试是否(x–5)²+(y–7)²3²的谓词来描述。要估计由这种谓词描述的区域的面积,首先要选择一个包含该区域的矩形。例如,对角位于(2,4)和(8,10)的矩形包含上面的圆。期望的积分是位于该区域中的矩形部分的面积。我们可以通过随机选取位于矩形中的点(`x`, y )来估计积分,并对每个点测试 P (`x`, y )以确定该点是否位于该区域中。如果我们用许多点来尝试,那么落在该区域中的点的分数应该给出位于该区域中的矩形的比例的估计。因此,用这个分数乘以整个矩形的面积应该可以得到积分的估计值。
将蒙特卡罗积分实现为一个函数 estimate_integral,该函数将谓词 P、矩形的上界和下界`x1``x2``y1``y2`以及为产生估计值要执行的试验次数作为参数。你的函数应该使用上面用来估算 π 的同一个`monte_carlo`函数。使用您的`estimate_integral`通过测量单位圆的面积来估算 π 。
......@@ -568,7 +568,7 @@ const paul_acc = make_joint(peter_acc, "open sesame", "rosebud");
环境是一系列帧。每一帧都是一个绑定的表格(可能是空的),它将名称与其对应的值相关联。(对于任何名称,单个框架最多可以包含一个绑定。)每个框架也有一个指向它的封闭环境的指针,除非为了讨论的目的,框架被认为是全局的。相对于环境的名称的值是由包含该名称的绑定的环境中的第一帧中的名称的绑定给出的值。如果序列中没有帧为该名称指定绑定,那么该名称在环境中被称为未绑定。
图 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 的绑定。
图 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)
......@@ -2118,7 +2118,7 @@ dAE = FL
这样的等式不是单向的。给定任何四个量,我们可以用它来计算第五个量。然而,将这个等式翻译成传统的计算机语言,将迫使我们从其他四个量中选择一个来计算。因此,用于计算面积`A`的函数不能用于计算挠度 d ,即使`A`和 d 的计算源自同一等式。 [^(34)](#c3-fn-0034)
在这一节中,我们概述了一种语言的设计,这种语言使我们能够根据关系本身来工作。该语言的基本元素是基本约束,它陈述了数量之间的特定关系。例如,`adder(a, b, c)`指定量 a 、 b 、`c`必须由等式 a+b=c,`multiplier(x, y, z)`表示约束 xy = z `constant(3.14, x)`表示`x`的值必须为 3.14。
在这一节中,我们概述了一种语言的设计,这种语言使我们能够根据关系本身来工作。该语言的基本元素是基本约束,它陈述了数量之间的特定关系。例如,`adder(a, b, c)`指定量`a``b``c`必须由等式 a+b=c,`multiplier(x, y, z)`表示约束 xy =`z``constant(3.14, x)`表示`x`的值必须为 3.14。
我们的语言提供了一种组合原始约束的方法,以便表达更复杂的关系。我们通过构建约束网络来组合约束,其中约束由连接器连接。连接器是“持有”一个值的对象,该值可以参与一个或多个约束。例如,我们知道华氏温度和摄氏温度之间的关系是
......@@ -2638,13 +2638,13 @@ balance = balance - amount;
### 3.4.2 控制并发的机制
我们已经看到,处理并发线程的困难在于需要考虑不同线程中事件顺序的交错。例如,假设我们有两个线程,一个有三个有序事件( a 、 b 、 c ),另一个有三个有序事件( x 、 y 、 z )。如果两个线程并发运行,并且对它们如何交错执行没有限制,那么对于与两个线程的单独排序一致的事件,有 20 种不同的可能排序:
我们已经看到,处理并发线程的困难在于需要考虑不同线程中事件顺序的交错。例如,假设我们有两个线程,一个有三个有序事件(`a``b`、 c ),另一个有三个有序事件(`x``y`、 z )。如果两个线程并发运行,并且对它们如何交错执行没有限制,那么对于与两个线程的单独排序一致的事件,有 20 种不同的可能排序:
| ( a 、 b 、 c 、 x 、 y 、 z ) | ( a 、 x 、 b 、 y 、 c 、 z ) | ( x , a , b , c , y , z ) | ( x , a , y , z , b , c ) |
| ( a , b , x , c , y , z ) | ( a , x , b , y , z , c ) | ( x , a , b , y , c , z ) | ( x , y , a , b , c , z ) |
| ( a 、 b 、 x 、 y 、 c 、 z ) | ( a 、 x 、 y 、 b 、 c 、 z ) | ( x , a , b , y , z , c ) | ( x , y , a , b , z , c ) |
| ( a 、 b 、 x 、 y 、 z 、 c ) | ( a 、 x 、 y 、 b 、 z 、 c ) | ( x , a , y , b , c , z ) | ( x , y , a , z , b , c ) |
| ( a , x , b , c , y , z ) | ( a , x , y , z , b , c ) | ( x , a , y , b , z , c ) | ( x , y , z , a , b , c ) |
| (`a``b``c``x``y`、 z ) | (`a``x``b``y``c`、 z ) | (`x``a``b``c``y`, z ) | (`x``a``y``z``b`, c ) |
| (`a``b``x``c``y`, z ) | (`a``x``b``y``z`, c ) | (`x``a``b``y``c`, z ) | (`x``y``a``b``c`, z ) |
| (`a``b``x``y``c`、 z ) | (`a``x``y``b``c`、 z ) | (`x``a``b``y``z`, c ) | (`x``y``a``b``z`, c ) |
| (`a``b``x``y``z`、 c ) | (`a``x``y``b``z`、 c ) | (`x``a``y``b``c`, z ) | (`x``y``a``z``b`, c ) |
| (`a``x``b``c``y`, z ) | (`a``x``y``z``b`, c ) | (`x``a``y``b``z`, c ) | (`x``y``z``a``b`, c ) |
作为设计这个系统的程序员,我们必须考虑这 20 个排序中每一个的影响,并检查每个行为是否可以接受。随着线程和事件数量的增加,这种方法很快变得不实用。
......@@ -4076,7 +4076,7 @@ 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:所示。编写一个函数`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)
......
......@@ -25,7 +25,7 @@ function gcd(a, b) {
}
```
执行这种算法的机器必须记录两个数字,`a` b ,所以让我们假设这些数字存储在两个具有这些名字的寄存器中。所需的基本操作是测试寄存器`b`的内容是否为零,并计算寄存器`a`的内容除以寄存器`b`的内容的余数。余数运算是一个复杂的过程,但是现在假设我们有一个计算余数的原始设备。在 GCD 算法的每个周期,寄存器`a`的内容必须被寄存器`b`的内容替换,而`b`的内容必须被`a`的旧内容除以`b`的旧内容的余数替换。如果这些替换可以同时进行,这将是很方便的,但是在我们的寄存器机器模型中,我们将假设在每个步骤中只有一个寄存器可以被分配一个新值。为了完成替换,我们的机器将使用第三个“临时”寄存器,我们称之为`t`。(首先将余数放入`t`,然后将`b`的内容放入`a`,最后将`t`中存储的余数放入`b`。)
执行这种算法的机器必须记录两个数字,`a``b`,所以让我们假设这些数字存储在两个具有这些名字的寄存器中。所需的基本操作是测试寄存器`b`的内容是否为零,并计算寄存器`a`的内容除以寄存器`b`的内容的余数。余数运算是一个复杂的过程,但是现在假设我们有一个计算余数的原始设备。在 GCD 算法的每个周期,寄存器`a`的内容必须被寄存器`b`的内容替换,而`b`的内容必须被`a`的旧内容除以`b`的旧内容的余数替换。如果这些替换可以同时进行,这将是很方便的,但是在我们的寄存器机器模型中,我们将假设在每个步骤中只有一个寄存器可以被分配一个新值。为了完成替换,我们的机器将使用第三个“临时”寄存器,我们称之为`t`。(首先将余数放入`t`,然后将`b`的内容放入`a`,最后将`t`中存储的余数放入`b`。)
我们可以使用图 5.1:中所示的数据路径图来说明该机器所需的寄存器和操作。在该图中,寄存器(`a``b``t`)由矩形表示。向寄存器赋值的每种方式都由一个箭头表示,箭头后面有一个按钮(绘制为),从数据源指向寄存器。按下按钮时,该按钮允许源端的值“流入”指定的寄存器。每个按钮旁边的标签是我们用来指代该按钮的名称。这些名称是任意的,可以选择具有助记值的名称(例如,`a<-b`表示按下将寄存器`b`的内容分配给寄存器`a`的按钮)。一个寄存器的数据来源可以是另一个寄存器(如在`a<-b`赋值中),一个运算结果(如在`t<-r`赋值中),或一个常数(一个不能改变的内置值,在数据路径图中用包含常数的三角形表示)。
......@@ -2013,7 +2013,7 @@ function fib(n) {
```
1. a. 对于 n ≥ 2,计算 Fib( n )所需的最大堆栈深度,用`n`给出一个公式。提示:在第 1.2.2 节中,我们认为这个进程使用的空间随着`n`线性增长。
2. b. 给出一个公式,用于计算 n ≥ 2 的 Fib( n )的总推送次数。您应该会发现推送的次数(与所用的时间密切相关)随着`n`成指数增长。提示:设 S ( n )为计算 Fib 时使用的推送次数( n )。你应该可以论证,有一个公式是用 S(n–1)S(n–2),和一些独立于`n`的固定“开销”常数`k`来表示 S ( n )。给出公式,并说出什么是 k 。然后证明 S ( n )可以表示为 aFib(n+1)+b 并给出`a``b`的值。
2. b. 给出一个公式,用于计算 n ≥ 2 的 Fib( n )的总推送次数。您应该会发现推送的次数(与所用的时间密切相关)随着`n`成指数增长。提示:设 S ( n )为计算 Fib 时使用的推送次数( n )。你应该可以论证,有一个公式是用 S(n–1)S(n–2),和一些独立于`n`的固定“开销”常数`k`来表示 S ( n )。给出公式,并说出什么是 k 。然后证明 S ( n )可以表示为 aFib(n+1)+`b`并给出`a``b`的值。
##### 练习 5.31
......@@ -3527,7 +3527,7 @@ display(string+" "+stringify(prepare(object)));
通过扩展编译器,允许编译后的代码调用解释后的函数,我们可以做得更好。见练习 5.50。
独立于执行策略,如果我们坚持在用户程序执行中遇到的错误应该被检测并发出信号,而不是被允许杀死系统或产生错误的答案,我们会招致巨大的开销。例如,越界数组引用可以通过在执行之前检查引用的有效性来检测。然而,检查的开销可能是数组引用本身成本的许多倍,程序员应该在速度和安全之间权衡,以确定是否需要这样的检查。一个好的编译器应该能够产生带有这种检查的代码,应该避免冗余检查,并且应该允许程序员控制编译代码中错误检查的程度和类型。
流行语言的编译器,如 C 和 C++,在运行的代码中几乎不加任何错误检查操作,以使程序尽可能快地运行。因此,明确提供错误检查就落到了程序员的肩上。不幸的是,人们经常忽略这一点,即使在速度不是限制的关键应用中也是如此。他们的项目过着快速而危险的生活。例如,1988 年使互联网瘫痪的臭名昭著的“蠕虫”利用了 UNIX ^(TM) 操作系统检查手指守护进程中的输入缓冲区是否溢出的失败。(见斯帕福德 1989 年。)
流行语言的编译器,如 C 和 C++,在运行的代码中几乎不加任何错误检查操作,以使程序尽可能快地运行。因此,明确提供错误检查就落到了程序员的肩上。不幸的是,人们经常忽略这一点,即使在速度不是限制的关键应用中也是如此。他们的项目过着快速而危险的生活。例如,1988 年使互联网瘫痪的臭名昭著的“蠕虫”利用了 UNIX 操作系统检查手指守护进程中的输入缓冲区是否溢出的失败。(见斯帕福德 1989 年。)
当然,无论是解释策略还是编译策略,我们都必须为新机器实现存储分配、输入和输出,以及我们在讨论求值器和编译器时视为“原语”的所有各种操作。这里最小化工作的一个策略是用 JavaScript 编写尽可能多的这些操作,然后为新机器编译它们。最终,一切都简化为一个为新机器手工编码的小内核(比如垃圾收集和应用实际机器原语的机制)。
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册