5.函数
定义新函数
一般用DEFUN宏定义函数,声明如下:
(defun name (parameter*)
"Optional documentation string."
body-form*)
一般函数名只包含字母字符和连字符,其他字符也会在明确用途时使用。如果一个函数是由一个类型转换到另一类型时会用->,比如:字符串转换控件string->widget。命名规则中用连字符比下划线和首字母大写更好,frob-widget比frob_widget或frobWidget更符合Lisp风格。
不需要参数时参数列表为空写作()。
在参数表后面的字符串会作为函数的解释文档,可以用DOCUMENTATION函数获取(比如:(documentation 'foo 'function)取得foo函数的文档)。
函数体包含任意数量的表达式,会按顺序求值,并把最后一个表达式的值作为函数的返回值。也可以用RETURN-FROM特殊操作符立即返回。
(defun verbose-sum (x y)
"Sum any two numbers after printing a message."
(format t "Summing ~d and ~d.~%" x y)
(+x y))
这个函数叫verbose-sum,有2个参数为x和y,有文档,函数体有2个表达式。函数返回值为+计算的值。
函数参数列表
像verbose-sum一样用简单的表作为参数列表,叫做“必须的参数”"requiredparameters"。在函数被调用时传递的参数太多或者太少都会出错。
可选参数
可选参数optionalparameters目的在于给调用者一定的选择,可以使用默认的参数,或者自己指定参数,性能更好。在C中常用NULL来作为默认值的判断,或者其他定义的常量或者-1,来解决这个问题。Java中虽然没有可选参数,但是它有函数重载,通过定义多个函数,有不同个数的参数列表来解决。
定义函数有可选参数,在所有必须的参数的后面,接&optional再接可选参数名。例子如下:
(defun foo (a b &optional c d) (list ab c d))
函数被调用时,先绑定所有必须的参数值,再对可选参数进行绑定,如果参数数目不足,可选参数绑定NIL。绑定结果如下:
(foo 1 2) → (1 2 NIL NIL)
(foo 1 2 3) → (1 2 3 NIL)
(foo 1 2 3 4) → (1 2 3 4)
调用时仍会效验参数数目是否在2和4之间,如果小于2或大于4就会报错。
改变可选参数的默认值,在定义时用一个表包含可选参数的名字和一个表达式。表达式只会在调用时该可选参数未指定值时求值。如下:
(defun foo (a &optional (b 10)) (list ab))
调用时:
(foo 1 2) → (1 2)
(foo 1) → (1 10)
默认值的表达式也可以引用前面已声明的参数,定义方式如下:
(defun make-rectangle (width &optional(height width)) ...)
这样在height没指定的情况下会和width的值一样。
在时需要区分可选参数是用户传递的值还是默认值时,用检查可选参数的值是否是默认值的方法,会不一定可行(因为调用者可能传递的恰好就是默认值)。这种情况下在默认值表达式的后面加一个变量,这个变量名与参数名一致在后面接“-supplied-p”。例如:
(defun foo (a b &optional (c 3c-supplied-p))
(list a b c c-supplied-p))
结果如下:
(foo 1 2) → (1 2 3 NIL)
(foo 1 2 3) → (1 2 3 T)
(foo 1 2 4) → (1 2 4 T)
变长参数
FORMAT和+函数都提供变长参数,+(可以用0个参数调用,返回0)。下面是这个函数的合法调用:
(format t "hello, world")
(format t "hello, ~a" name)
(format t "x: ~d y: ~d" x y)
(+)
(+ 1)
(+ 1 2)
(+ 1 2 3)
当然,可以通过给大量的可选参数来处理变长参数,但是这是痛苦乏味的,不是Lisp的方式。因为保守估计合法调用的参数数目至少有50,现在的实现中范围是4,096到536,870,911(常量CALL-ARGUMENTS-LIMIT)。
在&rest后面的参数,会把后面所有的参数都放到一个表中作为该参数的值。所以FORMAT和+看起来像这样:
(defun format (stream string &restvalues) ...)
(defun + (&rest numbers) ...)
关键字参数
假如:有一个函数有4个可选参数。调用者想给第1个参数提供值是没问题的。但是他如果只想给第2到4个参数提供值。这就出现问题了,因为可选参数是按照位置定义的,所以之前的参数都必须指定值,第1到3个参数就会成为必须的参数。
关键字参数允许调用者为某个参数指定值。同可选参数类似的&key之后指定任意数量的关键字参数。只有关键字参数的声明如下:
(defun foo (&key a b c) (list a b c))
调用时以冒号开头接关键字参数名,它会定义为自求值常量。如果没有向关键字参数传递值,它会像可选参数一样赋默认值。因为关键字参数是带标记的,所以可以按任意顺序指定。比如foo被如下调用:
(foo) → (NIL NIL NIL)
(foo :a 1) → (1 NIL NIL)
(foo :b 1) → (NIL 1 NIL)
(foo :c 1) → (NIL NIL 1)
(foo :a 1 :c 3) → (1 NIL 3)
(foo :a 1 :b 2 :c 3)→ (1 2 3)
(foo :a 1 :c 3 :b 2)→ (1 2 3)
和可选参数一样,关键字参数也可以自定义默认值,引用之前的值。检测是否调用者传递的supplied-p变量也可以添加到参数列表。
(defun foo (&key (a 0) (b 0b-supplied-p) (c (+ a b)))
(list a b c b-supplied-p))
(foo :a 1) → (1 0 1 NIL)
(foo :b 1) → (0 1 1 T)
(foo :b 1 :c 4) → (0 1 4 T)
(foo :a 2 :b 1 :c 4)→ (2 1 4 T)
如果想让关键字变得和变量名不同,可以用一个包含替换变量名的关键字表。例如foo的定义:
(defun foo (&key ((:apple a)) ((:box b)0) ((:charlie c) 0 c-supplied-p))
(list a b c c-supplied-p))
调用时如下:
(foo :apple 10 :box 20 :charlie 30)→ (10 20 30 T)
在想隐藏内部实现细节,内部使用短变量名,但是在API声明中使用有描述性的关键字时非常有用(这用处和C/C++中头文件定义声明时有类似效果)。
混用不同参数类型
在一个函数中,使用多个参数类型时有一定顺序要求:第1个是必须的参数requiredparameters,可选参数optionalparameters,变长参数restparameter,最后是关键字参数keywordparameters。一般用多个参数类型时,必须的参数可能与&optional和&rest结合使用。另外2个组合&optional或&rest与&key,会导致奇怪的行为。
&optional和&key结合使用会导致问题在于,调用者没为所有可选参数传递值,就会把关键字作为参数的值,并把关键字对应的值传递给关键字参数。例如:
(defun foo (x &optional y &key z)(list x y z))
如下调用时没问题:
(foo 1 2 :z 3) → (1 2 3)
这样也是对的:
(foo 1) → (1 nil nil)
但是这样就会出错:
(foo 1 :z 3) → ERROR
这是因为关键字:z被当做可选参数y的值,只留下参数3。在赋值关键字参数时,发现既不是键值对或者没东西就给错误。这还不是最坏的情况,如果这个函数有2个&optional参数,最后一次调用方式中:z和3会分别绑定到2个&optional参数而&key参数的z会得到默认值NIL,没有任何提示说赋值不正确(因为:z 3的目的在于对关键字参数赋值,然而却意外的赋给了2个可选参数)。
一般的,如果发现有函数同时用到&optional和&key参数,应该把所有的都改成&key参数——更灵活,可以增加新关键字参数而不必担心现有调用该函数的地方,也可以移除掉未用到的关键字参数。这样会让代码更容易维护和扩展。——如果想向函数增加新行为需要添加参数,可以增加关键字参数而不需要修改,甚至不用重新编译,现有使用该函数的代码。
可以放心的使用&rest和&key参数,但是结果有点奇怪。通常&rest或&key在参数表中把在必须和&optional后面的值按特殊的方式赋值——收集到一个表作为&rest参数或者按关键字匹配给&key参数。如果&rest和&key同时存在,那么这2个事都会发生——所有后面的值,包括关键字自身,整合到一个表绑定到&rest参数,并且匹配的值绑定到&key参数。例如:
(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)
函数返回值
用RETURN-FROM特殊操作符可以在函数中立即返回任何值。
在第20章会讲到RETURN-FROM实际上和函数没有关系;它是用在由BLOCK特殊操作符定义的块结构代码中返回的。DEFUN自动把函数体包围在一个和函数名一样名字的代码块中。传递给RETURN-FROM的是函数名和返回值,在它求值时,就会退出函数返回对应的值。RETURN-FROM的第1个“参数”是从哪个块返回的块名。这个名字不会被求值所以不用单引号。
下面这个函数用嵌套循环找2个小于10的数的乘积小于参数的,第1组数字,在找到时用RETURN-FROM返回。
(defun foo (n)
(dotimes (i 10)
(dotimes (j 10)
(when (> (* i j) n)
(return-from foo (list i j))))))
对指定函数名是一个麻烦的事情,在改变函数名的时候也要同样修改RETURN-FROM的地方。不过它不像C系列语言中return一样的频繁使用,因为所有Lisp表达式包括控制结构循环和条件语句,都会返回一个值。所以在实际使用中不是很大问题。
函数当数据,或者高阶函数
在Lisp中函数只是一种类型的对象。在用DEFUN定义函数的时候,实际上做了2件事情:创建一个新的函数对象给了一个名字。也可能是,在第3章中用LAMBDA表达式创建一个匿名函数。实际上都是函数对象,有名字或者匿名,在原生编译的Lisp中,它可能包含很多机器码。需要知道的是如何持有它,如何调用它。
特殊操作符FUNCTION可以取得函数对象。它需要1个名字并返回该名字的函数。名字不要是非求值的。如果定义一个foo函数:
CL-USER> (defun foo (x) (* 2 x))
FOO
这样取得函数对象:
CL-USER> (function foo)
#<Interpreted Function FOO>
实际上在第3章已经用到了FUNCTION,语法#'是FUNCTION的简写,如同'是QUOTE的简写一样。也可以这样取得foo的函数对象:
CL-USER> #'foo
#<Interpreted Function FOO>
得到函数对象后,可以用FUNCALL和APPLY来调用函数。它们在如何获取参数并传递给函数上面有点区别。
FUNCALL用在知道参数数量的情况。FUNCALL的第1个参数是调用的函数对象,剩下的参数都是传递给那个函数的。所以下面这2个表达式是一样的:
(foo 1 2 3) ≡ (funcall #'foo 1 2 3)
下面的函数展示,由参数传递函数对象。按step步进,用从min到max的值调用函数。根据函数返回值绘制*。
(defun plot (fn min max step)
(loop for i from min to max by step do
(loop repeat (funcall fn i) do (format t "*"))
(format t "~%")))
FUNCALL根据i计算值。LOOP则根据该值决定绘制*。
这里不没通过FUNCTION或者#'对fn取得函数对象;认为它是一个变量,变量的值就是函数对象。可以传递任何单数字参数的函数给plot,比如内建函数EXP返回e为底的指数。
CL-USER> (plot #'exp 0 4 1/2)
*
*
**
****
*******
************
********************
*********************************
******************************************************
NIL
FUNCALL对运行时才知道变量的情况处理的不好。如果一个表包含函数对象,最小值和最大值,步进值。假定变量plot-data就是这个表,想要把这些值传递给plot。可能会像下面这样:
(plot (first plot-data) (second plot-data)(third plot-data) (fourth plot-data))
这样没问题,但是很麻烦的需要把表解开参数逐个传递给plot。
该轮到APPLY登场了。同FUNCALL,第1个参数是函数对象。但是后面不必一一指定参数,而是用一个表。换做下面的写法:
(apply #'plot plot-data)
为了方便,APPLY也可以在最后的表中“缺少”参数。所以如果plot-data只包含min,mix和step的值,仍可以这样用APPLY绘制EXP函数的范围:
(apply #'plot #'exp plot-data)
APPLY不关心函数参数是&optional,&rest或&key参数——只要最终表对函数声明是合法的,有足够的必须参数和只是匹配的关键字参数。
匿名函数
只要开始把函数当作参数使用时,就会发现定义命名一个函数很麻烦,很可能它只在一个地方使用,甚至都不会用名字去调用它。
这样用DEFUN有点小题大做,可以用LAMBDA表达式创建“匿名”"anonymous"函数。和在第3章中提到的一样LAMBDA表达如下:
(lambda (parameters) body)
对LAMBDA表达式的一种理解是,认为它是一种特殊的函数,它的名字就是它自己做的事。这可以解释对与#'使用时把LAMBDA表达式当函数名来使用。
(funcall #'(lambda (x y) (+ x y)) 2 3)→ 5
甚至可以把LAMBDA表达式当作有“名字”的函数来进行函数调用。也可以用FUNCALL。
((lambda (x y) (+ x y)) 2 3)→ 5
匿名函数在需要把简单的、内联的函数当参数传递给其他函数时很有用。比如,需要向plot传递2x的函数,可能像下面这样定义:
(defun double (x) (* 2 x))
传递给plot。
CL-USER> (plot #'double 0 10 1)
**
****
******
********
**********
************
**************
****************
******************
********************
NIL
但是跟简单和清晰的写法:
CL-USER> (plot #'(lambda (x) (* 2 x)) 010 1)
**
****
******
********
**********
************
**************
****************
******************
********************
NIL
使用LAMBDA表达式另外一个重要的影响是构建闭包closures,函数会维护创建它时的环境。在第3章中用到了一点闭包,具体细节需要在了解变量之后,将在后面章节讨论。