CS61A Note

 

Python

1.2.6 非纯函数 print

纯函数(Pure functions):函数有一些输入(参数)并返回一些输出(调用返回结果)。

>>> abs(-2)
2

可以将内置函数 abs 描述为接受输入并产生输出的小型机器。

abs 就是纯函数,纯函数在调用时除了返回值外不会造成其他任何影响,而且在使用相同的参数调用纯函数时总是会返回相同的值。

非纯函数(Non-pure functions):除了返回值外,调用一个非纯函数还会产生其他改变解释器和计算机的状态的副作用(side effect)。一个常见的副作用就是使用 print 函数产生(非返回值的)额外输出。

>>> print(1, 2, 3)
1 2 3

虽然 printabs 在这些例子中看起来很相似,但它们的工作方式基本不同。print 返回的值始终为 None,这是一个不代表任何内容的特殊 Python 值。而交互式 Python 解释器并不会自动打印 None 值,所以 print 函数的额外输出就是它的副作用。

下面这个调用 print 的嵌套表达式就展示了非纯函数的特征。

>>> print(print(1), print(2))
1
2
None None

小心使用 print 函数!它返回 None 意味着它不应该用于赋值语句。

>>> two = print(2)
2
>>> print(two)
None

1.3.1 环境

求解表达式的环境由 序列组成,它们可以被描述为一些盒子。每个帧都包含了一些 绑定,它们将名称与对应的值相关联。全局 帧(global frame)只有一个。赋值和导入语句会将条目添加到当前环境的第一帧。目前,我们的环境仅由全局帧组成。

函数也会出现在环境图中。import 语句将名称与内置函数绑定。def 语句将名称与用户自定义的函数绑定。导入 mul 并定义 square 后的结果环境如下所示:

每个函数都是一行,以 func 开头,后面是函数名称和形式参数。mul 等内置函数没有正式的参数名称,所以都是使用 ... 代替。

函数名称重复两次,一次在环境帧中,另一次是作为函数定义的一部分。函数定义中出现的名称叫做 内在名称(intrinsic name),帧中的名称叫做 绑定名称(bound name)。两者之间有一个区别:不同的名称可能指的是同一个函数,但该函数本身只有一个内在名称。

1.3.2 调用用户定义的函数

为了求出操作符为用户定义函数的调用表达式,Python 解释器遵循了以下计算过程。与其他任何调用表达式一样,解释器将对操作符和操作数表达式求值,然后用生成的实参调用具名函数。

调用用户定义的函数会引入局部帧(local frame),它只能访问该函数。通过一些实参调用用户定义的函数:

  1. 在新的局部帧中,将实参绑定到函数的形参上。
  2. 在以此帧开始的环境中执行函数体。

求值函数体的环境由两个帧组成:一是包含形式参数绑定的局部帧,然后是包含其他所有内容的全局帧。函数的每个实例都有自己独立的局部帧。

为了详细说明一个例子,下面将会描述相同示例的环境图中的几个步骤。执行第一个 import 语句后,只有名称 mul 被绑定在全局帧中。

首先,执行定义函数 square 的语句。请注意,整个 def 语句是在一个步骤中处理的。直到函数被调用(而不是定义时),函数体才会被执行。

接下来,使用参数 -2 调用 square 函数,它会创建一个新的帧,将形式参数 x 与 -2 绑定。

然后在当前环境中查找名称 x,它由所示的两个帧组成,而在这两种情况下,x 的结果都是 -2,因此此 square 函数返回 4。

square() 帧中的“返回值”不是名称绑定的值,而是调用创建帧的函数返回的值。

即使在这个简单的示例中,也使用了两个不同的环境。顶级表达式 square(-2) 在全局环境中求值,而返回表达式 mul(x, x) 在调用 square 时创建的环境中求值。虽然 xmul 都在这个环境中,但在不同的帧中。

环境中帧的顺序会影响通过表达式查找名称而返回的值。我们之前说过,名称会求解为当前环境中与该名称关联的值。我们现在可以更准确地说:

名称求解(Name Evaluation):在环境中寻找该名称,最早找到的含有该名称的帧,其里边绑定的值就是这个名称的计算结果。

