在Common Lisp中,为什么在lambda前面要使用#'符号?

39

我想知道为什么我看到的大多数Common Lisp代码都会像这样:

(mapcar #'(lambda (x) (* x x)) '(1 2 3))

而不是简单地写成

(mapcar (lambda (x) (* x x)) '(1 2 3))

尽管两种方式都能工作。我开始学习Common Lisp,并且有Scheme的一些背景,这激起了我的兴趣。

编辑:我知道在函数名之前需要使用#',因为它们存在于不同的命名空间中。我的问题仅限于在lambda之前需要使用#',因为lambda已经返回一个函数对象(我想)。没有使用#号的lambda函数之所以可以工作是因为它经过了宏展开,这使得它更加有趣......

2个回答

43

#'foo 是读者对 (function foo)缩写

在 CL 中,有几个不同的命名空间,#'foo(function foo) 将返回 foo函数值

您可能想要搜索“Lisp-1 vs. Lisp-2”, 查看其他Stackoverflow问题, 或阅读Pitman和Gabriel的旧文章, 以了解多个命名空间(也称为符号的单元格)的概念。

在 lambda 的情况下,CL 中可以省略 #' 的原因是它是一个宏,其展开方式如下(摘自 Hyperspec):

(lambda lambda-list [[declaration* | documentation]] form*)
==  (function (lambda lambda-list [[declaration* | documentation]] form*))
==  #'(lambda lambda-list [[declaration* | documentation]] form*)

#' 可能仍然出于历史原因而使用(我认为在 Maclisp 中,lambda 没有扩展到函数形式),或者因为有些人认为,使用井号引用来标记 lambda 可以使代码更易读或更连贯。在某些特殊情况下,这可能会有所不同,但通常来说,选择哪种形式并不重要。

我想你可以这样想:(function (lambda ...)) 返回创建的函数 (lambda ...)。请注意,CL Hyperspec 中的 lambda 既有宏也有符号条目。从后者可以看出:

lambda 表达式是一个列表,可以在某些上下文中用作函数名称的替代,通过直接描述其行为而不是通过引用已建立的函数的名称间接地表示函数。

function文档 中可以了解到:

如果名称是 lambda 表达式,则返回一个词法闭包。

我认为差异也与这样调用lambda形式有关:((lambda ...) ...),其中它被视为要评估的表单,与(funcall #'(lambda ...) ...)不同。如果您想在此主题上阅读更多内容,则有一个c.l.l thread

该线程中的一些引用:

(lambda (x) ...本身只是一些未引用的列表结构。 它出现在FUNCTION特殊形式(function (lambda (x) ...作为参数时,才会导致函数对象存在

和:

这也受到事实的影响,即LAMBDA宏是ANSI Common Lisp的相当晚的添加,因此所有真正老的家伙(例如,像我这样的人)都是在需要在映射函数中提供#'来调用lambda表达式时学习他们的lisp。 否则,将调用不存在的lambda函数。

宏添加已更改此内容,但我们中的一些人太过于固执,不想改变。


1
我知道命名空间的区别。但是我期望由于lambda直接返回一个函数对象(或者说它确实这样做了),因此不需要调用'function'或#'。为什么会这样呢? - Vítor De Araújo
嗯,纯lambda不返回函数对象...感谢解释。 - Vítor De Araújo
另一个链接讨论(funcall(lambda...))和((lambda...))语法的二元性:http://letoverlambda.com/textmode.cl/guest/chap4.html#sec_4 - Clayton Stanley
这是很好知道的信息,特别是如果你正在使用一个相对的或者是先驱于CL的语言。 - brian_o

19
最好在大多数情况下避免使用 # ',因为它通常是不必要的,并且会让你的代码变得更加冗长。但有一些例外情况需要引用(见下面的示例 4)。
注意:本文中的所有示例都已在 Emacs Lisp(GNU Emacs 25.2.1)中测试过,但它们应该在任何 ANSI Common Lisp 中表现相同。这两种方言的基本概念是相同的。
简单解释: 首先,让我们研究一个最好避免引用的情况。函数是一等对象(例如,像任何其他对象一样对待它们,包括将它们传递给函数并将它们赋值给变量),它们评估为它们自己。匿名函数(例如 lambda 表单)就是其中之一。在 Emacs Lisp(M-x ielm RET)或任何 ANSI Common Lisp 上尝试以下操作。
((lambda (x) (+ x 10)) 20) -> 30

现在,请尝试引用的版本。

(#'(lambda (x) (+ x 10)) 20) -> "function error" or "invalid function..."  
如果您坚持使用 #',则必须编写:

如果您坚持使用 #',则必须编写

(funcall #'(lambda (x) (+ x 10)) 20) -> 30

详细解释
要真正理解何时需要引用,一个人必须知道Lisp如何评估表达式。请继续阅读。我承诺让这个过程简明扼要。

您需要了解一些有关Lisp的基本事实:

  1. Lisp“总是”评估每个表达式。除非表达式被引用,否则它将被返回未经评估的状态。
  2. 原子会自我评估。原子表达式不是列表。例如数字、字符串、哈希表和向量。
  3. 符号(变量名)存储两种类型的值。它们可以保存常规值和函数定义。因此,Lisp符号具有两个称为单元格的插槽来存储这两种类型。非功能性内容通常保存在符号的值单元格中,而函数保存在函数单元格中。同时保存非功能性和功能性定义的能力将Emacs Lisp和Common Lisp置于2-Lisp类别中。符号在表达式中使用哪个单元格取决于符号在列表中的位置。相比之下,在某些方言的Lisp中,Scheme是最著名的,符号只能保存一个值。Scheme没有价值和功能单元格的概念。这样的Lisps集体称为1-Lisps。

现在,您需要大致了解Lisp如何评估S表达式(圆括号表达式)。每个S表达式的评估方法大致如下:

  1. 如果引用,则返回未经评估的状态
  2. 如果未引用,请获取它的CAR(例如第一个元素)并使用以下规则对其进行评估:

a. 如果是原子,则只需返回其值(例如3-> 3,“pablo”->“pablo”)
b. 如果是S表达式,请使用相同的总体过程对其进行评估
c. 如果是符号,请返回其函数单元格中的内容

  1. 评估S表达式的CDR中的每个元素(例如列表的除第一个元素外的所有元素)。
  2. 将从CDR中的每个元素获得的值应用于从CAR获取的函数。

上述过程意味着,在未引用的S表达式的CAR中出现的任何符号都必须具有其函数单元格中的有效功能定义。

现在,让我们回到此帖子开头的示例。为什么

(#'(lambda (x) (+ x 10)) 20)  

为什么会产生错误?因为S表达式的CAR是#'(lambda (x) (+ x 10)),由于函数引用#',Lisp解释器不会对其进行求值。

#'(lambda (x) (+ x 10))

是不是一个函数,但是

(lambda (x) (+ x 10))

需要注意的是引用的目的是为了防止求值,在另一方面,lambda表达式会自我求值,它是一个函数形式,可以作为未引用列表的CAR(首要元素)。当Lisp对CAR进行求值时

((lambda (x) (+ x 10)) 20)  

它得到了(lambda (x) (+ x 20)),这是一个函数,可以应用于列表中其余的参数(前提是CDR的长度等于lambda表达式允许的参数数量)。因此,

((lambda (x) (+ x 10)) 20) -> 30  

问题是何时引用包含函数定义的函数或符号。答案几乎从不是,除非你做得“不正确”。 “不正确”意味着将函数定义放置在符号的值单元格或功能单元格中,而应该反过来。 请参阅以下示例以获得更好的理解:

示例1-存储在值单元格中的函数
假设您需要使用apply与期望可变数量的参数的函数。其中一个示例是符号+。Lisp将+视为常规符号。函数定义存储在+的功能单元格中。您可以使用以下方式将其值分配给其值单元格。

(setq + "I am the plus function").  

如果您进行评估

+ -> "I am the plus function"

然而,(+ 1 2) 的功能仍然如预期一样。

(+ 1 2) -> 3

在递归中,函数apply非常有用。假设你想要对列表中的所有元素求和。你不可以写出如下代码:

(+ '(1 2 3)) -> Wrong type...  

之所以会出现这种情况是因为+号操作符预期其参数为数字类型,而apply函数可以解决这个问题。

(apply #'+ '(1 2 3)) -> (+ 1 2 3) -> 6  

为什么我引用了 + 符号?请记住我上面概述的评估规则。Lisp评估符号apply,通过检索其函数单元中存储的值来实现。它获得一个可以应用于一系列参数的功能过程。但是,如果我不使用引号引用+,Lisp将会从其值单元中检索到存储的值,因为它不是S表达式中的第一个元素。由于我们将+的值单元设置为“I am the plus function”,所以Lisp没有获取+函数单元中保存的函数定义。实际上,如果我们没有将其值单元设置为“我是加法函数”,Lisp将会检索nil,这不是apply所需的函数。

有没有一种方法可以在apply中不加引号使用+?是的,有。您可以直接评估以下代码:

(setq + (symbol-function '+))  
(apply + '(1 2 3))  

由于Lisp在执行(apply + '(1 2 3))时会找到存储在+的值单元中的函数定义,因此这将计算为6

示例2 - 在值单元中存储函数定义
假设您要将函数定义存储在符号的值单元中,可以通过以下方式实现:

(setq AFunc (lambda (x) (* 10 x)))
评估
(AFunc 2)

由于Lisp在AFunc的函数单元中找不到一个函数,因此会生成一个错误。您可以通过使用funcall来解决这个问题,这告诉Lisp使用符号值单元中的值作为函数定义。您可以使用"funcall"来完成这个操作。

(funcall AFunc 2)

假设符号值单元格中存储的功能定义是有效的,


(funcall AFunc 2) -> 20  

您可以使用fset将lambda表达式放置在符号的函数单元中,从而避免使用funcall

(fset 'AFunc (lambda (x) (* 10 x)))  
(AFunc 2)  

这段代码块将返回20,因为Lisp在AFunc的函数单元中找到了一个函数定义。

例子3 - 本地函数
假设你正在编写一个函数,并需要一个仅在此函数范围内使用的函数。 一种典型的解决方案是定义一个仅在主函数作用域内有效的函数。试试这个:

(defun SquareNumberList (AListOfIntegers)
    "A silly function with an unnecessary
   local function."
  (let ((Square (lambda (ANumber) (* ANumber ANumber))))
    (mapcar Square AListOfIntegers)
    )
  )  

(SquareNumberList '(1 2 3))  

这个代码块将会返回

(1 4 9)  

以上示例未引用Square的原因是S表达式根据我上面概述的规则进行评估。首先,Lisp提取mapcar的函数定义。接下来,Lisp提取其第二个参数(例如'Square)值单元格的内容。最后,它将未经评估的(1 2 3)作为第三个参数返回。

示例4 - 符号值和函数单元格的内容
以下是需要使用引号的一个示例。

(setq ASymbol "Symbol's Value")  
(fset 'ASymbol (lambda () "Symbol's Function"))  
(progn  
  (print (format "Symbol's value -> %s" (symbol-value 'ASymbol)))  
  (print (format "Symbol's function -> %s" (symbol-function 'ASymbol)))
  )    

上面的代码将被评估为

"Symbol's value -> Symbol's Value"  
"Symbol's function -> (lambda nil Symbol's Function)"  
nil

需要报价

(fset 'ASymbol (lambda () "Symbol's Function"))  

(symbol-value 'ASymbol)  
and
(symbol-function 'ASymbol)  

否则的话,Lisp 会在每种情况下都得到 ASymbol 的值,从而阻止 fset、symbol-value 和 symbol-function 正确工作。

希望这篇冗长的帖子能对某些人有所帮助。


(setf AFunc (lambda (x) (* 10 x))) followed by (AFunc 2) produces an error: The function COMMON-LISP-USER::AFUNC is undefined. - Flux
你说得对。我在(setf AFunc (lambda (x) (* 10 x)))后面打错了一个字母,然后是(AFunc 2)。我应该输入(fset 'AFunc (lambda (x) (* 10 x))),它会在Emacs Lisp中返回(closure (t) (x) (* 10 x))。评估(AFunc 2)将返回20fset设置符号的函数单元格。我已经相应地更正了文本。 - Pablo A Perez-Fernandez

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接