在 Common Lisp 中,函数的实参数组被称为“lambda 数组”。
lambda 数组可以包含不同类型的实参。
这些不同的类型通过“lambda 数组关键字”来指定,它们都以&开头。
最常用的类型是可选实参、关键字实参和剩余实参。
lambda 数组中位于某个 lambda 数组关键字之后的每个形参都属于该类型。
一个 lambda 数组关键字在一个 lambda 数组中只能使用一次。
lambda 数组也用于其他构造,比如解构和宏,这些在后面会讲到。
在 Common Lisp 中,函数可以有一些可选实参。
它们在 lambda 数组中由 lambda 数组关键字&optional指定。
如果没有指定,形参会绑定到值nil。
如果有多个可选形参,它们会按顺序绑定。
可以为可选形参指定默认值。
最后,还可以为每个可选形参指定一个符号,它会根据函数的调用方是否提供了该形参而绑定为 true 或 false(这被称为“supplied-p 形参”)。
(defun optional-parameters (&optional x (y 'default) (z nil z-supplied-p))
(list x y (if z-supplied-p (list :z-was-supplied z)
(list :z-was-not-supplied z))))
(optional-parameters) ; => (NIL DEFAULT (:Z-WAS-NOT-SUPPLIED NIL))
(optional-parameters 5 nil 10) ; => (5 NIL (:Z-WAS-SUPPLIED 10))
在 Common Lisp 中,函数可以有具名形参(称为“关键字形参”)。
它们在 lambda 数组中由 lambda 数组关键字&key指定。
关键字形参不是必需形参。
和可选形参一样,它们也可以有默认值,以及用于绑定其“是否提供”状态的符号。
调用带有关键字形参的函数时,要把形参作为关键字的名字写在形参值前面。 函数的调用方可以按任意顺序指定关键字形参。
(defun keyword-parameters (&key x (y 'default) (z nil z-supplied-p))
(list x y (if z-supplied-p (list :z-was-supplied z)
(list :z-was-not-supplied z))))
(keyword-parameters) ; => (NIL DEFAULT (:Z-WAS-NOT-SUPPLIED NIL))
(keyword-parameters :y 5) ; => (NIL 5 (:Z-WAS-NOT-SUPPLIED NIL))
(keyword-parameters :z 10 :x 5) ; => (5 DEFAULT (:Z-WAS-SUPPLIED 10))
把可选形参和关键字形参组合在一起时要小心,因为关键字名和实参可能会被可选形参消耗掉:
(defun could-be-confusing (&optional x y &key z) (list x y z))
(could-be-confusing :z 'huh?) ; => (:Z HUH? NIL)
在 Common Lisp 中,函数可以有一个形参,用来保存处理完所有必需形参或可选形参之后“剩下”的实参。
这个形参由 lambda 数组关键字&rest指定。
如果函数的所有实参都被其他类型的形参用掉了,那么剩余形参会绑定到一个空数组。
如果有没用到的实参,剩余形参会绑定到由这些实参组成的数组。
(defun rest-of-it (req &optional opt &rest rest) (list req opt rest))
(rest-of-it 1) ; => (1 NIL NIL)
(rest-of-it 1 2) ; => (1 2 NIL)
(rest-of-it 1 2 3) ; => (1 2 (3))
(rest-of-it 1 2 3 4 5) ; => (1 2 (3 4 5))
在你的帮助下,Lisp 外星人 Lilly 肯定已经做了很多千层面。他们想送你一些剩菜表示感谢。不过在此之前,他们对你帮忙写的其中一个函数有些反馈。
在这个练习中,你要再写几个函数,帮助 Lilly 烹制千层面,并帮他们把剩菜和你分掉。
一共有 6 个任务:3 个是改进现有的千层面烹饪函数,3 个是关于分配剩菜的。
preparation-time-in-minutes更易使用Lilly 说,用你写的preparation-time-in-minutes有点别扭,因为调用函数之前得先把所有层都数一遍。Lilly 想知道,你能不能做一个这样的函数版本:把层(数量不限)作为实参传进去,然后根据有多少层来算出准备时间?提醒一下,准备一层需要 19 分钟。
(preparation-time 'sauce 'cheese 'right-handed-macaroni 'cheese 'sauce
'left-handed-macaroni 'sauce 'sauce 'cheese 'cheese)
;; => 190 (because there are 10 layers.)
虽然 Lilly 的家长总是把千层面烤 337 分钟,但 Lilly 也承认,其他 Lisp 外星人喜欢把千层面烤得久一点或短一点。
所以,如果remaining-minutes-in-oven不再使用expected-minutes-in-oven,而是可以接收一个_可选_形参,用来指定时间是更长、非常长、更短还是非常短,那就好了。更长或更短,是指在默认的 337 基础上加或减 100;非常长或非常短,就是加或减 200。如果没有提供形参,或者实参是 'normal',就应返回默认值 337。
(remaining-minutes-in-oven) ;; => 337
(remaining-minutes-in-oven :normal) ;; => 337
(remaining-minutes-in-oven :shorter) ;; => 237
(remaining-minutes-in-oven :very-short) ;; => 137
(remaining-minutes-in-oven :longer) ;; => 437
(remaining-minutes-in-oven :very-long) ;; => 537
你刚做完那处修改,Lilly 就想起有些 Lisp 外星人喜欢_特别短_的烹饪时间。实际上,他们想要的就是生的。所以现在,如果实参是NIL,函数应返回 0,其他情况的返回值和上一个任务相同。
(remaining-minutes-in-oven) ;; => 337
(remaining-minutes-in-oven nil) ;; => 0
既然你已经帮忙改进了千层面的烹饪函数,现在可是_有好多千层面_了!Lilly 想和你分掉这些剩菜,来报答你所有的帮助。你需要写一个函数,接收 3 个形参:剩菜总量、Lilly 在柜子里找到的装剩菜的容器数量,以及你找到的容器数量。因为寻找装剩菜的容器可能要花不同的时间,而剩菜的确切数量可能在你和 Lilly 翻柜子的时候才能确定,所以你俩一致同意:这个函数应该允许以任意顺序传入实参,这样每确定一个数字,你就可以先把它写下来。等 3 个数字都齐了,只要把函数调用的括号补上,求值就行了。
函数应返回剩下的剩菜量,也就是剩菜重量与你俩找到的容器总数之差。
(split-leftovers :weight 20 :human 10 :alien 5) ;; => 5
(split-leftovers :weight 20 :alien 10 :human 2) ;; => 8
(split-leftovers :alien 12 :weight 20 :human 4) ;; => 4
在各自往返柜子几趟之后,你俩一致认为,如果函数能在没有指定时直接_假定_你或 Lilly 有……比如说 10 个容器,那就太好了。
(split-leftovers :weight 20 :human 5) ;; => 5
(split-leftovers :weight 20 :alien 5) ;; => 5
(split-leftovers :weight 20) ;; => 0
更好的是,函数可以_假定_剩菜足够装进容器里。因此,如果没有提供重量,函数应返回:just-split-it。不过,如果提供了重量,而它是nil,则应返回:looks-like-someone-was-hungry。
(split-leftovers :human 5 :alien 5) ;; => :JUST-SPLIT-IT
(split-leftovers :weight NIL :human 5 :alien 5) ;; => :LOOKS-LIKE-SOMEONE-WAS-HUNGRY