1.3.3 示例:调用用户定义的函数

让我们再次思考两个简单的函数定义,并说明计算用户定义函数的调用表达式的过程。

Python 首先求解名称 sum_squares ,并将它绑定到全局帧中的用户定义的函数,而原始数值表达式 5 和 12 的计算结果为它们所代表的数字。

接下来 Python 会调用 sum_squares ,它引入了一个局部帧,将 x 绑定到 5,将 y 绑定到 12。

sum_squares 的主体包含此调用表达式:

  add     (  square(x)  ,  square(y)  )
________     _________     _________
operator     operand 0     operand 1

所有三个子表达式都在当前环境中计算,且始于标记为 sum_squares() 的帧。运算符子表达式 add 是在全局帧中找到的名称,它绑定到了内置的加法函数上。在调用加法之前,必须依次求解两个操作数子表达式,两个操作数都在标记为 sum_squares 的帧的环境中计算。

operand 0 中,square 在全局帧中命名了一个用户定义的函数,而 x 在局部帧中命名了数字 5。Python 通过引入另一个将将 x 与 5 绑定的局部帧来调用 square 函数。

此环境下表达式 mul(x, x) 的计算结果为 25。

继续求解 operand 1,其中 y 的值为 12。Python 会再次对 square 的函数体进行求解,此时引入另一个将 x 与 12 绑定的局部帧,计算结果为 144。

最后,对参数 25 和 144 调用加法得到 sum_squares 的最终返回值:169。

这个例子展示了我们到目前为止学到的许多基本思想。将名称绑定到值,而这些值会分布在多个无关的局部帧,以及包含共享名称的单个全局帧中。每次调用函数时都会引入一个新的局部帧,即使是同一个函数被调用两次。

所有这些机制的存在都是为了确保名称在程序执行期间的正确时间解析为正确的值。这个例子说明了为什么我们之前介绍了“模型需要复杂性”。所有三个局部帧都包含名称 x 的绑定,但该名称会与不同帧中的不同值进行绑定,局部帧会将这些名称分开。

1.5.6 测试

断言(Assertions):程序员使用 assert 语句来验证是否符合预期,例如验证被测试函数的输出。assert 语句在布尔上下文中有一个表达式,后面是一个带引号的文本行(单引号或双引号都可以,但要保持一致),如果表达式的计算结果为假值,则显示该行。

>>> assert fib(8) == 13, '第八个斐波那契数应该是 13'

当被断言的表达式的计算结果为真值时,执行断言语句无效。而当它是假值时,assert 会导致错误,使程序停止执行。

当在文件中而不是直接在解释器中编写 Python 时,测试通常是在同一个文件或带有后缀 _test.py 的相邻文件中编写的。

文档测试(Doctests):Python 提供了一种方便的方法,可以将简单的测试直接放在函数的文档字符串中。文档字符串的第一行应该包含函数的单行描述,接着是一个空行,下面可能是参数和函数意图的详细描述。此外,文档字符串可能包含调用该函数的交互式会话示例:

>>> def sum_naturals(n):
        """返回前 n 个自然数的和。

        >>> sum_naturals(10)
        55
        >>> sum_naturals(100)
        5050
        """
        total, k = 0, 1
        while k <= n:
            total, k = total + k, k + 1
        return total

然后,可以通过 doctest 模块 来验证交互,如下。

>>> from doctest import testmod
>>> testmod()
TestResults(failed=0, attempted=2)

如果仅想验证单个函数的 doctest 交互,我们可以使用名为 run_docstring_examplesdoctest 函数。不幸的是,这个函数调用起来有点复杂。第一个参数是要测试的函数;第二个参数应该始终是表达式 globals() 的结果,这是一个用于返回全局环境的内置函数;第三个参数 True 表示我们想要“详细”输出:所有测试运行的目录。

>>> from doctest import run_docstring_examples
>>> run_docstring_examples(sum_naturals, globals(), True)
Finding tests in NoName
Trying:
    sum_naturals(10)
Expecting:
    55
ok
Trying:
    sum_naturals(100)
Expecting:
    5050
ok

