ESC
输入关键词搜索文章
目录

函数

Build Your Own Lisp · 第 12 章
实现函数定义和调用,包括 Lambda 表达式和闭包
Chapter 12 · 函数
Lambda 与闭包

章节信息

原书章节:第 12 章 Functions

中文翻译KSCO (GitHub)

原书地址buildyourownlisp.com

函数定义

在上一章中,我们添加了变量和环境。现在我们可以给内置函数赋予名字,但还不能定义自己的函数。本章将改变这一点。

在 Lisp 中,定义函数的方式是使用 \(或 lambda)符号。它接受两个参数:第一个参数是形式参数列表(要绑定的变量名),第二个参数是函数体(一个表达式,将在函数被调用时求值)。

Lambda 表达式

Lambda 表达式是一种匿名函数,它由两部分组成:

  • 形式参数:函数接受的参数名列表
  • 函数体:一个表达式,在函数被调用时求值

示例:\ {x y} {+ x y} 定义了一个接受两个参数并返回它们之和的函数。

为了实现函数,我们需要一个新的 lval 类型来表示函数。这个类型需要存储:

  • 形式参数列表
  • 函数体
  • 定义函数时的环境(用于实现闭包)
typedef struct {
  lbuiltin builtin;
  lenv* env;
  lval* formals;
  lval* body;
} func;

对于内置函数,我们只需要 builtin 字段。对于用户定义的函数,我们需要 envformalsbody 字段。

闭包

当我们定义一个函数时,我们需要记住定义它时的环境。这是因为函数可能引用了定义它时环境中存在的变量。这种将函数与其定义环境结合在一起的技术叫做闭包(closure)。

闭包(Closure)

闭包是一个函数与其定义时的环境的组合。它"封闭"了定义时可见的所有变量,使得这些变量在函数被调用时仍然可用。

闭包使得函数可以访问其定义作用域中的变量,即使函数在该作用域之外被调用。

闭包是函数式编程的核心概念之一。它让我们可以:

  • 创建捕获外部变量的函数
  • 实现数据隐藏和封装
  • 创建工厂函数和高阶函数
函数调用

当我们调用一个用户定义的函数时,我们需要:

  1. 将形式参数绑定到实际参数
  2. 在函数的环境中求值函数体
  3. 返回求值结果

这个过程可以用以下代码实现:

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 表达式:匿名函数,由形式参数和函数体组成
  • 闭包:函数与其定义环境的组合
  • 词法作用域:函数只能访问定义时可见的变量
  • 形式参数:函数定义时声明的参数名
  • 实际参数:函数调用时传入的值