Common Lisp的restart特性

主流的編程語(yǔ)言中,表示出現(xiàn)錯(cuò)誤的手段不外乎兩種:

  • 函數(shù)調(diào)用返回錯(cuò)誤碼
  • 函數(shù)調(diào)用拋出異常

C語(yǔ)言就屬于前者,它的fopen(3)函數(shù)在成功打開文件時(shí)返回一個(gè)FILE指針,失敗時(shí)返回NULL,并將錯(cuò)誤代碼寫入全局變量errno;Ruby語(yǔ)言屬于后者,它的File.open方法在找不到文件時(shí)拋出ENOENT的異常,在File.open之外包裹一層begin...rescue...end即可捕捉其拋出的異常。

Common Lisp的錯(cuò)誤機(jī)制叫做狀況系統(tǒng)(Condition System),與異常機(jī)制相似,可以實(shí)現(xiàn)拋出異常(使用函數(shù)error)和捕捉異常(使用宏handler-case)。但與其它語(yǔ)言的異常機(jī)制不同的地方在于,狀況系統(tǒng)有一種名為RESTART的特性,能夠由使用者、或者由代碼決定是否要、以及如何從錯(cuò)誤中恢復(fù)過(guò)來(lái)。聽起來(lái)很別致也很詭異,不妨繼續(xù)往下看。

假設(shè)我有一個(gè)Web框架,它提供了將日志記錄到文件中的功能。這個(gè)功能需要先調(diào)用名為init-logger的函數(shù)進(jìn)行初始化,該函數(shù)實(shí)現(xiàn)如下

(defun init-logger ()
  (with-open-file (s "/tmp/log/web.log"
                     :direction :output
                     :if-exists :supersede)
    (format s "Logger module starting...")))

這個(gè)函數(shù)被框架的初始化代碼所調(diào)用,如下

(defun init-framework ()
  (format t "Framework starting...~%")
  (init-plugin))

(defun init-plugin ()
  (format t "Plugins starting...~%")
  (init-logger))

而框架的初始化代碼則由應(yīng)用的初始化代碼所調(diào)用,如下

(defun init-app ()
  (format t "Application starting...~%")
  (init-framework))

如果在目錄/tmp/log不存在時(shí)調(diào)用init-app函數(shù),我將會(huì)收到一個(gè)錯(cuò)誤,說(shuō)明其無(wú)法找到/tmp/log/web.log這個(gè)文件。為了避免錯(cuò)誤打斷了應(yīng)用的正常啟動(dòng)流程,可以讓init-app函數(shù)負(fù)責(zé)創(chuàng)建這個(gè)用于存放日志文件的目錄。將init-app函數(shù)改寫為如下形式

(defun init-app ()
  (format t "Application starting...~%")
  (ensure-directories-exist "/tmp/log/")
  (init-framework))

盡管這種做法確實(shí)可行,但它導(dǎo)致應(yīng)用層的代碼(init-app函數(shù))必須了解位于底層的日志模塊的實(shí)現(xiàn)細(xì)節(jié)。直覺(jué)告訴我這樣子是不對(duì)的,框架的事情應(yīng)該由框架本身提供方案來(lái)解決。借助Common Lisp的restart功能,框架確實(shí)可以對(duì)外提供一種解決方案。

首先需要主動(dòng)檢測(cè)用于存放日志文件的目錄是否存在。借助UIOP這個(gè)包可以寫出如下代碼

(defun init-logger ()
  (let ((dir "/tmp/log/"))
    (unless (uiop:directory-exists-p dir)
      (error 'file-error :pathname dir))
    (with-open-file (s (format nil "~Aweb.log" dir)
                       :direction :output
                       :if-exists :supersede)
      (format s "Logger module starting..."))))

接著,init-logger需要主動(dòng)為調(diào)用者提供目錄不存在的問(wèn)題的解決方案,一種辦法就是當(dāng)這個(gè)目錄不存在時(shí),可以由調(diào)用者選擇是否創(chuàng)建。使用Common Lisp的restart-case宏,我將init-logger改寫為如下形式

(defun init-logger ()
  (let ((dir "/tmp/log/"))
    (restart-case
        (unless (uiop:directory-exists-p dir)
          (error 'file-error :pathname dir))
      (create-log-directory ()
        :report (lambda (stream)
                  (format stream "Create the directory ~A" dir))
        (ensure-directories-exist dir)))
    (with-open-file (s (format nil "~Aweb.log" dir)
                       :direction :output
                       :if-exists :supersede)
      (format s "Logger module starting..."))))

此時(shí)如果調(diào)用第一版的init-app函數(shù),那么init-logger仍將拋出異常(類型為file-error與之前一樣)并將我?guī)氲絊BCL(我用的是SBCL)的調(diào)試器中,但看到的內(nèi)容會(huì)稍有不同

error on file "/tmp/log"
   [Condition of type FILE-ERROR]

Restarts:
 0: [CREATE-LOG-DIRECTORY] Create the directory /tmp/log ;; <- 新增了這一行
 1: [RETRY] Retry SLIME REPL evaluation request.
 2: [*ABORT] Return to SLIME's top level.
 3: [ABORT] abort thread (#<THREAD "new-repl-thread" RUNNING {1001F958A3}>)

在Restarts中新增了名為create-log-directory的一項(xiàng),這正是在init-logger中通過(guò)restart-case定義的新的”恢復(fù)措施“。我輸入0觸發(fā)這個(gè)restart,Common Lisp會(huì)回到它被定義的restart-case宏相應(yīng)的子句中執(zhí)行其中的表達(dá)式,也就是調(diào)用CL:ENSURE-DIRECTORY-EXIST函數(shù)創(chuàng)建/tmp/log。

如果總是希望執(zhí)行CREATE-LOG-DIRECTORY這個(gè)選項(xiàng)來(lái)創(chuàng)建存放日志文件的目錄,可以直接在代碼中指定,只需要配合使用Common Lisp的handler-bindinvoke-restart函數(shù)即可,最終init-app函數(shù)的實(shí)現(xiàn)如下

(defun init-app ()
  (format t "Application starting...~%")
  (handler-bind
      ((file-error #'(lambda (c)
                       (declare (ignorable c))
                       (invoke-restart 'create-log-directory))))
    (init-framework)))
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容