当函数的返回值与预期结果不匹配时,run_docstring_examples 函数会将此问题报告为测试失败。

当你在文件中编写 Python 时,可以通过使用 doctest 命令行选项启动 Python 来运行文件中的所有 doctest:

python -m doctest <python_source_file>

1.6.3 定义函数 III:嵌套定义

>>> def sqrt(a):
        def sqrt_update(x):
            return average(x, a/x)
        def sqrt_close(x):
            return approx_eq(x * x, a)
        return improve(sqrt_update, sqrt_close)

与局部赋值一样,局部 def 语句只影响当前局部帧。这些函数仅在求解 sqrt 时在作用域内。与求解过程一致,这些局部 def 语句在调用 sqrt 之前都不会被求解。

词法作用域(Lexical scope):局部定义的函数也可以访问定义作用域内的名称绑定。在此示例中, sqrt_update 引用名称 a,它是其封闭函数 sqrt 的形式参数。这种在嵌套定义之间共享名称的规则称为词法作用域。

我们需要对我们的环境模型实现两个扩展来启用词法作用域。

  1. 每个用户定义的函数都有一个父环境:定义它的环境。
  2. 调用用户定义的函数时,其局部帧会继承其父环境。

在调用 sqrt 之前,所有函数都是在全局环境中定义的,因此它们都有相同的父级:全局环境。相比之下,当 Python 计算 sqrt 的前两个子句时,它会创建局部环境关联的函数。在这次调用中

>>> sqrt(256)
16.0

环境首先为 sqrt 添加一个局部帧,然后求解 sqrt_updatesqrt_close 的 def 语句。

从现在开始我们将在环境图中的每个函数都增加一个新注释 –> 父级注释(a parent annotation)。函数值的父级是定义该函数的环境的第一帧。没有父级注释的函数是在全局环境中定义的。当调用用户定义的函数时,创建的帧与该函数具有相同的父级。

随后,名称 sqrt_update 解析为这个新定义的函数,该函数作为参数传给函数 improve 。在 improve 函数的函数体中,我们必须以 guess 的初始值 x 为 1 来调用 update 函数(绑定到 sqrt_update )。这个最后的程序调用为 sqrt_update 创建了一个环境,它以一个仅包含 x 的局部帧开始,但父帧 sqrt 仍然包含 a 的绑定。

这个求值过程中最关键的部分是将 sqrt_update 的父环境变成了通过调用 sqrt_update 创建的(局部)帧。此帧还带有 [parent=f1] 的注释。

继承环境(Extended Environments):一个环境可以由任意长的帧链构成,并且总是以全局帧结束。在举 sqrt 这个例子之前,环境最多只包含两种帧:局部帧和全局帧。通过使用嵌套的 def 语句来调用在其他函数中定义的函数,我们可以创建更长的(帧)链。调用 sqrt_update 的环境由三个帧组成:局部帧 sqrt_update 、定义 sqrt_updatesqrt 帧(标记为 f1)和全局帧。

sqrt_update 函数体中的返回表达式可以通过遵循这一帧链来解析 a 的值。查找名称会找到当前环境中绑定到该名称的第一个值。Python 首先在 sqrt_update 帧中进行检查 –> 不存在 a ,然后又到 sqrt_update 的父帧 f1 中进行检查,发现 a 被绑定到了 256。

因此,我们发现了 Python 中词法作用域的两个关键优势。

  • 局部函数的名称不会影响定义它的函数的外部名称,因为局部函数的名称将绑定在定义它的当前局部环境中,而不是全局环境中。
  • 局部函数可以访问外层函数的环境,这是因为局部函数的函数体的求值环境会继承定义它的求值环境。

这里的 sqrt_update 函数自带了一些数据:a 在定义它的环境中引用的值,因为它以这种方式“封装”信息,所以局部定义的函数通常被称为闭包(closures)。

1.6.7 Lambda 表达式

一个 lambda 表达式的计算结果是一个函数,它仅有一个返回表达式作为主体。不允许使用赋值和控制语句。

>>> def compose1(f, g):
        return lambda x: f(g(x))

我们可以通过构造相应的英文句子来理解 lambda 表达式的结构:

