galeo's blog

In the morning after, you begin to see the light.

Common Lisp - Function Parameter Lists

Practical Common Lisp post series

Common Lisp's parameter lists give you more flexible ways of mapping the arguments in a function call to the function's parameters … (They) provide a convenient solution to several common coding problems.

必要形参(Required Parameters)

形参列表如果仅是一个由变量名组成的简单列表,这些形参被称为 必要形参 。函数被调用时,必须为它的每一个必要形参都提供一个实参,每一个形参被绑定到与之对应的实参上。

可选形参(Optional Parameters)

在必要形参的名字之后放置符号 &optional ,后接可选形参。e.g.:

(defun foo (a b &optional c d)
  (list a b c d))

如果实参在所有可选形参被赋值之前用完了,那么其余的可选形参将自动绑定到值 NIL 上。如果想要一个不同于 NIL 的默认值,可以通过将形参名替换成一个含有 名字跟表达式 的列表来制定该默认值,在调用者没有传递足够的实参来为可选形参提供值的时候,这个表达式才会被求值。e.g.:

(defun foo (a &optional (b 10))
  (list a b))

可以灵活地选择默认值,默认值表达式可以应用早先出现在形参列表中的形参。e.g.:

(defun make-rectangle (width &optional (height width)) ...)

本例中,除非明确指定否则将导致 height 形参带有和 width 形参相同的值。

想确定一个可选形参的值究竟是被调用者明确指定的还是使用了默认值,通过在形参标识符的默认值表达式之后添加一个变量名来做到这一点。该变量将在调用者实际为该形参提供了一个实参时被绑定到真值,否则绑定到 NIL 。通常约定,这种变量的名字与对应的真实形参相同,但是带有一个 -supplied-p 后缀。e.g.:

(defun foo (a b &optional (c 3 c-supplied-p))
  (list a b c c-supplied-p))

剩余形参(Rest Parameters)

Lisp允许在符号 &rest 之后包括一揽子形参。如果函数带有 &rest 形参,那么任何满足了必要和可选形参之后的剩余所有实参就将被收集到一个列表里成为该 &rest 形参的值。 FORMAT+ 的形参列表可能看起来会是这样:

(defun format (stream string &rest values) ...)
(defun + (&rest numbers) ...)

关键字形参(Keywords Parameters)

可选形参是位置相关的,如果调用者想要给一个接受四个可选参数的函数的第四个可选形参传递一个显式的值,就会导致前三个可选形参对于该调用者来说变成了必要形参。关键字形参允许调用者指定具体形参的值。

使函数带有关键字形参,在任何必要的和 &optional 以及 &rest 形参之后,可以加上符号 &key 以及任意数量的关键字形参标识符,后者的格式类似于可选形参标识符。e.g.:

(defun foo (&key a b c)
  (list a b c))

当调用这个函数时,每一个关键字形参将被绑定到紧跟在同名键字后面的那个值上,关键字是以冒号开始的名字,并且它们将被自动定义为自求值常量。

如果一个给定的关键字没有出现在实参列表中,那么对应的形参将被赋予其默认值,如同可选形参那样。

如同可选形参那样,关键字形参也可以提供一个默认值形式以及一个 supplied-p 变量名。在关键字形参和可选形参中,默认值形式都可以引用那些早先出现在形参列表中的形参。e.g.:

(defun foo (&key (a 0) (b 0 b-supplied-p) (c (+a b)))
  (list a b c b-supplied-p))

(foo :a 2 :b 1 :c 4) ; --> (2 1 4 T)

如果出于某种原因想让调用者用来指定形参的关键字而不同于实际形参名,可以将形参名替换成一个列表,令其含有调用函数时使用的关键字以及用作形参的名字。e.g.:

(defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p))
  (list a b c-supplied-p))

(foo :apple 10 :box 20 :charlie 30)
; --> (10 20 30 T)

这种风格在想要 将函数的公共API与其内部细节相隔离 时特别有用,通常是因为想要在内部使用短变量名,而不是API中的描述性关键字。1

混合不同的形参类型(Mixing Different Parameter Types)

在单一函数里使用所有四种形参,它们必须以这样的顺序声明: 必要形参可选形参剩余形参关键字形参

在使用多种类型形参的函数中,一般是将 必要形参另外一种类型的形参 组合使用,或者可能是组合 &optional&rest 形参。

&optional 形参和 &rest 形参,当与 &key 形参组合使用时,可能导致奇怪的行为。将 &optional 形参和 &key 形参组合,如果调用者没有为所有可选形参提供值,那么没有得到值的可选形参将吃掉用于关键字形参的关键字和值。如果函数同时使用 &optional 形参和 &key 形参,应该将它变成全部使用 &key 形参的形式,使用关键字形参将会使代码相对易维护和拓展。

可以安全地组合使用 &rest 形参和 &key 形参,但其行为看起来可能比较奇怪。无论 &rest 还是 &key 出现在形参列表中,都将导致所有出现在必要形参和 &optional 形参之后的那些值被特殊处理——要么作为 &rest 形参被收集到一个形参列表中,要么基于关键字被分配到适当的 &key 形参中。如果 &rest&key 同时出现在形参列表中,那么所有剩余的值,包括关键字本身,都将被收集到一个列表里,然后被绑定到 &rest 形参上,而适当的值,也会被绑定到 &key 形参上。e.g.:

(defun foo (&rest rest &key a b c)
  (list rest a b c))

(foo :a 1 :b 2 :c 3) ; --> ((:A 1 :B 2 :C 3) 1 2 3)

参考来源

  • Functions, Practical Common Lisp, by Peter Seibel
  • 《实用Common Lisp编程》-人民邮电出版社,第1版,田春译

Footnotes:

1

实际中该特性不常被用到。

Comments powered by Disqus