《實戰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被稱爲可擴展的編程語言,這些平凡的功能卻依舊沒法方便地作到呢。