lambda              x         :              f(g(x))
"A function that    takes x   and returns    f(g(x))"

lambda 表达式的结果称为 lambda 函数(匿名函数)。它没有固有名称(因此 Python 打印 <lambda> 作为名称),但除此之外它的行为与任何其他函数都相同。

>>> s = lambda x: x * x
>>> s
<function <lambda> at 0xf3f490>
>>> s(12)
144

在环境图中,lambda 表达式的结果也是一个函数,以希腊字母 λ(lambda)命名。我们的 compose 示例可以用 lambda 表达式非常简洁地表示出来:

1.6.9 函数装饰器

Python 提供了一种特殊的语法来使用高阶函数作为执行 def 语句的一部分,称为装饰器(decorator)。最常见的例子也许就是 trace

>>> def trace(fn):
        def wrapped(x):
            print('-> ', fn, '(', x, ')')
            return fn(x)
        return wrapped

>>> @trace
    def triple(x):
        return 3 * x

>>> triple(12)
->  <function triple at 0x102a39848> ( 12 )
36

在这个例子中,定义了一个高阶函数 trace,它返回一个函数,该函数在调用其参数前先输出一个打印语句来显示该参数。tripledef 语句有一个注解(annotation) @trace,它会影响 def 执行的规则。和往常一样,函数 triple 被创建了。但是,名称 triple 不会绑定到这个函数上。相反,这个名称会被绑定到在新定义的 triple 函数调用 trace 后返回的函数值上。代码中,这个装饰器等价于:

>>> def triple(x):
        return 3 * x
>>> triple = trace(triple)

装饰器符号 @ 也可以后跟一个调用表达式。跟在 @ 后面的表达式会先被解析(就像上面的 ‘trace’ 名称一样),然后是 def 语句,最后将装饰器表达式的运算结果应用到新定义的函数上,并将其结果绑定到 def 语句中的名称上。

2.4.4 局部状态

>>> withdraw = make_withdraw(100)

make_withdraw 的实现需要一种新的声明形式:非局部(nonlocal)声明。当我们调用 make_withdraw 的时候,我们将初始余额声明为 balance 变量,然后我们再定义并返回一个局部函数 withdraw,它会在调用时更新并返回 balance 的值。

>>> def make_withdraw(balance):
        """返回一个每次调用都会减少 balance 的 withdraw 函数"""
        def withdraw(amount):
            nonlocal balance                 # 声明 balance 是非局部的
            if amount > balance:
                return '余额不足'
            balance = balance - amount       # 重新绑定
            return balance
        return withdraw

当 balance 属性为声明为 nonlocal 后,每当它的值发生更改时,相应的变化都会同步更新到 balance 属性第一次被声明的位置。回想一下,在没有 noncal 声明之前,所有对 balance 的重新赋值操作都会在当前环境的第一帧中绑定。非局部语句指示名称不会出现在第一个(局部)帧或最后一个(全局)帧,而是出现在其他地方。

以下运行环境图展示了多次调用由 make_withdraw 创建的函数的效果。

第一个 def 声明的表现符合我们的预期:它创建一个新的自定义函数并将该函数以 make_withdraw 为名绑定到全局帧中。随后调用 make_withdraw 创建并返回一个局部定义的函数 withdraw。参数 balance 则绑定在该函数的父帧中。最重要的是,在这个示例中,变量名 balance 只有一个绑定关系。

接下来,我们调用 make_withdraw 得到函数 wd,然后调用 wd 方法并入参 5。withdraw 函数执行在一个新的环境中,并且该环境的 parent 是定义 withdraw 函数的环境。跟踪 withdraw 的执行,我们可以发现 Python 中 nonlocal 声明的效果:当前执行帧之外的变量可以通过赋值语句更改。

非局部语句(nonlocal statement)会改变 withdraw 函数定义中剩余的所有赋值语句。在将 balance 声明为 nonlocal 后,任何尝试为 balance 赋值的语句,都不会直接在当前帧中寻找并更改 balance,而是找到定义 balance 变量的帧,并在该帧中更新该变量。如果在声明 nonlocal 之前 balance 还没有赋值,则 nonlocal 声明将会报错。

