编程语言简介/评估策略
编程语言采用的参数评估策略定义了在函数调用期间何时评估参数。主要有两种策略:严格评估和惰性评估。
严格评估策略是在将参数传递给函数之前完全评估参数。两种最常见的评估策略:按值调用和按引用调用,都属于此类。
按值调用: 按值调用策略是指将实际参数的内容复制到形式参数中。在形式参数中执行的状态更改不会反映回实际参数。以下在 C 中实现的交换函数就是一个著名的此类行为的例子
void swap(int x, int y) {
int aux = x;
x = y;
y = aux;;
}
int main() {
int a = 2;
int b = 3;
printf("%d, %d\n", a, b);
swap(a, b);
printf("%d, %d\n", a, b);
}
一旦调用 swap 函数,变量 a 和 b 的内容分别被复制到形式参数 x 和 y 中。在 swap 函数体中发生的數據交换只影响形式参数,而不是实际参数。换句话说,在这个程序中,swap 的调用是无害的。为了规避这种语义,C 语言允许我们使用指针传递一个位置的地址,而不是它的内容。因此,下面的函数按预期交换了两个变量的内容
void swap(int *x, int *y) {
int aux = *x;
*x = *y;
*y = aux;
}
int main() {
int a = 2;
int b = 3;
printf("%d, %d\n", a, b);
swap(&a, &b);
printf("%d, %d\n", a, b);
}
按值调用策略在编程语言中非常常见。它是 C、Java、Python 甚至 C++ 中的首选策略,尽管 C++ 也支持按引用调用。
按引用调用: 在按值调用策略中,我们将实际参数的内容复制到形式参数中,而在按引用调用策略中,我们将实际参数的地址复制到形式参数中。一些语言实现了按引用调用策略。C++ 就是其中之一。以下程序重新实现了 swap 函数,使用按引用调用策略
void swap(int &x, int &y) {
int aux = x;
x = y;
y = aux;
}
int main() {
int a = 2;
int b = 3;
printf("%d, %d\n", a, b);
swap(a, b);
printf("%d, %d\n", a, b);
}
在 C++ 中,按引用传递参数是 语法糖,用于使用指针。如果我们仔细研究一下 g++(C++ 编译器)为上面的 swap 函数和带有指针的 swap 函数生成的汇编代码,我们会发现它们是完全相同的。
如果传递给函数的数据结构的大小很大,按引用调用可能比按值调用更快。然而,这种策略在目前的主流语言中并不存在,除了 C++。按引用传递参数可能会导致难以理解的程序。例如,下面的函数也实现了 swap 函数;但是,它结合了三个 异或 操作来避免需要辅助变量。
void xor_swap(int &x, int &y) {
x = x ^ y;
y = x ^ y;
x = x ^ y;
}
如果形式参数 x 和 y 引用了同一个位置,这个函数可能会导致意想不到的结果。例如,以下程序使用 xor_swap 实现,将实际参数置零,而不是保留其值
int main() {
int a = 2;
int b = 3;
printf("%d, %d\n", a, b);
xor_swap(a, a);
printf("%d, %d\n", a, b);
}
严格评估策略强制在将实际参数传递给被调用函数之前对其进行评估。为了说明这一点,以下在 Python 中实现的程序将循环。
def andF(a, b):
if not a:
return True
else:
return b
def g(x):
if g(x):
return True
else:
return False
f = andF(False, g(3))
有一些参数传递策略不需要在将参数传递给被调用函数之前对其进行评估。这些策略被称为惰性。三种最著名的惰性策略是按宏展开调用、按名调用和按需调用。
按宏展开调用: 许多编程语言,包括 C、Lisp 和 Scheme,为开发者提供了一种机制,可以在核心语言语法中添加新的语法,称为 宏。宏由一个宏 预处理器扩展为代码。这些宏可能包含参数,这些参数在预处理器生成的最终代码中被复制。例如,以下 C 程序通过宏实现了 swap 函数
#define SWAP(X,Y) {int temp=X; X=Y; Y=temp;}
int main() {
int a = 2;
int b = 3;
printf("%d, %d\n", a, b);
SWAP(a, b);
printf("%d, %d\n", a, b);
}
这个宏实现了一个有效的 swap 例程。预处理后的程序将类似于以下代码。因为宏体被直接复制到调用程序的文本中,所以它是在该程序的上下文中操作的。换句话说,宏将直接引用它接收的变量名,而不是它们的值。
int main() {
int a = 2;
int b = 3;
printf("%d, %d\n", a, b);
{ int tmp = (a); (a) = (b); (b) = tmp; };
printf("%d, %d\n", a, b);
}
传递给宏作为参数的表达式会在每次它们在宏体中使用时进行评估。如果参数从未使用,则它根本不会被评估。例如,以下程序将使变量 b 自增两次
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
int main() {
int a = 2, b = 3;
int c = MAX(a, b++);
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
宏有一个问题,叫做变量捕获。如果一个宏定义了一个在调用者环境中已经定义的变量 v,而 v 被作为参数传递给宏,那么宏体将无法区分 v 的一个出现和另一个出现。例如,以下程序有一个定义变量 temp 的宏。main 中的调用会导致这个函数中定义的变量 temp 被宏体中的定义捕获。
#define SWAP(X,Y) {int temp=X; X=Y; Y=temp;}
int main() {
int a = 2;
int temp = 17;
printf("%d, temp = %d\n", a, temp);
SWAP(a, temp);
printf("%d, temp = %d\n", a, temp);
}
一旦这个程序被 C 预处理器扩展,我们将得到以下代码。这个程序无法交换变量 temp 和 a 的值
int main() {
int a = 2;
int temp = 17;
printf("%d, temp = %d\n", a, temp);
{int temp=a; a=temp; temp=temp;};
printf("%d, temp = %d\n", a, temp);
}
有一些惰性评估策略可以避免变量捕获问题。两种最著名的技术是按名调用和按需调用。
按名调用: 在这种评估策略中,实际参数只会在函数内部使用时才进行评估;但是,这种评估使用的是调用者的上下文。例如,在下面的例子中,取自 韦伯的书,我们有一个返回整数 6 的函数 g。在函数 f 中,第一个赋值,例如 b = 5,将 5 存储到变量 i 中。第二个赋值,b = a,读取 i 的值(当前为 5),并对其加 1。然后,这个值被存储到 i 中。
void f(by-name int a, by-name int b) {
b=5;
b=a;
}
int g() {
int i = 3;
f(i+1,i);
return i;
}
很少有语言实现按名调用评估策略。其中最著名的语言是 Algol。 Simula 是 Algol 的直接后代,也实现了按名调用,正如我们在这个 例子 中看到的。按名调用总是会导致参数的评估,即使这个参数被多次使用。这种行为在 引用透明 的语言中可能会造成浪费,因为在这些语言中,变量是不可变的。有一种评估策略可以解决这个问题:按需调用。
按需调用: 在这种评估策略中,参数只会在使用时才进行评估。然而,一旦第一次评估发生,它的结果就会被缓存,因此对参数的进一步使用不需要重新评估。这种机制提供以下三个保证
- 表达式只有在调用函数需要其结果时才进行评估;
- 表达式只会被调用函数需要评估的程度进行评估;
- 表达式绝不会被评估超过一次,称为应用顺序评估。
Haskell 是一种以使用按需调用而闻名的语言。这种评估策略是语言设计者用来保持 Haskell 为纯函数式语言的关键特征。例如,按需调用允许语言将输入通道模拟为无限列表,该列表只需要读取数据时才进行评估。例如,以下程序计算 斐波那契数列 的第 n 项。然而,生成这个序列的函数 fib 没有终止条件!
fib m n = m : (fib n (m+n))
getIt [] _ = 0
getIt (x:xs) 1 = x
getIt (x:xs) n = getIt xs (n-1)
getN n = getIt (fib 0 1) n
getIt 函数只扩展 fib 生成的列表,只要读取它的第 n 个元素就足够了。例如,以下是一系列计算斐波那契数列第 4 项的调用
getIt (fib 0 1) 4
= getIt (0 : fib 1 1) 4
getIt (fib 1 1) 3
= getIt (1 : fib 1 2) 3
getIt (fib 1 2) 2
= getIt (1 : fib 2 3) 2
getIt (fib 2 3) 1
= getIt (2 : fib 3 5) 1
= 2