很多開發者開始認真寫 Lisp 不久,就深深感受到編輯括弧的不便。首先第一個問題:「這麼多括弧,該怎麼排版呢?」
不暪各位說,很多年我剛開始寫 Lisp 時,我排版程式碼是像下方這樣子。
(fn factorial [x]
(if (< x 2)
1
(* x (factorial (- x 1
)
)
)
)
)
上述的排版方式方便寫程式時,用肉眼去對齊括弧。另一方面,閱讀程式碼時,大大地增加了雜訊,而且讓行數大幅增加,當然是不理想的。 Lisp 社群的標準排版方式則是下方的排法。
(fn factorial [x]
(if (< x 2)
1
(* x (factorial (- x 1)))))
第二種排版方式比較容易閱讀,但是衍生了其它的問題:「手寫程式碼時,如何可以確保括弧總是成對呢?」
接下來,我們會一邊拆解問題、同時介紹輔助插件的功能。
這個問題主要有兩個層次:
在 day02 時有透過 vim-plug 安裝這個插件:
" rainbow parentheses
Plug 'frazrepo/vim-rainbow'
參考下圖,有了這個插件,括弧之間的前後對應就非常清楚。
在 Lisp 社群,對於這個插件其實有不同的看法,也有人認為寫 Lisp 久了之後,其實閱讀程式碼時的重點,注意力並不是放在括弧上,其實沒有必要。彩虹括弧插件反而有分散注意力、過多視覺雜訊的缺點。
另一方面,考慮到初學者起步時的既有思考模式,我認為這個還是相當必要。
對應的插件:
Plug 'jiangmiao/auto-pairs', { 'tag': 'v2.0.0' }
它可以讓你每次輸入左括弧時,右括弧就會自動產生。而且無論你輸入的是:圓括弧、方括弧、大括弧、字串的 Quote 符號。換言之,一旦啟用了這個之後,你的括弧就會被強制成對了。
多年之前,我剛把『括弧自動配對』插件安裝好之後,我反而覺得寫程式超級卡了。光是兩個常見的編輯不順,就一度讓我快要放棄『括弧自動配對』。
由於寫程式時,總是會有需要「複製、貼上」的時候,這時會用到 Neovim 的 Normal Mode 指令。但是,括弧自動配對插件,它管理的只有 Insert Mode 指令。換言之,一旦我用了「複製、貼上」,一不小心,就會讓括弧不成對。可是,這種時候,因為手動輸入的括弧又一定成對,就會卡在原地。
解法是用 Normal Mode 的指令 x
去刪除游標所在的字元,這樣子就可以直接刪去不成對的括弧。
圍繞括弧的需求是什麼呢?比方說,如果我已經打好了一個單字 local,打完之後才想到要輸入括弧,這就遲了。
local
因為『括弧自動配對』的關系,無論我在 local 之前或是之後輸入插弧,它都會強制一次輸入一對括弧。
()local
在我剛開始學 Lisp 的頭半年,我都是對右括弧 )
下 slurp 指令,叫它把右邊的單字吃下去。我就這樣子長達半年,全靠 slurp 和 barf 權宜處理所有的這類問題。
後來,我才發現其實有優雅的解法:下指令圍繞括弧。
cseb
:對游標所在的元素 (element),圍繞圓括弧。cse[
:對游標所在的元素,圍繞方括弧。cse{
:對游標所在的元素,圍繞大括弧。dsf
:對游標所在的元素,刪除圍繞元素的括弧 (無論是哪一種括弧)。什麼是元素呢?元素是 vim-sexp-mappings-for-regular-people
插件所定義的概念,定義有點長,但是讀者記憶定義裡的三條規則即可:
()
、[]
或 {}
所界定的文字區塊。要使用上述的指令,需要的插件是:
Plug 'guns/vim-sexp'
Plug 'tpope/vim-sexp-mappings-for-regular-people'
Plug 'kylechui/nvim-surround'
眼尖的讀者可能會發現:vim-sexp
這個關鍵字。沒錯,上面三組,它們就是 S 表達式編輯的輔助插件。還有,其實前半年我也一直都有在用 S 表達式編輯,但是我只會用 slurp 和 barf。
這邊再另外介紹三組常用指令:
在 Linux shell 時,可以如此移動游標:
CTRL + a
:移動到指令的行首CTRL + e
:移動到指令的行尾在 Neovim 裡編輯單行時,可以如此移動游標:
0
:移動到行首$
:移動到行尾而一旦進入了 Lisp 的世界,我們對程式碼的觀點,基礎的單位就不再只是程式碼行 (line)、或是英文字 (word)。最重要的基礎單位之一,是一個又一個的 S 表達式。
在編輯 S 表達式時,可以如此移動游標:
(
:移動到 S 表達式的左括弧。)
:移動到 S 表達式的右括弧。%
:左括弧跳右括弧,或是右括弧跳左括弧。>e
:右移游標所在的元素。<e
:左移游標所在的元素。這個功能在寫條件判斷 if
時,特別地有用。有時候,我們會想要交換 true branch 和 false branch 。下一個 >e
指令在 branch 的元素上,就完成交換了。
我們也可以把之前 vim-sexp-mappings-for-regular-people
插件所定義的元素與複合形式當作被刪除的標的物。
dae
:刪掉游標所在的元素。daf
:刪掉游標所在的複合形式。刪掉之後,移到游標到別的地方,按下貼上指令 p
,被刪掉的元素或是複合形式就會在新的地方貼上。
讀者可能想問:「到底 S 表達式編輯好在哪?有沒有什麼概念性的解釋?」
工程師在開發軟體時,本來腦子裡就有語法樹的概念,比方說,我們會想要交換條件判斷的 true branch 和 false branch ,每一個 branch 就是一個子語法樹。然而,不幸的事情是,在多數的時候,編輯器並沒有辦法跟人類一樣地去解析語法樹。也因此,寫程式時,就算腦子裡想的事情是極度高階的語意:「交換 true/false branch」,我們還是得先將高階語意轉化為低階的編輯器指令,才能完成編輯。
由於 Lisp 語法本身就反映了程式碼的語法樹結構,這個特殊的程式語言設計,就讓編輯器插件可以簡單地解析 Lisp 的語法樹,也因此讓高階編輯指令 (即 S 表達式編輯) 化為可能。
不是 Lisp 的話,編輯程式碼,我們是在行 (line)、單字 (word)、字元 (character) 裡來來去去 (註1);然而,在 Lisp 的話,編輯程式碼,我們可以在語法樹裡穿梭編輯。
從設計的角度來講,我們也可以說,Lisp 與 S 表達式編輯也是一種 UNIX 哲學的體現:
Rule of Representation: Fold knowledge into data so program logic can be stupid and robust.
Lisp 開發者常遇到的挑戰,主要圍繞著括弧成對和括弧編輯。
為了克服這些問題,本文介紹了兩大類實用的編輯器工具與技巧:
自動化括弧管理:透過彩虹括弧和括弧自動配對等編輯器插件,可以讓括弧關係一目了然,並自動確保其成對,解決了視覺和輸入的困擾。
高階 S 表達式編輯:不同於傳統的逐行或逐字編輯,Lisp 允許我們直接在語法樹的層次上進行操作。藉由圍繞括弧、游標快速移動、移動元素和刪除元素等指令,編輯過程變得更直覺、更有效率。
若說高階程式語言提升了程式碼的表達力,那麼 Lisp 則進一步提升了編輯器的表達力,讓開發者得以在語法樹的層次自由地穿梭與操作。
註1:現代的 IDE 在使用了 Tree-sitter 之類的高級函式庫之後,也能取得程式碼的語法樹,甚至做出類似穿梭語法樹的效果。但是,在我寫這篇文章的時刻,Lisp 的 S 表達式編輯還是領先 non-Lisp 語言。