通过改变 balance 的绑定,我们也改变了 withdraw 函数。下一次调用该函数时,变量 balance 的值将会是 15,而不是 20。因此,当我们第二次调用 withdraw 时,返回值将是 12,而不是 17。第一次调用对 balance 的改变会影响到第二次调用的结果。

第二次调用 withdraw 像往常一样创建了第二个局部帧。并且,这两个 withdraw 帧都具有相同的父级帧。也就是说,它们都集成了 make_withdraw 的运行环境,而变量 balance 就是在该环境中定义和声明的。因此,它们都可以访问到 balance 变量的绑定关系。调用 withdraw 会改变当前运行环境,并且影响到下一次调用 withdraw 的结果。nonlocal 声明语句允许 withdraw 更改 make_withdraw 运行帧中的变量。

自从我们第一次遇到嵌套的 def 语句,我们就发现到嵌套定义的函数可以在访问其作用域之外的变量。访问 nonlocal 声明的变量名称并不需要使用非局部语句。相比之下,只有在非局部语句之后,函数才能更改这些帧中名称的绑定。

Python 特质 (Python Particulars)。这种非局部赋值模式是具有高阶函数和词法作用域的编程语言的普遍特征。大多数其他语言根本不需要非局部语句。相反,非局部赋值通常是赋值语句的默认行为。

Python 在变量名称查找方面也有一个不常见的限制:在一个函数体内,多次出现的同一个变量名必须处于同一个运行帧内。因此,Python 无法在非局部帧中查找某个变量名对应的值,然后在局部帧中为同样名称的变量赋值,因为同名变量会在同一函数的两个不同帧中被访问。此限制允许 Python 在执行函数体之前预先计算哪个帧包含哪个名称。当代码违反了这个限制时,程序会产生令人困惑的错误消息。为了演示,请参考下面这个删掉了 nonlocal 语句的 make_withdraw 示例。

出现此 UnboundLocalError 是因为 balance 在第 5 行中被赋值,因此 Python 假定对 balance 的所有引用也必须出现在当前帧中。这个错误发生在第 5 行执行之前,这意味着 Python 在执行第 3 行之前,就以某种方式考虑了第 5 行的代码。等我们研究解释器设计的时候,我们就会看到在执行函数体之前预先计算有关函数体的实际情况是很常见的。此时,Python 的预处理限制了 balance 可能出现的帧,从而导致找不到对应的变量名。添加 nonlocal 声明可以修复这个问题。Python 2 中不存在 nonlocal 声明。

2.4.8 调度字典(Dispatch Dictionaries)

dispatch 函数是实现抽象数据消息传递接口的通用方法。为实现消息分发,到目前为止,我们使用条件语句将消息字符串与一组固定的已知消息进行比较。

内置字典数据类型提供了一种查找键值的通用方法。我们可以使用带有字符串键的字典,而不是使用条件来实现调度。

下面的 account 数据是用字典实现的。它有一个构造器 amount 和选择器 check_balance,以及存取资金的功能。此外,帐户的局部状态与实现其行为的函数一起存储在字典中。

在 amount 的方法声明中,使用字典声明了一个 dispatch 变量并将其返回,该字典包含一个帐户可能被操作的各种情况。balance 是一个数字,而消息存款 deposit 和取款 withdraw 则是两个函数。这些函数可以访问 dispatch 字典,因此它们可以读取和更改 balance。通过将 balance 存储在 dispatch 字典中而不是直接存储在帐户帧中,我们避免了在 deposit 和 withdraw 函数中使用 nonlocal 声明。

2.7.2 专用方法

在 Python 中,某些特殊名称会在特殊情况下被 Python 解释器调用。例如,类的 __init__ 方法会在对象被创建时自动调用。__str__ 方法会在打印时自动调用,__repr__ 方法会在交互式环境显示其值的时候自动调用。

Python 还允许我们定义像函数一样可以被“调用的对象”,只要在对象中包含一个 __call__ 方法。通过这个方法,我们可以定义一个行为像高阶函数的类。

