《实战Common Lisp》系列主要讲述在使用Common Lisp时能派上用场的小函数,但愿能为Common Lisp的复兴作一些微小的贡献。MAKE COMMON LISP GREAT AGAIN。序言
写了一段时间的Python后,总以为它跟Common Lisp(下文简称CL)有亿点点像。例如,Python和CL都支持可变数量的函数参数。在Python中写做html
def foo(* args): print(args)
而在CL中则写成python
(defun foo (&rest args) (print args))
Python的语法更紧凑,而CL的语法表意更清晰。此外,它们也都支持关键字参数。在Python中写成编程
def bar(*, a=None, b=None): print('a={}\tb={}'.format(a, b))
而在CL中则是数组
(defun bar (&key (a nil) (b nil)) (format t "a=~A~8Tb=~A~%" a b))
尽管CL的&key仍然更清晰,但声明参数默认值的语法确实是Python更胜一筹。编程语言
细心的读者可能发现了,在Python中有一个叫作format的方法(属于字符串类),而在CL则有一个叫作format的函数。而且,从上面的例子来看,它们都负责生成格式化的字符串,那么它们有类似之处吗?ide
答案是否认的,CL的format简直就是格式化打印界的一股泥石流。函数
format的基本用法不妨从上面的示例代码入手介绍CL中的format(下文在不引发歧义的状况下,简称为format)的基本用法。首先,它须要至少两个参数:布局
听起来很神秘,但其实跟C语言的fprintf也没什么差异。编码
在控制字符串中,通常会有许多像占位符通常的命令(directive)。正如Python的format方法中,有各式各样的format_spec可以格式化对应类型的数据,控制字符串中的命令也有不少种,常见的有:rest
另外,format的命令也支持参数。在Python中,能够用下列代码打印右对齐的、左侧填充字符0的、二进制形式的数字5
print('{:0>8b}'.format(5))
format函数也能够作到一样的事情
(format t "~8,'0B" 5)
到这里为止,你可能会以为format的控制字符串,不过就是将花括号去掉、冒号换成波浪线,以及参数语法不同的format方法的翻版罢了。
接下来,让咱们进入format的黑科技领域。
format的高级用法前面列举了打印2、8、十,以及十六进制的命令,但format还支持其它的进制。使用命令~R搭配参数,format能够打印数字从2到36进制的全部形态。
(format t "~3R~%" 36) ; 以 3进制打印数字36,结果为1100 (format t "~5R~%" 36) ; 以 5进制打印数字36,结果为 121 (format t "~7R~%" 36) ; 以 7进制打印数字36,结果为 51 (format t "~11R~%" 36) ; 以11进制打印数字36,结果为 33 (format t "~13R~%" 36) ; 以13进制打印数字36,结果为 2A (format t "~17R~%" 36) ; 以17进制打印数字36,结果为 22 (format t "~19R~%" 36) ; 以19进制打印数字36,结果为 1H (format t "~23R~%" 36) ; 以23进制打印数字36,结果为 1D (format t "~29R~%" 36) ; 以29进制打印数字36,结果为 17 (format t "~31R~%" 36) ; 以31进制打印数字36,结果为 15
之因此最大为36进制,是由于十个阿拉伯数字,加上二十六个英文字母正好是三十六个。那若是不给~R加任何参数,会使用0进制吗?非也,format会把数字打印成英文单词
(format t "~R~%" 123) ; 打印出one hundred twenty-three
甚至可让format打印罗马数字,只要加上@这个修饰符便可
(format t "~@R~%" 123) ; 打印出CXXIII
天晓得为何要内置这么冷门的功能。
你,做为一名细心的读者,可能留意到了,format的~X只能打印出大写字母,而在Python的format方法中,{:x}能够输出小写字母的十六进制数字。即便你在format函数中使用~x也是无效的,由于命令是大小写不敏感的(case insensitive)。
那要怎么实现打印小写字母的十六进制数字呢?答案是使用新的命令~(,以及它配套的命令~)
(format t "~(~X~)~%" 26) ; 打印1a
配合:和@修饰符,一共能够实现四种大小写风格
(format t "~(hello world~)~%") ; 打印hello world (format t "~:(hello world~)~%") ; 打印Hello World (format t "~@(hello world~)~%") ; 打印Hello world (format t "~:@(hello world~)~%") ; 打印HELLO WORLD
在Python的format方法中,能够控制打印出的内容的宽度,这一点在“format的基本用法”中已经演示过了。若是设置的最小宽度(在上面的例子中,是8)超过了打印的内容所占据的宽度(在上面的例子中,是3),那么还能够控制其采用左对齐、右对齐,仍是居中对齐。
在CL的format函数中,不论是~B、~D、~O,仍是~X,都没有控制对齐方式的选项,数字老是右对齐。要控制对齐方式,须要用到~<和它配套的~>。例如,下面的CL代码可让数字在八个宽度中左对齐
(format t "|~8<~B~;~>|" 5)
打印内容为|101 |。~<跟前面提到的其它命令不同,它不消耗控制字符串以后的参数,它只控制~<和~>之间的字符串的布局。这意味着,即便~<和~>之间是字符串常量,它也能够起做用。
(format t "|~8,,,'-<~;hello~>|" 5)
上面的代码运行后会打印出|---hello|:8表示用于打印的最小宽度;三个逗号(,)之间为空,表示忽略~<的第二和第三个参数;第四个参数控制着打印结果中用于填充的字符,因为-不是数字,所以须要加上单引号前缀;~;是内部的分隔符,因为它的存在,hello成了最右侧的字符串,所以会被右对齐。
若是~<和~>之间的内容被~;分隔成了三部分,还能够实现左对齐、居中对齐,以及右对齐的效果
(format t "|~24<left~;middle~;right~>|") ; 打印出|left middle right|
一般状况下,控制字符串中的命令会消耗参数,好比~B和~D等命令。也有像~<这样不消耗参数的命令。但有的命令甚至能够作到“一参多用”,那就是~*。好比,给~*加上冒号修饰,就可让上一个被消耗的参数从新被消耗一遍
(format t "~8D~:*~8D~8D~%" 1 2) ; 打印出 1 1 2
在~8D消耗了参数1以后,~:*让下一个被消耗的参数从新指向了1,所以第二个~8D拿到的参数仍然是1,最后一个拿到了2。尽管控制字符串中看起来有三个~D命令而参数只有两个,却依然能够正常打印。
在format的文档中一个不错的例子,就是让~*和~P搭配使用。~P能够根据它对应的参数是否大于1,来打印出字母s或者什么都不打印。配合~:*就能够实现根据参数打印出单词的单数或复数形式的功能
(format t "~D dog~:*~P~%" 1) ; 打印出1 dog (format t "~D dog~:*~P~%" 2) ; 打印出2 dogs
甚至你能够组合一下前面的毕生所学
(format t "~@(~R dog~:*~P~)~%" 2) ; 打印出Two dogs
命令~[和~]也是成对出现的,它们的做用是选择性打印,不过比起编程语言中的if,更像是取数组某个下标的元素
(format t "~[~;one~;two~;three~]~%" 1) ; 打印one (format t "~[~;one~;two~;three~]~%" 2) ; 打印two (format t "~[~;one~;two~;three~]~%" 3) ; 打印three
但这个特性还挺鸡肋的。想一想,你确定不会平白无故传入一个数字来做为下标,而这个做为下标的数字极可能自己就是经过position之类的函数计算出来的,而position就要求传入待查找的item和整个列表sequence,而为了用上~[你还得把列表中的每一个元素硬编码到控制字符串中,很有南辕北辙的味道。
给它加上冒号修饰符以后却是有点用处,好比能够将CL中的真(NIL之外的全部对象)和假(NIL)打印成单词true和false
(format t "~:[false~;true~]" nil) ; 打印false
圆括号和方括号都用了,又怎么能少了花括号呢。没错,~{也是一个命令,它的做用是遍历列表。例如,想要打印出一个列表中的每一个元素,而且两两之间用逗号和空格分开的话,能够用下列代码
(format t "~{~D~^, ~}" '(1 2 3)) ; 打印出1, 2, 3
~{和~}之间也能够有不止一个命令,例以下列代码中每次会消耗列表中的两个元素
(format t "{~{\"~A\": ~D~^, ~}}" '(:a 3 :b 2 :c 1))
打印结果为{"A": 3, "B": 2, "C": 1}。若是把这两个format表达式拆成用循环写的、不使用format的等价形式,大约是下面这样子
; 与(format t "~{~D~^, ~}" '(1 2 3))等价 (progn (do ((lst '(1 2 3) (cdr lst))) ((null lst)) (let ((e (car lst))) (princ e) (when (cdr lst) (princ ", ")))) (princ #\Newline)) ; 与(format t "{~{\"~A\": ~D~^, ~}}" '(:a 3 :b 2 :c 1))等价 (progn (princ "{") (do ((lst '(:c 3 :b 2 :a 1) (cddr lst))) ((null lst)) (let ((key (car lst)) (val (cadr lst))) (princ "\"") (princ key) (princ "\": ") (princ val) (when (cddr lst) (princ ", ")))) (princ "}") (princ #\Newline))
这么看来,~{确实可让使用者写出更紧凑的代码。
在前面的例子中,尽管用~R搭配不一样的参数能够将数字打印成不一样进制的形式,但毕竟这个参数是固化在控制字符串中的,局限性很大。例如,若是我想要定义一个函数print-x-in-base-y,使得参数x能够打印为y进程的形式,那么也许会这么写
(defun print-x-in-base-y (x y) (let ((control-string (format nil "~~~DR" y))) (format t control-string x)))
但format的灵活性,容许使用者将命令的前缀参数也放到控制字符串以后的列表中,所以能够写成以下更简练的实现
(defun print-x-in-base-y (x y) (format t "~VR" y x))
并且不仅一个,你能够把全部参数都写成参数的形式
(defun print-x-in-base-y (x &optional y &rest args &key mincol padchar commachar commainterval) (declare (ignorable args)) (format t "~V,V,V,V,VR" y mincol padchar commachar commainterval x))
恭喜你从新发明了~R,并且还不支持:和@修饰符。
要在CL中打印形如2021-01-29 22:43这样的日期和时间字符串,是一件比较麻烦的事情
(multiple-value-bind (sec min hour date mon year) (decode-universal-time (get-universal-time)) (declare (ignorable sec)) (format t "~4D-~2,'0D-~2,'0D ~2,'0D:~2,'0D~%" year mon date hour min))
谁让CL没有内置像Python的datetime模块这般完善的功能呢。不过,借助format的~/命令,咱们能够在控制字符串中写上要调用的自定义函数,来深度定制打印出来的内容。以打印上述格式的日期和时间为例,首先定义一个后续要用的自定义函数
(defun yyyy-mm-dd-HH-MM (dest arg is-colon-p is-at-p &rest args) (declare (ignorable args is-at-p is-colon-p)) (multiple-value-bind (sec min hour date mon year) (decode-universal-time arg) (declare (ignorable sec)) (format dest "~4D-~2,'0D-~2,'0D ~2,'0D:~2,'0D~%" year mon date hour min)))
而后即可以直接在控制字符串中使用它的名字
(format t "~/yyyy-mm-dd-HH-MM/" (get-universal-time))
在个人机器上运行的时候,打印内容为2021-01-29 22:51。
后记format能够作的事情还有不少,CL的HyperSpec中有关于format函数的详细介绍,CL爱好者必定不容错过。
最后,其实Python跟CL并不怎么像。往往看到Python中的__eq__、__ge__,以及__len__等方法的巧妙运用时,身为一名Common Lisp爱好者,我都会流露出羡慕的神情。纵然CL被称为可扩展的编程语言,这些平凡的功能却依旧没法方便地作到呢。