正如 Bicameral, Not Homoiconic 一文所述(推薦先看完 Bicameral 再看我這裡寫的東西),真正讓 LISP 脫穎而出的功能,是它 pipeline 特殊的雙層解析器:
- scan 出詞素(token)
- 用 reader 讀出「資料」,通常是一種 form,以 LISP 來說就是 s-expression。而且 reader 會順便讀取 macro 定義跟把 macro 展開
- 最後用 parser 解析成表面語言
根據程式語言的設計會影響實現,有些實現方式會非常多的靜態檢查,以至於在這些實現內部消除了絕大多數的語意錯誤,像是 dependent typed 的語言,這時候我們幾乎可以相信這些編譯器的內部表示跟我們等下提到的客觀語法真的一樣。要注意這並不總是成立,也有很多語意錯誤會被延遲到執行時再處理(如 Python、Racket 的情況),所以在程式語言的實現中,總是假設並依賴一個理想的客觀語法,是由我們規定的語意生成的,我們通常用化簡語意或是指稱語意去研究,而編譯器很大一部分工作就是保證遵循這個語意來實現語言。一套客觀語法可以有很多種檢視方式,比如「指定 a 的新值是整數 1」這個客觀語法可以寫成
- Racket
(set-box! a 1) - C
a = 1; - OCaml
a := 1
更瘋狂一點的話,也不是不能寫成 JSON
{
"assign": {
"variable": "a",
"expression": {
"type": "int",
"val": 1
}
}
}
為什麼表面語法有可能是錯的呢?比如 OCaml 的 parser 不會拒絕 a := 1 的寫法,即使 a 不是 int ref 類型。要到 type checker 的檢查階段,這段程式碼才會因為類型錯誤而被拒絕(這樣就消除了一個語意不正確的表面語句)。
所以一般開發者說的語法,其實可以說是「檢視語法」,當然這並不是說檢視不重要,正如 Concrete syntax matters, actually 說的,好的檢視方式本身就揭示了我們在談論什麼:我當然可以用 * 表示加法、用 + 表示乘法,然後問 3 + 2 * 5 是什麼,但如果有人算錯,我不應該太意外。
所以之所以要視之為檢視,是因為要讓我們可以去想像更好的表示方式,比如
「檢視」暗示了更豐富的運用,像在 proof assistant 的應用中,我們經常想要知道當前 context 可以看到的定義,以及打算證明的目標的類型;在除錯時我們想看到執行期的堆疊等等,都可以考慮為與跟生成客觀語法的語意匹配的檢視方式。
最後,釐清一些常見的誤解
- Bicameral 語法不一定要是 s-expression: Rhombus 使用 Shrubbery Notation
- 不是所有 LISPy 語言都用 list 表示其資料層,Racket 就用了 syntax object 抽象(Bicameral 一文也有說這件事)