举个例子,思考下面这个高阶函数,它返回一个函数,这个函数将一个常量值加到其参数上。

>>> def make_adder(n):
    	def adder(k):
            return n + k
        return adder

>>> add_three = make_adder(3)
>>> add_three(4)
7

我们可以创建一个 Adder 类,定义一个 __call__ 方法来提供相同的功能。

>>> class Adder(object):
		def __init__(self, n):
			self.n = n
		def __call__(self, k):
			return self.n + k
>>> add_three_obj = Adder(3)
>>> add_three_obj(4)
7

Adder 类表现的就像 make_adder 高阶函数,而 add_three_obj 表现得像 add_three。我们进一步模糊了数据和函数之间的界限。

2.7.3 多重表示

Python 有一种简单的计算属性的特性,可以通过零参数函数实时的计算属性。@property 修饰符允许函数在没有调用表达式语法(表达式后跟随圆括号)的情况下被调用。Complex 类存储了 realimag 属性并在需要时计算 magnitudeangle 属性。

>>> from math import atan2
>>> class ComplexRI(Complex):
		def __init__(self, real, imag):
			self.real = real
			self.imag = imag
		@property
		def magnitude(self):
			return (self.real ** 2 + self.imag ** 2) ** 0.5
		@property
		def angle(self):
			return atan2(self.imag, self.real)
		def __repr__(self):
			return 'ComplexRI({0:g}, {1:g})'.format(self.real, self.imag)

在这个实现下,所有四个属性复数算术运算所需要的属性都可以在不需要任何调用表达式的情况下被访问,并且对于 realimag 的修改会反映到 magnitudeangle 中。

>>> ri = ComplexRI(5, 12)
>>> ri.real
5
>>> ri.magnitude
13.0
>>> ri.real = 9
>>> ri.real
9
>>> ri.magnitude
15.0

Scheme

3.2.1 表达式

Scheme 的语法一直采用前缀形式。也就是说,操作符像 +* 都放在前面。函数调用可以互相嵌套,并且可能会写在多行上:

(+ (* 3 5) (- 10 6))
(+ (* 3
      (+ (* 2 4)
         (+ 3 5)))
   (+ (- 10 7)
      6))

在 Scheme 里,if 表达式是一种特殊结构。尽管从语法上看,它似乎和常规的函数调用相似,但其求值方式却与众不同。一般来说,if 表达式的结构如下:

(if <predicate> <consequent> <alternative>)

要理解 if 表达式是如何工作的,首先需要看它的 <predicate> 部分,即条件判断。如果这个条件成立(即为真),解释器就会执行并返回 <consequent> 的值;如果条件不成立,就会执行并返回 <alternative> 的值。

我们可以用常见的比较操作符来比较数字,但在 Scheme 中,这些操作符仍然采用前缀的形式:

(>= 2 1)

Scheme 中的布尔值有 #t 或者 true 代表真和 #ffalse 代表假。你可以使用一些特定的布尔操作来组合它们,这些操作的执行逻辑和 Python 里的很相似。

  • (and <e1> ... <en>) 解释器会从左到右依次检查 <e> 表达式。一旦有一个 <e> 的结果是假,整个 and 表达式就直接返回假,并且剩下的 <e> 表达式不再检查。只有当所有 <e> 都是真时,and 表达式的返回值才是最后一个表达式的结果。
  • (or <e1> ... <en>) 解释器会从左到右依次检查 <e> 表达式。一旦有一个 <e> 的结果是真,or 表达式就直接返回那个真值,并且剩下的 <e> 表达式不再检查。只有当所有 <e> 都是假时,or 表达式才返回假。
  • (not <e>)<e> 表达式的结果是假时,not 表达式就返回真,否则返回假。

3.2.2 定义

你可以通过使用 define 这一特殊结构为某个值赋予一个名字:

(define pi 3.14)
(* pi 2)

你可以用 define 的另一个版本来定义新的函数(在 Scheme 中,我们称之为“过程”)。例如,如果我们想定义一个求平方的函数,可以这样写:

(define (square x) (* x x))

定义一个过程(procedure)的标准格式如下:

(define (<name> <formal parameters>) <body>)

