Skip to content

Commit

Permalink
3.6
Browse files Browse the repository at this point in the history
  • Loading branch information
wizardforcel committed Sep 12, 2016
1 parent 69381b3 commit 1b51d92
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 0 deletions.
129 changes: 129 additions & 0 deletions 3.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -901,3 +901,132 @@ Logo 的解释器可以拥有和计算器解释器相同的结构。解析器产

![](img/logo_apply.png)

最终的函数 `logo_apply`接受两种参数:基本过程和用户定义的过程,二者都是`Procedure`的实例。`Procedure`是一个 Python 对象,它拥有过程的名称、参数数量、主体和形式参数作为使用属性。`body`属性可以有各种类型。基本过程在 Python 中已经实现,所以它的`body`就是 Python 函数。用户定义的过程(非基本)定义在 Logo 中,所以它的 `body`就是 Logo 代码行的列表。`Procedure`也拥有两个布尔值属性。一个用于表明是否是基本过程,另一个用于表明是否需要访问当前环境。

```py
>>> class Procedure():
def __init__(self, name, arg_count, body, isprimitive=False,
needs_env=False, formal_params=None):
self.name = name
self.arg_count = arg_count
self.body = body
self.isprimitive = isprimitive
self.needs_env = needs_env
self.formal_params = formal_params
```

基本过程通过在参数列表上调用主体,并返回它的返回值作为过程输出来应用。

```py
>>> def logo_apply(proc, args):
"""Apply a Logo procedure to a list of arguments."""
if proc.isprimitive:
return proc.body(*args)
else:
"""Apply a user-defined procedure"""
```

用户定义过程的函主题是代码行的列表,每一行都是 Logo 句子。为了在参数列表上调用过程,我们在新的环境中求出主体的代码行。为了构造这个环境,我们向当前环境中添加新的帧,过程的形式参数在里面绑定到实参上。这个过程的重要结构化抽象是,求出用户定义过程的主体的代码行,需要递归调用`eval_line`

**求值/应用递归。**实现求值过程的函数,`eval_line ``logo_eval`,以及实现函数应用过程的函数,`apply_procedure``collect_args``logo_apply`,是互相递归的。无论何时调用表达式被发现,求值需要调用它。应用使用求值来求出实参中的操作数表达式,以及求出用户定过程的主体。这个互相递归过程的通用结构在解释器中非常常见:求值以应用定义,应用又使用求值定义。

![](img/eval_apply.png)

这个递归循环终止于语言的基本类型。求值的基本条件是,求解基本表达式、变量、引用表达式或定义。函数调用的基本条件是调用基本过程。这个互相递归的结构,在处理表达式形式的求值函数和处理函数极其参数的应用之间,构成了求值过程的本质。

## 3.6.4 环境

既然我们已经描述了 Logo 解释器的结构,我们转而实现`Environment `类,便于让它使用动态作用域正确支持赋值、过程定义和变量查找。`Environment`实例表示名称绑定的共有集合,可以在程序执行期间的某一点上访问。绑定在帧中组织,而帧以 Python 字典实现。帧包含变量的名称绑定,但不包含过程。运算符名称和`Procedure`实例之间的绑定在 Logo 中是单独储存的。在这个实现项目中,包含变量名称绑定的帧储存为字典的列表,位于`Environment``_frames`属性中,而过程名称绑定储存在值为字典的`procedures`属性中。

帧不能直接访问,而是通过两个`Environment`的方法:`lookup_variable``set_variable_value`。前者实现了一个过程,与我们在第一章的计算环境模型中引入的查找过程相同。名称在当前环境第一帧(最新添加)中与值匹配。如果它被找到,所绑定的值会被返回。如果没有找到,会在被当前帧扩展的帧中寻找。

`set_variable_value `也会寻找与变量名称匹配的绑定。如果找到了,它会更新为新的值,如果没哟刟,那么会在全局帧上创建新的绑定。这些方法的实现留做配套项目中的练习。

`lookup_variable `方法在求解变量名称时由`logo_eval`调用。`set_variable_value ``logo_make`函数调用,它用作 Logo 中`make`基本过程的主体。

除了变量和`make`基本过程之外,我们的解释器支持它的第一种抽象手段:将名称绑定到值上。在 Logo 中,我们现在可以重复第一章中的,我们的第一种抽象步骤。

```logo
? make "radius 10
? print 2 * :radius
20
```

赋值只是抽象一种有限总是。我们已经从这门课的开始看到,即使对于不是很大的程序,用户定义函数是个管理复杂性的关键工具。我们需要两个改进来实现 Logo 中的用户定义过程。首先,我们必须描述`eval_definition`的实现,如果当前行是定义,`logo_eval`会调用这个 Python 函数。其次,我们需要在`logo_apply`中完成我们的表描述,它在一些参数上调用用户过程。这两个改动都需要利用上一节定义的`Procedure`类。

