軟體世界有許多規則與法則,其中一條最有趣的是 Kernighan 法則:

除錯的難度是撰寫程式碼的兩倍。因此,如果你寫的程式碼越聰明,從定義上來說,你就不夠聰明去除錯它。

我一直認為 Kernighan 法則是關於複雜度——保持你寫的程式碼盡可能簡單易懂。

現在有了大型語言模型(LLM),我發現這也與語言設計息息相關。

我仍然在 Twitter 上看到不少人偶爾抱怨他們嘗試過 AI 驅動的編碼流程,但輸出很糟糕,自己寫反而更快。雖然在 Opus 4.5 和 Gemini 3 的世界中這樣的人少了,但仍存在。每次看到這種情況,我都想知道他們在做什麼,使用哪些語言和函式庫。

目前軟體工程師的主要基準測試是 SWEBench(針對程式碼)和 TerminalBench(針對電腦任務)。基準測試應該代表所有編碼任務,因此重要的是要注意 SWEBench 專注於 Python。TerminalBench 涉及更多元的電腦任務,但當代理需要寫程式碼時,他們寫的也是 Python。

這些基本上是 Python 的基準測試。

那其他語言呢?幸運的是有 AutoCodeBench,不僅測試不同模型,還涵蓋 20 種不同的程式語言。結果如何?結果是這樣的:

我們被告知模型的好壞取決於訓練資料的多寡,因此擁有龐大訓練資料的語言應該表現最好,對吧?

事實證明,模型普遍在 Python 和 JavaScript 上表現相對較差。

表現最佳的語言(不論模型)是 C#、Racket、Kotlin,而第一名是 Elixir。

我已經使用 Elixir 作為主要語言好幾年了,所以我有明顯的偏見。但這指出了語言設計的一個關鍵,以及未來程式設計的趨勢。訓練資料的量並不像我們想像的那麼重要。函數式範式轉移得很好。結構勝過數量。JavaScript 有大量訓練資料,但架構不佳。Elixir 資料較少,但與架構相輔相成。

特斯拉押注於視覺,而其他人則在車頂裝雷射雷達。這是否天真?也許吧……人眼是廉價的感測器,容易被眩光、雨水和黑暗欺騙。但特斯拉押注於此,因為道路的混亂並非隨機,而是特定的人類方式,因為人類建造了它們:一世紀的視覺語法,內化於每個駕駛者,編碼於每個紅綠燈和路口。

特斯拉和 Figure 在機器人領域也押注同樣策略。人形外型、人形手、人體尺度的動作。這比輪子和夾爪更難設計。但人類為人類建造了世界。門是人寬,樓梯是人高,工具有符合手掌的握把。建造一個像我們一樣移動的機器人,千年的基礎建設就自動到位。

這策略優化了複雜性已存在的地方。對於汽車和機器人來說,是視覺和手,因為我們的物理世界是為眼睛和手指設計的。

對軟體而言,複雜性其實不在程式碼本身。程式碼只是實作。複雜性往往存在於用英文書寫的想法中:需求文件、錯誤報告、介面規格、審計日誌。人類用語言指定意圖並驗證結果。程式碼只是中間的過程。

Abelson 和 Sussman 曾說:「程式必須為人類閱讀而寫,機器執行只是附帶。」但我們花了五十年優化程式語言以便人類撰寫。我們建立了具有身份和狀態的物件,因為這是我們感知現實的方式:嬰兒八個月大時會發展出物體恆存感。這感覺很自然。但瓶頸從來不是創造,而是驗證。

當葛瑞絲·霍普最初設想並撰寫第一個編譯器時,她想像翻譯層直接從英文到機器碼。七十五年後,我們終於能實現她的原始願景。

「程式必須為人類驗證而寫,機器執行只是附帶。」

這是關於責任的宣言。人類擁有規格,人類擁有驗證。中間的一切都是實作。

我們人類不擅長寫程式。機器更擅長,而且它們已經是最差的狀態了。多厲害?甚至 Anthropic——我相當有信心說他們擁有一些最優秀的程式設計師——也表示 Opus 4.5 現在在他們的編碼測試中打敗了所有新進員工。機器更強大,差距將持續擴大。為什麼?

人類以片段和敘事記憶。我們演化出追蹤動物躲在岩石後的能力,記住看不見的莓果叢,並構建因果關係的小電影。難怪我們愛 if-then 陳述,難怪我們建立的程式語言模擬現實並像劇情一樣閱讀:先做這個,再做那個,然後檢查是否成功。我們寫的程式碼符合腦中的電影。

複雜性確實存在於程式碼庫,但程式設計師需要推理程式運行時的狀態。這部分函數中這個應用是什麼?var foo 在此函數中如何綁定?

複雜性應該存在於此嗎?還是應該綁定於英文中,那些圍繞程式碼的混亂且本質上是敘事的產品需求、設計文件和錯誤報告?

現在考慮 LLM 的差異化技能。它們是卓越的模式匹配者。它們能在龐大文本中找到慣用語,良好處理宣告結構,在受限上下文中局部推理。

但它們在物理空間和時間上表現不佳。這也是為何它們需要大量資料和微調才能在影像和影片中正確呈現手部。它們在敘事一致性上表現差,難以維持大量狀態。

