iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0

很多開發者開始認真寫 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)))))

第二種排版方式比較容易閱讀,但是衍生了其它的問題:「手寫程式碼時,如何可以確保括弧總是成對呢?」

接下來,我們會一邊拆解問題、同時介紹輔助插件的功能。

括弧成對問題

這個問題主要有兩個層次:

  1. 閱讀程式碼時,如何一眼看出括弧之間的成對關系。
  2. 編寫程式碼時,如何確保總是可以讓括弧成對。

彩虹括弧

day02 時有透過 vim-plug 安裝這個插件:

" rainbow parentheses
Plug 'frazrepo/vim-rainbow'

參考下圖,有了這個插件,括弧之間的前後對應就非常清楚。
https://ithelp.ithome.com.tw/upload/images/20250909/2016186964FZ9VfRHh.png

在 Lisp 社群,對於這個插件其實有不同的看法,也有人認為寫 Lisp 久了之後,其實閱讀程式碼時的重點,注意力並不是放在括弧上,其實沒有必要。彩虹括弧插件反而有分散注意力、過多視覺雜訊的缺點。

另一方面,考慮到初學者起步時的既有思考模式,我認為這個還是相當必要。

括弧自動配對

對應的插件:

Plug 'jiangmiao/auto-pairs', { 'tag': 'v2.0.0' }

它可以讓你每次輸入左括弧時,右括弧就會自動產生。而且無論你輸入的是:圓括弧、方括弧、大括弧、字串的 Quote 符號。換言之,一旦啟用了這個之後,你的括弧就會被強制成對了。

括弧編輯問題

多年之前,我剛把『括弧自動配對』插件安裝好之後,我反而覺得寫程式超級卡了。光是兩個常見的編輯不順,就一度讓我快要放棄『括弧自動配對』。

  1. 括弧不成對時,難以修正。
  2. 難以直接將一個括弧圍繞到一個打好的單字 (word) 上。

括弧不成對

由於寫程式時,總是會有需要「複製、貼上」的時候,這時會用到 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 插件所定義的概念,定義有點長,但是讀者記憶定義裡的三條規則即可:

  • 複合形式 (compound form) 是指由成對的 ()[]{} 所界定的文字區塊。
  • 若游標位於字串中,元素 (element) 即為目前字串。
  • 若游標位於成對的結構化括弧上,元素即為當前的複合形式

要使用上述的指令,需要的插件是:

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 語言。


上一篇
Lisp 深入淺出 -- 互動式開發
系列文
在 Neovim 中探索 Fennel 與函數式編程9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言