函数
在上一章中,我们添加了变量和环境。现在我们可以给内置函数赋予名字,但还不能定义自己的函数。本章将改变这一点。
在 Lisp 中,定义函数的方式是使用 \(或 lambda)符号。它接受两个参数:第一个参数是形式参数列表(要绑定的变量名),第二个参数是函数体(一个表达式,将在函数被调用时求值)。
Lambda 表达式
Lambda 表达式是一种匿名函数,它由两部分组成:
- 形式参数:函数接受的参数名列表
- 函数体:一个表达式,在函数被调用时求值
示例:\ {x y} {+ x y} 定义了一个接受两个参数并返回它们之和的函数。
为了实现函数,我们需要一个新的 lval 类型来表示函数。这个类型需要存储:
- 形式参数列表
- 函数体
- 定义函数时的环境(用于实现闭包)
typedef struct {
lbuiltin builtin;
lenv* env;
lval* formals;
lval* body;
} func;
对于内置函数,我们只需要 builtin 字段。对于用户定义的函数,我们需要 env、formals 和 body 字段。
当我们定义一个函数时,我们需要记住定义它时的环境。这是因为函数可能引用了定义它时环境中存在的变量。这种将函数与其定义环境结合在一起的技术叫做闭包(closure)。
闭包(Closure)
闭包是一个函数与其定义时的环境的组合。它"封闭"了定义时可见的所有变量,使得这些变量在函数被调用时仍然可用。
闭包使得函数可以访问其定义作用域中的变量,即使函数在该作用域之外被调用。
闭包是函数式编程的核心概念之一。它让我们可以:
- 创建捕获外部变量的函数
- 实现数据隐藏和封装
- 创建工厂函数和高阶函数
当我们调用一个用户定义的函数时,我们需要:
- 将形式参数绑定到实际参数
- 在函数的环境中求值函数体
- 返回求值结果
这个过程可以用以下代码实现:
lval* lval_call(lenv* e, lval* f, lval* a) {
/* If Builtin then simply apply that */
if (f->builtin) { return f->builtin(e, a); }
/* Record Argument Counts */
int given = a->count;
int total = f->formals->count;
/* While arguments still remain to be processed */
while (a->count) {
/* If we've ran out of formal arguments to bind */
if (f->formals->count == 0) {
lval_del(a);
return lval_err("Function passed too many arguments. "
"Got %i, Expected %i.", given, total);
}
/* Pop the first symbol from the formals */
lval* sym = lval_pop(f->formals, 0);
/* Pop the next argument from the list */
lval* val = lval_pop(a, 0);
/* Bind a copy into the function's environment */
lenv_put(f->env, sym, val);
/* Delete symbol and value */
lval_del(sym);
lval_del(val);
}
/* Argument list is now bound so can be cleaned up */
lval_del(a);
/* If all formals have been bound evaluate */
if (f->formals->count == 0) {
/* Set the parent environment */
f->env->par = e;
/* Evaluate and return */
return builtin_eval(f->env,
lval_add(lval_sexpr(), lval_copy(f->body)));
} else {
/* Otherwise return partially evaluated function */
return lval_copy(f);
}
}
我们的函数使用词法作用域(lexical scoping),这意味着函数只能访问它定义时可见的变量,而不是调用时可见的变量。
词法作用域 vs 动态作用域
词法作用域:函数可以访问定义时作用域中的变量
动态作用域:函数可以访问调用时作用域中的变量
大多数现代编程语言(包括 Lisp 的大多数方言)使用词法作用域,因为它更可预测且更容易推理。
为了实现词法作用域,我们在定义函数时捕获环境,并在调用函数时使用捕获的环境。这确保了函数总是访问相同的变量,无论它在哪里被调用。
复习速查
- Lambda 表达式:匿名函数,由形式参数和函数体组成
- 闭包:函数与其定义环境的组合
- 词法作用域:函数只能访问定义时可见的变量
- 形式参数:函数定义时声明的参数名
- 实际参数:函数调用时传入的值