Common Lisp/基础主题/函数
函数是一个几乎在每种编程语言中都会遇到的概念,但在 Lisp 中,函数尤其重要。从历史上看,Lisp 受 lambda 演算 的启发,其中每个对象都是一个函数。另一方面,在许多编程语言中,函数几乎不是对象。Lisp 不是这样:这里的函数与其他对象具有相同的权限,我们将在本章中讨论它。
函数最常使用defun(DEfine FUNction)宏来创建。此宏接受一个参数列表和一个 Lisp 表达式序列,称为函数的主体。defun 的典型用法如下
(defun print-arguments-and-return-sum (x1 x2)
(print x1)
(print x2)
(+ x1 x2))
这里,参数列表是 (x1 x2),主体是 (print x1) (print x2) (+ x1 x2)。当调用函数时,主体中的每个表达式都会按顺序(从第一个到最后一个)求值。如果发生了一些事情,我们想在到达最后一个表达式之前从函数中返回怎么办?return-from 宏允许我们做到这一点。例如
(defun print-arguments-and-return-sum (x1 x2)
(print x1)
(print x2)
(unless (and (numberp x1) (numberp x2))
(return-from print-arguments-and-return-sum "Error!"))
(+ x1 x2))
return-from 的第二个参数是可选的,这意味着它可以只使用一个参数调用 - 在这种情况下,函数将返回nil。但是等等:他们是怎么做到的?我们的函数只接受两个参数:不多也不少。答案是我们所说的“参数列表”并不像看起来那样简单。它实际上被称为lambda 列表,而我们将在其他地方遇到它。稍后,我将解释如何在函数中允许可选和关键字参数。
如本章开头所述,Lisp 函数可以像任何其他对象一样使用:它们可以存储在变量中,作为参数传递给其他函数,并作为函数的值返回。在上一节中,我们定义了一个函数。该函数现在存储在符号print-arguments-and-return-sum的函数单元中 - defun 将其放在那里。但是,它并不永远绑定到这个位置 - 我们可以将其提取出来,并放到另一个符号中,例如。为了访问符号的函数单元,我们可以使用访问器symbol-function。让我们将我们的函数存储在另一个符号中
(setf paars (symbol-function 'print-arguments-and-return-sum))
第一个反应是做这样的事情
>(paars 1 1)
EVAL: undefined function PAARS
[Condition of type SYSTEM::SIMPLE-UNDEFINED-FUNCTION]
这是因为我们将函数放入了符号的值单元而不是其函数单元。很容易修复
(setf (symbol-function 'paars)
(symbol-function 'print-arguments-and-return-sum))
由于symbol-function 是一个访问器,我们可以使用setf 与其一起使用。现在 (paars 1 1)
会产生它应该产生的结果。
虽然symbol-function 存在是有原因的,但它几乎从未在真实代码中使用。这是因为它被 Lisp 的其他几个特性所取代。其中之一是function 特殊运算符。它与symbol-function 特殊运算符的工作方式类似,只是它不会求值其参数,并且它返回当前绑定到符号的函数,这可能实际上不是其函数单元。(function foo)
也可以缩写为 #'
,这极大地提高了它的实用性。另一方面,不可能写
(setf (function paars) (function print-arguments-and-return-sum))
幸运的是,可以从除符号的函数单元以外的其他地方调用函数。函数设计器要么是一个符号(在这种情况下使用其函数单元),要么是函数本身。funcall 和apply 用于通过其函数设计器调用函数。请记住,符号paars 现在在其函数单元和值单元中包含相同的函数。让我们改变它的值单元,以便差异显而易见
(setf paars #'+)
现在让我们以不同的方式调用它
(funcall paars 1 2) ;equivalent to (+ 1 2)
(funcall 'paars 1 2) ;equivalent to (funcall (symbol-function paars) 1 2)
(funcall #'paars 1 2) ;equivalent to (paars 1 2)
第二个和第三个例子之间的区别在于,如果paars 暂时绑定(使用flet 或labels)到其他函数,则第三个 funcall 将使用此临时函数,而第二个 funcall 将仍然使用其函数单元。