定义通过创建新的`Procedure`实例来求值,它表示用户定义的过程。考虑下面的 Logo 过程定义:

```logo
? to factorial :n
> output ifelse :n = 1 [1] [:n * factorial :n - 1]
> end
? print fact 5
120
```

定义的第一行提供了过程的名称`factorial`和形参`n`。随后的一些行组成了过程体。这一行并不会立即求值,而是为将来使用而储存。也就是说,这些行由`eval_definition`读取并解析,但是并不传递给`eval_line`。主体中的行一直读取,直到出现了只包含`end`的行。在 Logo 中,`end`并不是需要求值的过程,也不是过程体的一部分。它是个函数定义末尾的语法标记。

`Procedure`实例从这个过程名称、形参列表以及主体中创建,并且在环境中的`procedures`的字典属性中注册。不像 Python,在 Logo 中,一旦过程绑定到一个名称,其它定义都不能复用这个名称。

`logo_apply``Procedure`实例应用于一些参数,它是表示为字符串的 Logo 值(对于单词),或列表(对于句子)。对于用户定义过程,`logo_apply`创建了新的帧,它是一个字典对象,键是过程的形参,值是实参。在动态作用域语言例如 Logo 中,这个新的帧总是扩展自过程调用处的当前环境。所以,我们将新创建的帧附加到当前环境上。之后,主题中的每一行都依次传递给`eval_line `。最后,在主体求值完成后,我们可以从环境中移除新创建的帧。由于 Logo 并不支持高阶或一等过程,在程序执行期间,我们并不需要一次跟踪多于一个环境。

下面的例子演示了真的列表和动态作用域规则,它们由调用这个两个 Logo 的用户定义过程产生:

```logo
? to f :x
> make "z sum :x :y
> end
? to g :x :y
> f sum :x :x
> end
? g 3 7
? print :z
13
```

从这些表达式求值中创建的环境分为过程和帧,它们维护在分离的命名空间中。帧的顺序由调用顺序决定。

![](img/scope.png)

## 3.6.5 数据即程序

在思考求值 Logo 表达式的程序时,一个类比可能很有帮助。程序含义的一个可取观点是,程序是抽象机器的描述。例如,再次思考下面的计算阶乘的过程:

```logo
? to factorial :n
> output ifelse :n = 1 [1] [:n * factorial :n - 1]
> end
```

我们可以在 Python 中表达为等价的程序,使用传统的表达式。

```py
>>> def factorial(n):
return 1 if n == 1 else n * factorial(n - 1)
```

我们可能将这个程序看做机器的描述,它包含几个部分,减法、乘法和相等性测试,并带有两相开关和另一个阶乘机器(阶乘机器是无限的,因为它在其中包含另一个阶乘机器)。下面的图示是一个阶乘机器的流程图,展示了这些部分是怎么组合到一起的。

![](img/factorial_machine.png)

与之相似,我们可以将 Logo 解释器看做非常特殊的机器,它接受机器的描述作为输入。给定这个输入,解释器就能配置自己来模拟做描述的机器。例如,如果我们像解释器中输入阶乘的定义,解释器就可以计算阶乘。

![](img/universal_machine.png)

从这个观点得出,我们的 Logo 解释器可以看做通用的机器。当输入以 Logo 程序描述时,它模拟了其它机器。它在由我们的编程语言操作的数据对象,和编程语言自身之间起到衔接作用。想象一下,一个用户在我们正在运行的 Logo 表达式中输入了 Logo 表达式。从用户的角度来看,类似`sum 2 2`的输入表达式是编程语言中的表达式,解释器应该对其求值。但是,从解释器的角度来看,表达式只是单词组成的句子,可以根据定义好的一系列规则来操作它。

用户的程序是解释器的数据,这不应该是混乱的原因。实际上,有时候忽略这个差异会更方便,以及让用户能够显式将数据对象求值为表达式。在 Logo 中,无论我们何时使用`run `过程,我们都使用了这种能力。Python 中也存在相似的函数:`eval`函数会求出 Python 表达式,`exec`函数会求出 Python 语句,所以:

```py
>>> eval('2+2')
4
```


```py
>>> 2+2
4
```

返回了相同的结果。求解构造为指令的一部分的表达式是动态编程语言的常见和强大的特性。这个特性在 Logo 中十分普遍,很少语言是这样,但是在程序执行期间构造和求解表达式的能力,对任何程序员来说都是有价值的工具。
Binary file added img/eval_apply.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/factorial_machine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/scope.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/universal_machine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 1b51d92

Please sign in to comment.