当存储在函数单元格中时,Lisp 将函数更改为 lambda 表达式

Lisp changes function to lambda expression when stored in function cell

post中,我切题地问为什么我在SBCL

中声明
(defun a (&rest x)
    x)

然后检查函数单元格包含的内容

(describe 'a)
COMMON-LISP-USER::A
  [symbol]

A names a compiled function:
  Lambda-list: (&REST X)
  Derived type: (FUNCTION * (VALUES LIST &OPTIONAL))
  Source form:
    (LAMBDA (&REST X) (BLOCK A X))

我看到了原始功能的这个特殊细分。有人能解释一下这个输出是什么意思吗?我对最后一行感到特别困惑

 Source form:
        (LAMBDA (&REST X) (BLOCK A X))

这很神秘,因为出于某种我不清楚的原因,Lisp 已将原始函数转换为 lambda 表达式。如果知道像这样分解的函数是如何被调用的细节也很好。这个例子是 SBCL。在 Elisp 中

(symbol-function 'a)

给予

(lambda (&rest x) x)

再次,奇怪。正如我在另一个 post 中所说的那样,这在 Scheme 中更容易理解——但这在答案中造成了混淆。所以我再一次问,为什么 Lisp 采用普通函数声明并将其存储为 lambda 表达式?

DEFUN 是定义宏。宏转换代码。

在普通 Lisp 中:

(defun foo (a)
  (+ a 42))

以上是定义形式,但会被DEFUN转成其他代码

效果类似

(setf (symbol-function 'foo)
      (lambda (a)
        (block foo
          (+ a 42))))

以上将符号FOO的函数单元格设置为一个函数。 BLOCK 构造由 SBCL 添加,因为在 Common Lisp 中由 DEFUN 定义的命名函数创建一个与函数名称同名的 BLOCK。然后 RETURN-FROM 可以使用此块名称从特定函数启用非本地 return。

此外,DEFUN 还执行特定于实现的事情。实现中还记录了开发信息:源代码、定义位置等

方案有定义:

(define (foo a)
  (+ a 10))

这会将 FOO 设置为函数对象。

我还是有点不清楚你在困惑什么,但这里试图解释一下。我会坚持使用 CL(主要是 ANSI CL),因为 elisp 有很多历史上的怪事,这只会让事情变得难以理解(elisp 有一个附录)。 Pre-ANSI CL 在很多事情上也不太清楚。

我会尝试通过 编写 一个宏来解释事情,它是 defun 的简单版本:我称之为 defun/simple,并且它的一个使用示例是

(defun/simple foo (x)
  (+ x x))

所以我需要做的是弄清楚这个宏的扩展应该是什么,以便它做一些大致相同的事情(但比更简单)defun

函数命名空间 & fdefinition

首先,我假设您对以下观点感到满意:在 CL(和 elisp)中,函数的命名空间不同于变量绑定的命名空间:两种语言都是 lisp-2。所以在像 (f x) 这样的形式中, f 是在函数绑定的命名空间中查找的,而 x 是在变量绑定的命名空间中查找的。这意味着像

这样的形式
(let ((sin 0.0))
  (sin sin))

在 CL 或 elisp 中很好,而在 Scheme 中它们会出错,因为 0.0 不是函数,因为 Scheme 是 lisp-1。

所以我们需要某种方式来访问该名称空间,在 CL 中最通用的方式是 fdefinition(fdefinition <function name>) 获取 <function name> 的函数定义,其中 <function name> 是命名函数的东西,就我们的目的而言,它将是一个符号。

fdefinition 是 CL 调用的 accessor:这意味着 setf macro 知道如何处理它,因此我们可以改变函数通过 (setf (fdefinition ...) ...) 绑定一个符号。 (这不是真的:我们可以使用 fdefinition 访问和改变的是符号的 顶级 函数绑定,我们 不能 访问或改变词法函数绑定,CL 没有提供这样做的方法,但这在这里并不重要。)

所以这告诉我们宏扩展需要是什么样子:我们想要将名称的(顶级)定义设置为某个函数对象。宏的展开应该是这样的:

(defun/simple foo (x)
  x)

应该扩展到涉及

的东西
(setf (fdefinition 'foo) <form which makes a function>)

所以我们现在可以写这个宏了:

(defmacro defun/simple (name arglist &body forms)
  `(progn
     (setf (fdefinition ',name)
           ,(make-function-form name arglist forms))
     ',name))

这是这个宏的完整定义。它在其展开中使用 progn,以便展开它的结果是被定义的函数的名称,这与 defun 相同:展开通过副作用完成所有实际工作。

但是 defun/simple 依赖于一个辅助函数,叫做 make-function-form,我还没有定义它,所以你实际上不能 使用 它还.

函数形式

所以现在我们需要写make-function-form。这个函数在宏展开时被调用:它的工作不是创建一个函数:它是return一点源代码,它将创建一个函数,我正在调用一个 'function form'.

那么,CL 中的函数形式是什么样的?好吧,在可移植 CL 中实际上只有一种这样的形式(这可能是错误的,但我认为是真的),这是一种使用特殊运算符 function 构造的形式。所以我们需要 return 一些看起来像 (function ...) 的表格。那么,... 可以是什么? function.

有两种情况
  • (function <name>)表示在当前词法环境中由<name>命名的函数。所以 (function car) 就是我们说 (car x).
  • 时调用的函数
  • (function (lambda ...))表示(lambda ...)指定的函数:a lambda expression.

第二个是(如上警告)我们可以构建表示新函数的形式的方式。所以 make-function-form 将需要 return 这第二种 function 形式。

所以我们可以写一个make-function-form的初始版本:

(defun make-function-form (name arglist forms)
  (declare (ignore name))
  `(function (lambda ,arglist ,@forms)))

这足以让 defun/simple 工作:

> (defun/simple plus/2 (a b)
    (+ a b))
plus/2

> (plus/2 1 2)
3

但还不太对:defun 定义的函数可以做的一件事是 return 来自他们自己:他们知道自己的名字并且可以使用 return-from 来return 来自它:

> (defun silly (x)
    (return-from silly 3)
    (explode-the-world x))
silly

> (silly 'yes)
3

defun/simple 还不能这样做。为此,make-function-form 需要在函数体周围插入一个合适的 block

(defun make-function-form (name arglist forms)
  `(function (lambda ,arglist
               (block ,name
                 ,@forms))))

现在:

> (defun/simple silly (x)
    (return-from silly 3)
    (explode-the-world x))
silly

> (silly 'yes)
3

一切都很好。

这是defun/simple及其辅助函数的最终定义。

查看 defun/simple

的扩展

我们可以用 macroexpand 以通常的方式做到这一点:

> (macroexpand '(defun/simple foo (x) x))
(progn
  (setf (fdefinition 'foo) 
        #'(lambda (x) 
            (block foo
              x)))
  'foo)
t

这里唯一令人困惑的是,因为 (function ...) 在源代码中很常见,所以它有语法糖 #'...:这与 quote 的原因相同有特殊语法。

真正的 defun 形式的宏扩展值得一看:它们通常有一堆特定于实现的东西,但你可以在那里找到同样的东西。这是来自 LW 的示例:

 > (macroexpand '(defun foo (x) x))
(compiler-let ((dspec::*location* '(:inside (defun foo) :listener)))
  (compiler::top-level-form-name (defun foo)
    (dspec:install-defun 'foo
                         (dspec:location)
                         #'(lambda (x)
                             (declare (system::source-level
                                       #<eq Hash Table{0} 42101FCD5B>))
                             (declare (lambda-name foo))
                             x))))
t

好吧,这里有很多额外的东西,而且 LW 显然有一些关于这个 (declare (lambda-name ...)) 形式的技巧,可以让 return-from 在没有显式块的情况下工作。但是你可以看到基本上是一样的事情在发生。

结论:你是如何制作函数的

总而言之:像 defun 这样的宏或任何其他函数定义形式需要扩展为一种形式,在计算时将构造一个函数。 CL 恰好提供了一种这样的形式:(function (lambda ...)): 这就是您在 CL 中创建函数的方式。所以像 defun 这样的东西必然要扩展成这样。 (准确地说:defun 的任何可移植版本:实现在某种程度上可以自由地执行实现魔法,并且可以这样做。但是它们 不能 自由地添加新的特殊运算符.)

当您调用 describe 时,您看到的是,在 SBCL 编译您的函数后,它会记住源格式是什么,并且源格式正是您从 defun/simple 此处给出宏。


备注

lambda 作为宏

在 ANSI CL 中,lambda 被定义为一个宏,其扩展是一个合适的 (function (lambda ...)) 形式:

> (macroexpand '(lambda (x) x))
#'(lambda (x) x)
t

> (car (macroexpand '(lambda (x) x)))
function

这意味着您不必自己编写 (function (lambda ...)):您可以依靠 lambda 的宏定义为您完成。从历史上看,lambda 在 CL 中并不总是一个宏:我找不到我的 CLtL1 副本,但我很确定它没有被定义为一个。我有理由相信 lambda 的宏定义已经到来,因此可以在 CL 之上编写与 ISLisp 兼容的程序。它必须在语言中,因为 lambdaCL 包中,因此用户不能为其定义可移植的宏(尽管他们经常定义这样的宏,或者至少我是这样) .我没有依赖上面这个宏定义。

defun/simple 并不是 defun 的正确克隆:它的唯一目的是展示如何编写这样的宏。特别是它没有正确处理声明,我认为:它们需要从 block 中移除,但不是。

Elisp

Elisp比CL恐怖多了。特别是,在 CL 中有一个定义明确的 function 类型,它与列表不相交:

> (typep '(lambda ()) 'function)
nil

> (typep '(lambda ()) 'list)
t

> (typep (function (lambda ())) 'function)
t

> (typep (function (lambda ())) 'list)
nil

(请特别注意,(function (lambda ())) 是一个函数,而不是一个列表:function 正在完成它创建函数的工作。)

然而,在 elisp 中,一个解释函数 只是一个列表,其 car 是 lambda(警告:如果词法绑定在这不是这种情况:它是然后是汽车为 closure 的列表)。所以在 elisp 中(没有词法绑定):

ELISP> (function (lambda (x) x))
(lambda (x)
  x)

ELISP> (defun foo (x) x)
foo
ELISP> (symbol-function 'foo)
(lambda (x)
  x)

然后 elisp 解释器将按照您自己的方式解释这个列表。 function 在 elisp 中几乎与 quote.

相同

但是 function 与 elisp 中的 quote 不完全相同:字节编译器知道,当遇到像 (function (lambda ...)) 这样的形式时,这是一个函数形式,它应该字节编译主体。所以,我们可以看看defun在elisp中的展开:

ELISP> (macroexpand '(defun foo (x) x))
(defalias 'foo
  #'(lambda (x)
      x))

(原来defalias是现在最原始的东西。)

但是如果我将这个定义放在一个文件中,然后进行字节编译和加载,那么:

ELISP> (symbol-function 'foo)
#[(x)
  "7"
  [x]
  1]

你可以进一步探索这个:如果你把 this 放在一个文件中:

(fset 'foo '(lambda (x) x))

然后字节编译加载,然后

ELISP> (symbol-function 'foo)
(lambda (x)
  x)

所以字节编译器没有对 foo 做任何事情,因为它没有得到它应该做的提示。但是 foo 仍然是一个很好的函数:

ELISP> (foo 1)
1 (#o1, #x1, ?\C-a)

只是没有编译。这也是为什么如果在其中编写带有匿名函数的 elisp 代码,您应该使用 function(或等效的 #')。 (最后,当然,如果启用词法作用域,(function ...) 会做正确的事情。)

在 CL 中创建函数的其他方法

最后,我在上面说过 function 并且特别是 (function (lambda ...)) 是在 CL 中创建新函数的唯一原始方法。我不完全确定这是真的,尤其是考虑到 CLOS(几乎所有 CLOS 都会有某种 class 实例,它们是函数,但可以被 subclassed)。但这没关系:这是 a 方式就足够了。