Practical Common Lisp学习笔记——之第5章函数

5.函数

 

定义新函数

一般用DEFUN宏定义函数,声明如下:

 

(defun name (parameter*)

 "Optional documentation string."

 body-form*)

 

一般函数名只包含字母字符和连字符,其他字符也会在明确用途时使用。如果一个函数是由一个类型转换到另一类型时会用->,比如:字符串转换控件string->widget。命名规则中用连字符比下划线和首字母大写更好,frob-widgetfrob_widgetfrobWidget更符合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个参数为xy,有文档,函数体有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)

 

调用时仍会效验参数数目是否在24之间,如果小于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,096536,870,911(常量CALL-ARGUMENTS-LIMIT)

 

&rest后面的参数,会把后面所有的参数都放到一个表中作为该参数的值。所以FORMAT+看起来像这样:

 

(defun format (stream string &restvalues) ...)

(defun + (&rest numbers) ...)

 

关键字参数

假如:有一个函数有4个可选参数。调用者想给第1个参数提供值是没问题的。但是他如果只想给第24个参数提供值。这就出现问题了,因为可选参数是按照位置定义的,所以之前的参数都必须指定值,第13个参数就会成为必须的参数。

 

关键字参数允许调用者为某个参数指定值。同可选参数类似的&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参数,最后一次调用方式中:z3会分别绑定到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>

 

得到函数对象后,可以用FUNCALLAPPLY来调用函数。它们在如何获取参数并传递给函数上面有点区别。

 

FUNCALL用在知道参数数量的情况。FUNCALL的第1个参数是调用的函数对象,剩下的参数都是传递给那个函数的。所以下面这2个表达式是一样的:

 

(foo 1 2 3) (funcall #'foo 1 2 3)

 

下面的函数展示,由参数传递函数对象。按step步进,用从minmax的值调用函数。根据函数返回值绘制*

 

(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只包含minmixstep的值,仍可以这样用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章中用到了一点闭包,具体细节需要在了解变量之后,将在后面章节讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
疫情居家办公系统管理系统按照操作主体分为管理员和用户。管理员的功能包括办公设备管理、部门信息管理、字典管理、公告信息管理、请假信息管理、签到信息管理、留言管理、外出报备管理、薪资管理、用户管理、公司资料管理、管理员管理。用户的功能等。该系统采用了MySQL数据库,Java语言,Spring Boot框架等技术进行编程实现。 疫情居家办公系统管理系统可以提高疫情居家办公系统信息管理问题的解决效率,优化疫情居家办公系统信息处理流程,保证疫情居家办公系统信息数据的安全,它是一个非常可靠,非常安全的应用程序。 管理员权限操作的功能包括管理公告,管理疫情居家办公系统信息,包括外出报备管理,培训管理,签到管理,薪资管理等,可以管理公告。 外出报备管理界面,管理员在外出报备管理界面中可以对界面中显示,可以对外出报备信息的外出报备状态进行查看,可以添加新的外出报备信息等。签到管理界面,管理员在签到管理界面中查看签到种类信息,签到描述信息,新增签到信息等。公告管理界面,管理员在公告管理界面中新增公告,可以删除公告。公告类型管理界面,管理员在公告类型管理界面查看公告的工作状态,可以对公告的数据进行导出,可以添加新公告的信息,可以编辑公告信息,删除公告信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值