Skip to content

LISP - 使用 `apply` 调用函数

Lisp - 使用 apply 和 funcall 动态调用函数

Section titled “Lisp - 使用 apply 和 funcall 动态调用函数”

Lisp 以其同像性(homoiconicity,即代码即数据)而闻名,这使得强大的元编程(metaprogramming)成为可能。其中一个关键部分是动态调用函数的能力。apply 和 funcall 函数是完成这项任务的主要工具。它们允许你调用在运行时(runtime)确定的函数。

funcall 和 apply 都用于调用函数,但它们接受参数的方式有所不同:

  • funcall:接受一个函数对象(function object),然后是它的各个参数,一个接一个地传入。
  • apply:接受一个函数对象、任意数量的初始参数,以及一个包含其余参数的列表(list)。
;; 目标是计算 (+ 10 20 30)
;; 使用 FUNCALL:参数是单独传递的。
(funcall #'+ 10 20 30)
;=> 60
;; 使用 APPLY:参数是在一个列表中传递的。
(apply #'+ '(10 20 30))
;=> 60
;; APPLY 也可以在列表之前接受初始参数。
(apply #'+ 10 '(20 30))
;=> 60
;; 常见的错误是给 funcall 传递一个列表。
; (funcall #'+ '(10 20 30)) ; 这会是一个错误!

核心要点:当你的参数已经被收集到一个列表(list)中时,使用 apply。

想象一下,你需要一个函数,它接受一个操作(如 + 或 *)和一个数字列表,并返回结果。这正是 apply 的完美应用场景,因为操作的参数已经在一个列表中了。

(defun calculate (operation numbers)
"Applies a given operation to a list of numbers."
(unless (listp numbers)
(error "'numbers' must be a list."))
(apply operation numbers))
;; --- 使用 calculate 函数 ---
;; 调用加法函数的符号
(print (calculate '+ '(10 20 30)))
;; 调用一个 lambda 函数
(print (calculate (lambda (x y) (- x y)) '(100 25)))
;; 调用一个预定义函数(适用于任意元数 arity)
(print (calculate #'max '(1 8 5 9 2)))

当你执行这段代码时,它会返回以下结果:

60
75
9

apply 对于编写需要将参数转发给另一个函数的包装器(wrapper)函数或宏(macro)至关重要。这通常与函数定义中的 &rest 参数结合使用。

(defun logging-wrapper (function-to-call &rest args)
"A wrapper that logs before and after calling a function with its arguments."
;; 一个包装器,在调用函数及其参数之前和之后进行日志记录。
(format t "--> Calling ~s with arguments ~s~%" function-to-call args)
;; 'args' 是一个列表,所以我们必须使用 APPLY 来调用函数。
(let ((result (apply function-to-call args)))
(format t "<-- Function ~s returned ~s~%" function-to-call result)
result))
;; 使用该包装器
(logging-wrapper #'+ 1 2 3)
--> Calling #<FUNCTION +> with arguments (1 2 3)
<-- Function #<FUNCTION +> returned 6
  • #' 符号:使用 #'(sharp-quote,尖引号)是获取函数对象(例如 #'+)的现代、首选方式。它比使用像 '+ 这样的裸符号(bare symbol)更明确。
  • 动态派发(Dynamic Dispatch):apply 和 funcall 是 Lisp 中动态派发的核心。它们允许你编写灵活的、泛型(generic)的代码,根据运行时条件决定调用哪个函数。
  • 错误处理:请注意,如果使用错误数量或类型的参数调用 apply 或 funcall 来执行目标函数,将导致运行时错误,就像直接调用一样。