一旦我们定义了 square,就可以在调用表达式中使用它了:

(square 21)

(square (+ 2 5))

(square (square 3))

用户自定义的函数可以接受多个参数,并且还可以包含特殊形式:

(define (average x y)
  (/ (+ x y) 2))

(average 1 3)
(define (abs x)
    (if (< x 0)
        (- x)
        x))

Scheme 支持与 Python 相同的词法作用域规则,允许进行局部定义。下面,我们使用嵌套定义和递归定义了一个用于计算平方根的迭代过程:

(define (sqrt x)
  (define (good-enough? guess)
    (< (abs (- (square guess) x)) 0.001))
  (define (improve guess)
    (average guess (/ x guess)))
  (define (sqrt-iter guess)
    (if (good-enough? guess)
        guess
        (sqrt-iter (improve guess))))
  (sqrt-iter 1.0))
(sqrt 9)

匿名函数是通过 lambda 特殊形式创建的。Lambda 用于创建过程,与 define 相似,但不需要为过程指定名称:

(lambda (<formal-parameters>) <body>)

生成的过程与使用 define 创建的过程一样,唯一的区别是它没有在环境中关联任何名称。实际上,以下表达式是等效的:

(define (plus4 x) (+ x 4))
(define plus4 (lambda (x) (+ x 4)))

和任何返回过程的表达式一样,lambda 表达式可以在调用表达式中用作运算符:

((lambda (x y z) (+ x y (square z))) 1 2 3)

3.2.3 复合类型

在 Scheme 语言中,pair 是内置的数据结构。出于历史原因,pair 是通过内置函数 cons 创建的,而元素则可以通过 carcdr 进行访问:

(define x (cons 1 2))

x

(car x)

(cdr x)

Scheme 语言中也内置了递归列表,它们使用 pair 来构建。特殊的值 nil'() 表示空列表。递归列表的值是通过将其元素放在括号内,用空格分隔开来表示的:

(cons 1
      (cons 2
            (cons 3
                  (cons 4 nil))))

(list 1 2 3 4)

(define one-through-four (list 1 2 3 4))

(car one-through-four)

(cdr one-through-four)

(car (cdr one-through-four))

(cons 10 one-through-four)

(cons 5 one-through-four)

要确定一个列表是否为空,可以使用内置的 null? 谓词。借助它,我们可以定义用于计算长度和选择元素的标准序列操作:

(define (length items)
  (if (null? items)
      0
      (+ 1 (length (cdr items)))))
(define (getitem items n)
  (if (= n 0)
      (car items)
      (getitem (cdr items) (- n 1))))
(define squares (list 1 4 9 16 25))

(length squares)

(getitem squares 3)

3.2.4 符号数据

迄今为止,我们使用的所有复合数据对象最终都是由数字构建的。Scheme 的一个强大之处在于能够处理任意符号作为数据。

为了操作符号,我们需要语言中的一个新元素:引用数据对象的能力。假设我们想构建列表 (a b)。我们不能通过 (list a b) 来实现这一目标,因为这个表达式构建的是 ab 的值,而不是它们自身的符号。在 Scheme 中,我们通过在它们前面加上一个单引号来引用符号 ab 而不是它们的值:

(define a 1)
(define b 2)

(list a b)

(list 'a 'b)

(list 'a b)

在 Scheme 中,任何不被求值的表达式都被称为被引用。这种引用的概念源自一个经典的哲学区分,即一种事物(比如一只狗)会四处奔跑和吠叫,而“狗”这个词是一种语言构造,用来指代这种事物。当我们在引号中使用“狗”时,我们并不是指代某只特定的狗,而是指代一个词语。在语言中,引号允许我们讨论语言本身,而在 Scheme 中也是如此:

(list 'define 'list)

引用还使我们能够输入复合对象,使用传统的打印表示方式来表示列表:

(car '(a b c))

(cdr '(a b c))

完整的 Scheme 语言包括更多功能,如变异操作(mutation operations)、向量(vectors)和映射(maps)。然而,到目前为止,我们介绍的这个子集已经提供了一个强大的函数式编程语言,可以实现我们在本文中讨论过的许多概念。