JavaScript 提供了什麼?三小時除錯 React 元件,我們已經深入五層堆疊追蹤卻毫無頭緒。錯誤在 useEffect。哪個 useEffect?是掛載時觸發的,還是更新時觸發的,或是狀態改變時觸發但依賴陣列卻說謊?我們必須在腦中重建整個生命週期——什麼時候執行、順序、哪個 Promise 完成、綁定了什麼——只為了理解按鈕為何不切換。我們最終在考古自己星期二的工作。

另一方面,在 Elixir 中函數是純粹的。你有輸入,得到輸出。函數接受這種形狀,返回那種形狀。就是這樣。沒有複雜性藏匿之處。沒有可變狀態。所有資料都是不可變的。模式匹配意味著資料的「形狀」總是在參數中明確定義。結合多重函數頭,對人類來說一開始感覺怪異,但你會得到非常明確的局部上下文:這個函數做這件事,資料總是長這樣。

對人類來說,物件導向程式設計感覺自然,函數式程式設計感覺怪異。函數需要翻譯。沒有移動的杯子;只有一個函數接受「位置 A 的杯子」並返回「位置 B 的杯子」。對於演化出追蹤物件空間的生物來說,這非常不直覺。

物件和狀態的程式設計較易撰寫。函數和不可變資料的程式設計較易驗證。這是「容易」與「簡單」的差別。當我們不斷加入「容易」的東西,系統反而變得過於複雜。

程式碼最好用簡單工具、簡單原語和可重複結構撰寫。不只是 Elixir 語言設計令人驚艷,整個生態系統也是。Elixir 有一個建置系統,一個格式選項,一個枚舉和集合函式庫。命名可預測。簡單是明確的價值,且一切運作良好。訓練 Elixir 的 LLM 會反覆看到相同模式。訓練 JavaScript 的 LLM 則看到千百種變化。

Elixir 最大化程式意義在局部上下文的可見度,而 LLM 是上下文機器。

這又回到 Kernighan 法則。當我們被鼓勵寫複雜狀態機時,我們難以除錯,因為我們很容易達到認知極限。

現在有了 LLM,大家擔心機器會寫出人類無法閱讀的程式碼。我不擔心這點,但我確實在意語言設計。LLM 可以寫出過得去的 Python 和 JS,但它們寫出卓越的 Elixir 和 Racket。訓練資料量不重要。

重要的是局部性:LLM 是否能在不從其他地方重建狀態的情況下看到所需一切。模式匹配讓資料形狀明確。不可變性意味著沒有隱藏的變異。管線和組合意味著流程可預測。單一做法意味著模式重複。

這些正是幫助人類審核程式碼的特性,使程式碼可審查、可除錯、可證明。

具有明確語義的函數式語言優化了機器生成和人類驗證。這是甜蜜點,也是我們的演進方向。

過去,人類撰寫、閱讀和除錯程式碼。

現在 LLM 撰寫程式碼,人類閱讀和除錯。(而 LLM 在冗長語言中寫出大量平庸程式碼。)

人類將做得越來越少。LLM 將撰寫程式碼、除錯並管理邊緣案例。LLM 將根據人類規格、人類審核、人類需求進行驗證。人類只在不一致時介入,因為他們有簡單的驗證機制。

要做好這點,我們需要明確合約、明確效果、可測試屬性、可審計邏輯、可組合元件。

這些是函數式程式設計的美德,也是形式驗證可能的原因,且是 LLM 最擅長的。

我現在每天使用 Claude Code,處理數百萬個標記,輸出比我一天打字還多的程式碼。全是 Elixir,配合嚴謹的規劃文件來理解複雜性。結構簡單,介面清晰,直到函數層級。如果我寫 React,我會擔心函式庫、元件結構、與建置工具的互動……我會害怕自己活在一鍋意大利麵裡。

我現在無法回頭自己寫程式碼。速度會慢很多,但這明顯更好。可讀、可驗證。寫起來對我來說更難,但理解起來很容易。

LLM 已經到來,並向我們展示哪些語言設計得好。AutoCodeBench 測試是一個信號:「難」的語言從未真正難過,它們只是在等待不需要電影思維的頭腦。

軟體工程的未來仍依賴人類,但我們不再寫程式碼。把這交給機器,並給它們好的工具完成工作。

堅持 Haskell 和 Erlang 的極客們只是語言設計的先行者。最早的是 McCarthy:1958 年的 Lisp。

NVIDIA 將推動我們所有進展的 AI 晶片架構命名為葛瑞絲·霍普。以她命名的機器終於讓我們看到她所見。

但這不只是函數式程式設計本身。Rust 雖然明確、型別化且帶有函數式特性,但表現很差。為什麼?因為有大量狀態:借用檢查器的複雜性需要全局推理生命週期。

有趣的是,TypeScript(47.2%)比 JavaScript(38.6%)高約 9 個百分點——型別系統有幫助。但兩者仍在下半部。我敢打賭任何 React 特定程式碼分數會更低,因為 useState 變異、useEffect 依賴追蹤和生命週期複雜性。

Elixir 繼承 Erlang 的「讓它崩潰」哲學。在大多數語言中,你寫防禦性程式碼來處理每個邊緣案例:空值檢查、try/catch 區塊、每個邊界的驗證。在 Elixir 中,你寫快樂路徑,讓監督樹處理失敗。這意味著較少分支邏輯、更清晰的程式碼意圖,以及較少錯誤處理路徑出錯。LLM 不必猜測周圍程式碼使用哪五種錯誤處理範式中的哪一種。