Lisp (Allegro Common Lisp) 如何在 lambda 中使用 ' vs #' 中的变量

How Lisp (Allegro Common Lisp) uses variables in lambda with ' vs #'

我希望有人能解释为什么测试 1-5 有效而测试 6 无效。我认为用 ' 引用 lambda 并在 lambda 前面使用 #' 都返回指向函数的指针,唯一的区别是 #' 将首先编译它。

(defun test-1 (y)
  (mapcar (lambda (x) (expt x 2))
      '(1 2 3)))

(defun test-2 (y)
  (mapcar (lambda (x) (expt x y))
      '(1 2 3)))

(defun test-3 (y)
  (mapcar #'(lambda (x) (expt x 2))
      '(1 2 3)))

(defun test-4 (y)
  (mapcar #'(lambda (x) (expt x y))
      '(1 2 3)))

(defun test-5 (y)
  (mapcar '(lambda (x) (expt x 2))
      '(1 2 3)))

(defun test-6 (y)
  (mapcar '(lambda (x) (expt x y))
      '(1 2 3)))

我使用的是免费版的 Franz Industries Allegro Common Lisp。以下是输出:

(test-1 2)     ; --> (1 4 9)
(test-2 2)     ; --> (1 4 9)
(test-3 2)     ; --> (1 4 9)
(test-4 2)     ; --> (1 4 9)
(test-5 2)     ; --> (1 4 9)
(test-6 2)     ; --> Error: Attempt to take the value of the unbound variable `Y'. [condition type: UNBOUND-VARIABLE]

让我们添加一个 y 参数来避免关闭变量,看看我们正在操纵什么样的值:

USER> (type-of #'(lambda (x y) (expt x y)))
FUNCTION

USER> (type-of (lambda (x y) (expt x y)))
FUNCTION

USER> (type-of '(lambda (x y) (expt x y)))
CONS

如您所见,前两个 lambda-like 形式被评估为函数,而第三个被评估为 cons-cell。就 Lisp 而言,第三个参数只是一棵没有意义的符号树。

Reader 宏

I thought that quoting a lambda with ' and using #' in front of a lambda both returned pointers to the function with the only difference being that the #' will compile it first.

我们回到定义,'#'分别是reader个宏,分别是Single-Quote and Sharpsign Single-Quote。它们位于其他形式的前面,例如 'f 读作 (quote f)#'f 读作 (function f)。在 read-time、f 和生成的表格只是未评估的数据。

我们将在下面看到两个特殊运算符是如何解释的,但真正重要的是词法范围,所以让我们打开一个括号。

词汇环境

Lexical environments are the set of bindings in effect at some point of your code. When you evaluate a let or an flet it enriches the current environment with new bindings. When you call EVAL 在表达式上,您从空词法环境开始计算,即使对 eval 本身的调用是在 non-null 环境中。

此处 x 只是在 eval 期间未绑定:

(let ((x 3)) (eval '(list x))) ;; ERROR

这里我们建立一个让eval求值的let:

(eval '(let ((x 3)) (list x)))
=> (3)

这就是词法环境速成课程的全部内容。

特殊运算符

功能

特殊运算符FUNCTION接受一个函数名(符号或setf)或lambda表达式的参数;特别是:

The value of function is the functional value of name in the current lexical environment.

这里的lambda表达式是在当前的词法环境中求值的,这意味着它可以引用lambda表达式之外的变量。这就是闭包的定义,它们捕获周围的绑定。

注意。您不需要在 lambda 前加上 #',因为有一个名为 (lambda ...) 的宏 扩展为 (function (lambda ...))。看起来这可以永远递归扩展,但事实并非如此:首先扩展宏,使 (lambda ...) 变为 (function (lambda ...)),然后特殊运算符 function 知道如何计算lambda 表达式本身。 这意味着 (lambda ...)#'(lambda ...) 是等价的。需要特别注意的是,此时是否编译了一种形式没有任何意义,编译器将在宏展开后看到相同的表达式。

引用

特殊运算符 QUOTE(quote f) 计算为 f,其中 f 本身未计算。在test-5test-6中,没有函数,只是一个未计算的结构化表达式,可以解释为代码。

类型强制

现在,某些功能如MAPCAR用于应用功能。请注意规范如何说 function 参数是一个函数 designator:

function --- a designator for a function that must take as many arguments as there are lists.

类型的指示符不一定是该类型的值,但可以是可以强制转换为该类型的值。有时用户想指定一个路径名,然后输入一个字符串,但字符串不是pathname类型的值:系统必须将字符串转换为路径名。

Common Lisp 定义了一个 COERCE 函数,其中包含有关如何将值转换为其他值的规则。在你的例子中,mapcar 首先执行 (coerce (lambda ...) 'function)。定义如下:

If the result-type is function, and object is a lambda expression, then the result is a closure of object in the null lexical environment.

该值因此在 空词法环境 中计算,因此它无法访问周围的绑定; y 是您的 lambda 表达式中的一个自由变量,并且由于它是在 null 环境中计算的,因此它是未绑定的。这就是 test-5 通过但 test-6 失败的原因。

名称解析、编译器和后期绑定

引用函数 f 时写 #'f'f 是有区别的,其中 f 是一个符号:在第一种情况下,表达式评估为 function 类型的对象,在第二种情况下,您只评估一个符号。

此函数的名称解析可能会根据编译器的工作方式而改变。使用符号作为函数指示符,甚至不需要定义函数,当必须将符号强制转换为函数时,名称已解析。

当您编写 #'f 时,某些编译器可能会删除一级间接,并直接使您的代码跳转到与该函数关联的代码,而无需在运行时解析名称。

然而,这也意味着使用此类编译器(例如 SBCL),您需要重新编译一些函数重新定义的调用点,as-if 函数被声明为 inline ,否则某些旧代码仍将引用 #'f 的先前定义。开始时不一定要考虑这一点,但在实时编码时要牢记这一点可能会造成混淆。

首先,您应该知道您的测试 1-4 符合 Common Lisp,而您的测试 5 和 6 不符合。我相信 Allegro 完全可以做它为 5 和 6 做的事情,但它所做的超出了标准。标准中谈到这一点的是 mapcar, which take function designators as argument, and the definition of a function designator:

等函数的定义

function designator n. a designator for a function; that is, an object that denotes a function and that is one of: a symbol (denoting the function named by that symbol in the global environment), or a function (denoting itself). The consequences are undefined if a symbol is used as a function designator but it does not have a global definition as a function, or it has a global definition as a macro or a special form. [...]

从这里可以清楚地看出,像 (lambda (...) ...) 这样的列表不是函数指示符:它只是一个列表,其汽车恰好是 lambda。 Allegro 正在做的是注意到这个列表实际上是可以转化为函数的东西并执行此操作。

好吧,让我们写一个 mapcar 的版本,它可以完成 Allegro 的功能:

(defun mapcar/coercing (maybe-f &rest lists)
  (apply #'mapcar (coerce maybe-f 'function) lists))

这只使用了 coerce 函数,它知道如何将这样的列表转换为函数等。如果它的参数已经是一个函数,coerce 只是 returns 它。

现在我们可以使用这个函数编写两个测试:

(defun test-5/coercing (y)
  (mapcar/coercing '(lambda (x) (expt x 2))
                   '(1 2 3)))

(defun test-6/coercing (y)
  (mapcar/coercing '(lambda (x) (expt x y))
                   '(1 2 3)))

那么,在那个序言之后,为什么 test-6/explicit 不能工作?好吧,答案是 Common Lisp 是(特殊变量除外)词法范围词法范围 只是一种奇特的说法,可用的绑定(变量)完全是并且只有你可以通过查看 看到 的绑定程序的来源。 (除了用于特殊绑定的 CL,我将忽略它,因为这里有 none。)

因此,鉴于此,考虑 test-6/coercing,尤其是对 mapcar/coercing 的调用:在该调用中,coerce 必须将列表 (lambda (x) (expt z y)) 转换为一个功能。所以它就是这样做的。但是它 returns 的函数没有绑定 y 并且其中没有可见的 y 的绑定:该函数使用 y 'free'.

唯一可行的方法是 coerce 为我们构造的函数 动态地 寻找 y 的绑定。嗯,这就是 dynamically-scoped 语言所做的,但 CL 不是 dynamically-scoped。

也许使这一点更清楚的一种方法是认识到我们可以直接从函数中提取函数创建:

(defun test-7 (y f)
  (mapcar f '(1 2 3)))

> (test-7 1 (coerce '(lambda (x) (expt x y)) 'function))

很明显,这不适用于 lexically-scoped 语言。

那么,测试 1-4 是如何进行的?

嗯,首先这里实际上只有两个测试。在 CL 中,lambda 是一个宏,(lambda (...) ...) 完全等同于 (function (lambda (...) ...))。当然 #'(lambda (...) ...) (function (lambda (...) ...)) 相同:它只是一个 read-macro。

(function ...) 是一个神奇的东西(一种特殊形式),表示 'this is a function'。 function 的重要之处在于它不是一个函数:它是一个非常神奇的东西,它告诉求值器(或编译器)它的参数是一个函数 在当前词法上下文中的描述 ,所以,例如在

(let ((x 1))
  (function (lambda (y) (+ x y))))

这个创建的函数引用的 x 是由 let 绑定的 x。所以在你的测试 2 和 4(相同)中:

(defun test-4 (y)
  (mapcar (function (lambda (x) (expt x y)))
      '(1 2 3)))

创建的函数引用的y的绑定是词法可见的y的绑定,是test-4本身的参数。