每種程式語言都有形式語法。編譯器讀取它,構建剖析樹,拒絕任何不符合的內容。編輯器完全無視語法,讓你想輸入什麼就輸入什麼。
這種脫節是大量摩擦的驚人來源。自動完成建議的識別碼在上下文中毫無意義。語法突顯在重構中途崩潰。編輯器眼睜睜看著你犯錯,然後報出「Unexpected token」錯誤。我們將其視為正常,因為四十年來我們只有文字編輯器。
文字是一種中間表示。編譯器在剖析的那一刻就將其丟棄。那麼我們為什麼要編輯文字?
結構化編輯的實際含義
結構化編輯意味著真相來源是抽象語法樹,而不是字元 buffer。編輯器將樹渲染為文字以方便你使用,但每個操作都直接操縱節點。FunctionDeclaration、BinaryExpression、IfStatement。這些就是原子。
想新增參數?你不是把游標定位在括號之間然後輸入。你在 FunctionDeclaration 節點上呼叫「新增參數」操作。編輯器建立一個帶有預留位置的新 Parameter 節點。渲染的文字隨之更新。你永遠不會建立不平衡的括號,因為括號不是你輸入的字元。它們是渲染器產生的視覺邊界。
這不是科幻小說。自二十世紀六十年代以來,Lisp 就是這樣工作的,因為 S 表示式按定義就是樹。Smalltalk 將方法視為瀏覽器中的可選物件,而不是可 grep 的文字檔案。更近一些,Unison 語言將程式碼儲存為 AST 節點的 Merkle 樹,JetBrains MPS 是一種投影編輯器,語法同時定義了語言和編輯體驗。
結構安全保證
以下是樹編輯器在重構時所做的。這個 Python 指令碼近似其內部邏輯:
import ast
# The canonical representation is a tree, not text
tree = ast.parse("result = calculate(x, y)")
# Find the Call node and append a keyword argument
for node in ast.walk(tree):
if isinstance(node, ast.Call):
node.keywords.append(
ast.keyword(arg='debug', value=ast.Constant(value=True))
)
# Render to text only when needed (Python 3.9+)
print(ast.unparse(tree))
# result = calculate(x, y, debug=True)
該操作在結構上是安全的。你不會意外產生 calculate(x, y debug=True),因為 keywords 是一個型別化的 keyword 節點清單,而不是字串 buffer。渲染器插入逗號和等號。樹始終語法有效。
這就是大多數格式化工具和程式碼檢查工具已經工作的方式。Black、prettier 和 rustfmt 都剖析到 AST,轉換樹,然後輸出。區別在於它們讀取文字、轉換、再寫入文字。樹編輯器跳過了文字往返。
為什麼沒人使用樹編輯器
如果這這麼好,為什麼每個專業開發者仍然使用 VS Code、Vim 或 Emacs?
互動速度是第一個原因。文字編輯針對一個緊密迴圈進行了最佳化:你想到了更改,手指輸入字元,眼睛驗證。每次按鍵都是相同的操作。在樹編輯器中,「插入 if 陳述式」「重新命名變數」「提取方法」都是不同的手勢。你不能只是猛敲鍵盤。認知開銷會累積。
生態系統壁壘是第二個原因。流水線中的每個工具都假設是文字。git diff、grep、GitHub 程式碼審查、程式碼搜尋、網際網路上的每個教學。結構化格式存在,但沒有一個取代了原始碼的原始文字。網路效應是絕對的。
視覺渲染問題是第三個原因。程式設計師以數十年排版慣例的文字形式閱讀程式碼。當註解描述半個節點時,它放在哪裡?當使用者尚未完成指定其需求時,如何顯示操作中的樹?將樹渲染為可讀可編輯的文字比將文字渲染為樹更難。
文字編輯器中的結構化命令
你不需要切換編輯器就能獲得樹感知的好處。多種工具已經在 AST 上執行,同時呈現文字介面。
Lisp 的 Paredit 和 Smartparens 將 S 表示式視為樹。你可以將表示式「吞入」父作用域或將其「吐出」。括號仍然是文字,但編輯命令是樹操作。你可以在從未產生語法錯誤的情況下移動整個子樹。
現代 IDE 重構剖析你的程式碼,構建 AST,確定哪些變數成為參數,並重寫樹。當你在 IntelliJ 中執行「提取方法」時,它並不是在進行文字替換。它正在執行結構轉換並輸出結果。
語言伺服器為每個操作在內部構建 AST。轉到定義、重新命名和內嵌提示都遍歷樹。protocol為相容性傳遞文字位置,但智慧是結構性的。
如何在不更換編輯器的情況下實驗
如果你想縮小編寫程式碼的方式與編譯器理解程式碼的方式之間的差距,從這裡開始:
-
使用 Python 的
ast模組。 編寫一個指令碼,剖析真實程式碼,用ast.walk遍歷樹並進行轉換。大多數 Python 工具(flake8、mypy、Black)正是這樣做的。 -
探索 Tree-sitter。 在線上遊樂場中剖析檔案並檢查具體語法樹。看看剖析器實際看到了什麼。
-
嘗試 Lisp 的結構化編輯。 如果你寫 Clojure 或 Emacs Lisp,安裝 Paredit。強迫自己一整天按表示式而不是字元導航。這種緩慢本身就是教育性的。
文字檔案哪也不會去。工具太多,歷史太長,肌肉記憶太深。但文字編輯與結構理解之間的差距是缺陷、摩擦和認知負荷的真正來源。你不需要樹編輯器來修復它。你需要停止假裝文字是真實的東西。