format,不僅是格式化

《實戰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)的基本用法。首先,它須要至少兩個參數:佈局

  • 第一個參數控制了format將會把格式化後的字符串打印到什麼地方。t表示打印到標準輸出;
  • 第二個參數則是本文的主角,名爲控制字符串(control-string)。它指導format如何格式化。

聽起來很神祕,但其實跟C語言的fprintf也沒什麼差異。編碼

在控制字符串中,通常會有許多像佔位符通常的命令(directive)。正如Python的format方法中,有各式各樣的format_spec可以格式化對應類型的數據,控制字符串中的命令也有不少種,常見的有:rest

  • 打印二進制數字的~B,例如(format t "~B" 5)會打印出101;
  • 打印八進制數字的~O,例如(format t "~O" 8)會打印出10;
  • 打印十進制數字的~D;
  • 打印十六進制數字的~X,例如(format t "~X" 161)會打印出A1;
  • 打印任意一種類型的~A,通常打印字符串的時候會用到。

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

相關文章
相關標籤/搜索