計應(yīng)追求簡單:從Lua看工程實踐的克制之美)
計算機語言的設(shè)計應(yīng)當(dāng)盡可能地簡單這句話我真正聽進去花了快十年。寫代碼這些年我從 C 到 Java、到 Python、到 Go 再到現(xiàn)在常駐的 Lua經(jīng)歷過的每次摩擦和內(nèi)耗幾乎都能歸結(jié)為同一個問題這門語言是不是在強迫我處理本來不該我關(guān)心的事。作為一個寫了十幾年代碼的人我越來越確信這不是一句理想主義口號而是異常實用的工程標準。這篇文章不打算講高深編譯原理就想聊透這句話背后到底藏著什么——為什么“簡單”在計算機語言設(shè)計里這么重要以及作為普通工程師我們怎么用這個標準來選語言、寫代碼、做架構(gòu)。1. 簡單性的真實含義別把“簡單”理解成“容易”1.1 簡單不等于容易更不等于“像英語一樣”很多人在討論計算機語言時把“簡單”自動翻譯成了“好上手”。于是大家天然地認為關(guān)鍵字少、語法接近自然語言、寫起來不費勁就是簡單。這個印象不能說全錯但它錯在把“最初的體驗”當(dāng)成了“長期的心智負擔(dān)”。舉個例子SQL 的 SELECT 查詢剛學(xué)起來非常舒服像讀英文句子一樣。可真做復(fù)雜報表的時候嵌套子查詢、窗口函數(shù)、各種隱式轉(zhuǎn)換迭在一起一條二十行的 SQL 能讓兩個高級工程師爭執(zhí)半天最后還要用 EXPLAIN 分析執(zhí)行計劃。這種“能讀但難想清”的狀態(tài)恰恰是很多看似友好的語言給人的長期困擾。JavaScript 也是典型——函數(shù)式、原型鏈、this 綁定的動態(tài)切換ES2015 之后又加入了大量語法糖語法層面沒有多難但心智層面的坑一個比一個深。我理解的“簡單”不是指第一次寫“hello world”有多順暢而是指一門語言能在并未做很多事的前提下讓使用者在不斷增長的應(yīng)用規(guī)模中不需要不斷地重新學(xué)習(xí)它、不需要記住大量隱藏規(guī)則、不會因為不同特性的相互作用而莫名其妙踩坑。換句話說簡單性真正考驗的不是“入門體驗”而是“長大之后的體質(zhì)”。提示我在評估一門語言時會刻意問自己一個問題——如果三個月后接手一個很大的項目我還能不能很快讀懂這些代碼這個問題的答案往往比“第一次運行是否順利”更能反映語言本身的簡單程度。1.2 簡單性可以拆成四個層次來看日常討論中大家說的“簡單”經(jīng)常不是同一個維度。為了避免雞同鴨講我習(xí)慣把計算機語言的簡單性拆成四層層次關(guān)注點典型問題語法簡單關(guān)鍵字、符號、語句形式同樣的邏輯需要多少行代碼才能表達語義簡單規(guī)則是否一致、是否有例外函數(shù)、變量、類型之間有沒有大量隱式綁定表達簡單組合能力是否強用少量基礎(chǔ)語法能否組合出各種復(fù)雜功能實現(xiàn)簡單運行時/編譯器本身是否輕量部署、調(diào)試、跨平臺是否依賴一個龐大的環(huán)境這四個層次經(jīng)常互相拉扯。有些語言在語法層面做了大量簡化比如用很短的代碼寫循環(huán)、寫集合操作但語義層卻塞滿了隱藏類型轉(zhuǎn)換、運算符重載、自動裝箱使用者在閱讀和調(diào)試時被迫承受額外的認知負擔(dān)有些語言內(nèi)核極小比如 Lisp 家族語法規(guī)則少得可憐但表達層極強一些人覺得過于抽象這其實不是“不簡單”而是“簡單得太徹底反而不適合所有人的腦回路”。最理想的狀態(tài)是四層同時克制語法不故意炫技語義沒有大量隱藏規(guī)則表達靠少數(shù)核心機制的組合而不是堆砌幾十種專用寫法實現(xiàn)層面也足夠輕量。這也是為什么我后來對 Lua 情有獨鐘——它幾乎把“內(nèi)核小、組合性高”這件事做到了極致。一個那么小的語言解釋器居然能支撐大量游戲和應(yīng)用靠的就是基礎(chǔ)語法之外沒有太多額外負擔(dān)。2. 判斷一門計算機語言是否簡單的六個實用檢查點2.1 從使用者的角度如何實際評估“簡單”如果“簡單”這個概念聽起來還是太抽象我建議你直接用下面這幾個檢查點去衡量一門語言。它們都不是理論指標而是我在實際選型和寫代碼中總結(jié)出來的“體檢項”。第一從知道一門語言到寫出第一個像樣的工具需要多久。這不是指 hello world而是指一個能處理真實數(shù)據(jù)的小腳本或小服務(wù)。如果過程順暢、不需要查大量配置說明語言的上手成本低。第二錯誤信息可不可讀。一個能把“類型不匹配”翻譯成人話的語言和一個只拋 stack overflow 讓開發(fā)者自己猜的語言維護成本差距是數(shù)量級的。第三工具鏈是否臃腫。一門語言的安裝包動輒幾百兆一個空工程要生成幾十個配置文件使用者的注意力早就從寫代碼轉(zhuǎn)移到了“伺候環(huán)境”。第四標準庫是否風(fēng)格一致。如果有些函數(shù)返回對象、有些函數(shù)返回布爾值有些函數(shù)會拋異常、有些函數(shù)默默返回 nil使用者的記憶負擔(dān)會爆炸式增長。第五隱藏規(guī)則的多少。運算符重載密集、隱式類型轉(zhuǎn)換頻繁、變量生命周期和閉包綁定的規(guī)則復(fù)雜都會讓代碼表面看著簡單、實際行為詭異。第六社區(qū)示例和常用框架是否還在保持同樣的簡單性。很多語言本身不錯但被社區(qū)框架注了水——一個簡單的增刪改查也要引入七八層抽象這時候“語言簡單”已經(jīng)和你沒什么關(guān)系了。這六個檢查點我最看重的是“隱藏規(guī)則”和“錯誤信息可讀性”這兩項因為它們直接決定了一個資深工程師在排查問題時是花十分鐘還是花一天。隱藏規(guī)則多的地方代碼讀起來只能靠猜錯誤信息不清的地方調(diào)試起來只能靠試。2.2 幾門主流語言在“簡單性”上的氣質(zhì)對比下面這個表是我個人心里的感受值不是精確的基準測試主要用來展示“簡單性”在不同語言身上是怎么體現(xiàn)的。同一門語言在不同場景下觀感差別會很大。語言內(nèi)核大小隱藏規(guī)則工具鏈負擔(dān)適合場景典型難點Lua極小少極輕嵌入式、游戲腳本、膠水代碼生態(tài)小庫奇缺Python中等中等中等快速原型、數(shù)據(jù)腳本動態(tài)類型下大型項目重構(gòu)難Go小少輕后端服務(wù)和網(wǎng)絡(luò)程序泛型和錯誤處理有一定繁瑣感Java大中等偏多較重大型企業(yè)系統(tǒng)樣板代碼多環(huán)境配置復(fù)雜Rust大多重系統(tǒng)編程、性能敏感場景所有權(quán)和生命周期心智負擔(dān)很重這張表不是為了給語言排名次而是想說明一個事實越接近系統(tǒng)底層、越需要在性能上壓榨的語言往往越難做到純粹的簡單。Rust 本身在并發(fā)安全和性能方面非常出色但它的復(fù)雜是“縫合”出來的——精確控制往往需要更多顯式規(guī)則于是簡單性就勢必讓位給其他目標。我們在選型時要有這個權(quán)衡意識不能用“簡單”這個單一維度去否定其他價值。3. 實操演示用 Lua 快速實現(xiàn)一個單詞統(tǒng)計工具3.1 為什么拿 Lua 當(dāng)“簡單設(shè)計”的樣本聊完抽象的判斷標準我想用一個具體的實操案例讓大家直觀看到“內(nèi)核小、組合性高”到底是什么體驗。我選擇了 Lua因為 Lua 可能是目前最常見的“簡單但不簡陋”的計算機語言樣本它的官方參考手冊只需要幾天就能通讀語法規(guī)則少到可以在一屏內(nèi)展示核心數(shù)據(jù)結(jié)構(gòu)只有一張表——但偏偏是這張表既能當(dāng)數(shù)組、又能當(dāng)字典、還能當(dāng)對象。打開 Lua 解釋器十分鐘普通人就能寫出像樣的腳本這種體驗在主流語言里非常難得。Lua 的設(shè)計者一貫堅持“提供少數(shù)核心機制而不是為每種需求都新增特例”。例如函數(shù)是一等公民閉包能力原生支持表可以承載幾乎所有的數(shù)據(jù)組合需求。這不是能力不足而是刻意選擇他們希望語言本身保持輕量把擴展空間留給使用者和宿主環(huán)境。這種設(shè)計哲學(xué)恰好就是標題里那句話的正向注解。3.2 完整實現(xiàn)與逐步解讀下面這個例子用 Lua 實現(xiàn)了一個非常常見的需求讀取一個文本文件統(tǒng)計每個英文單詞出現(xiàn)的次數(shù)并輸出結(jié)果。保存為wordcount.lua在命令行執(zhí)行。local counts {} for line in io.lines(arg[1]) do for word in line:gmatch(%a) do word word:lower() counts[word] (counts[word] or 0) 1 end end for word, n in pairs(counts) do print(word .. : .. n) end這段代碼只有十來行但功能是完整的。第一行l(wèi)ocal counts {}創(chuàng)建了一張空表用表來當(dāng)字典用io.lines(arg[1])每次抓取輸入文件的一行l(wèi)ine:gmatch(%a)用正則模式把一行里的連續(xù)字母序列迭代出來相當(dāng)于一次拿一個單詞word word:lower()把大寫轉(zhuǎn)小寫避免把Hello和hello統(tǒng)計成兩個詞counts[word] (counts[word] or 0) 1是核心技巧如果這個單詞還沒出現(xiàn)過counts[word]是 nilnil or 0返回 0于是從 0 開始加 1如果出現(xiàn)過就取出舊值加 1。最后用pairs(counts)遍歷整張表并打印。這個(counts[word] or 0) 1寫法第一次看可能覺得不習(xí)慣理解之后就會發(fā)現(xiàn)它把“判斷是否存在 初始化為 0 自增”三件事壓縮成一行而且語義非常清晰。這在 Lua 社區(qū)是非常典型的慣用法也是“用極少語法表達復(fù)雜邏輯”的一個縮影。運行方式也很簡單在命令行直接執(zhí)行l(wèi)ua wordcount.lua sample.txt如果sample.txt內(nèi)容是一段英文文本程序會一個詞一行輸出統(tǒng)計結(jié)果。整個過程不需要安裝任何額外的包不需要初始化項目不需要 import 任何東西打開文本編輯器寫完存盤就能跑。對新人是極其友好的體驗。3.3 和 Java 版本對比看設(shè)計哲學(xué)的差別為了把“簡單”這個體會說得更直觀這里放一段同樣功能的 Java 核心代碼。不是要踩 Java而是想展示不同語言在“設(shè)計上的取舍”如何直接影響日常編碼體驗。import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class WordCount { public static void main(String[] args) throws IOException { MapString, Integer counts new HashMap(); Pattern pattern Pattern.compile([a-zA-Z]); try (BufferedReader reader new BufferedReader( new FileReader(args[0]))) { String line; while ((line reader.readLine()) ! null) { Matcher matcher pattern.matcher(line); while (matcher.find()) { String word matcher.group().toLowerCase(); counts.put(word, counts.getOrDefault(word, 0) 1); } } } for (Map.EntryString, Integer entry : counts.entrySet()) { System.out.println(entry.getKey() : entry.getValue()); } } }要承認Java 版本邏輯上和 Lua 版本一模一樣核心代碼行數(shù)也差不多但周圍要包裹的東西明顯更多類聲明、main 方法簽名、import、BufferedReader 的 try-with-resources、Pattern 和 Matcher 的配合。這是 Java 工程化的代價也是它的優(yōu)點——它用這些顯式的樣板換來了大規(guī)模企業(yè)應(yīng)用里高度統(tǒng)一的組織和審閱方式。我沒有否定 Java 的意思我想說的是如果只是做一個小工具、寫一個小腳本Lua 的簡單性讓實現(xiàn)成本低得多如果是幾百人協(xié)作的復(fù)雜系統(tǒng)Java 的規(guī)矩性反而能壓制混亂。語言無非是適合不同土壤的工具。實操心得我第一次寫這個工具時下意識加了一個“行內(nèi)單詞去重表”想先把每一行的唯一詞拿出來再統(tǒng)計。結(jié)果跑完發(fā)現(xiàn)輸出明顯不對一排查才發(fā)現(xiàn)我把它做成了逐行去重同一行里重復(fù)的單詞只算了一次。這個坑提醒我簡化代碼的前提是先保證業(yè)務(wù)語義正確簡單的代碼也必須老老實實地反映真實需求。3.4 簡單語言的邊界也得如實說看著 Lua 很爽我也得把它的邊界說清楚。上面這段正則用的%a只匹配 ASCII 字母中文文本一個詞都抓不到Lua 的標準庫很精簡想處理 Unicode、日期比較、網(wǎng)絡(luò)請求都得自己找?guī)旎蛘{(diào)用宿主能力。這就是簡單性的另一面語言層面不帶太多東西依賴社區(qū)和宿主環(huán)境補全。如果你想在 Lua 里處理中文最常用的做法是裝一個 LuaUTF8 庫然后把正則模式換成能識別 UTF-8 連續(xù)字節(jié)的寫法。一種比較笨但有效的做法是先按空白分隔再過濾掉純標點符號或者直接把整個文本按字符迭代判斷是否屬于 CJK 區(qū)間。這個處理過程比英文統(tǒng)計明顯要寫更多代碼因為它超出了 Lua 默認自帶的“簡單范圍”。所以你看“簡單”從來都不是免費的。它省掉了配置、概念、樣板代碼但會把一部分能力負擔(dān)轉(zhuǎn)移給使用者和生態(tài)。這也是我在選型時反復(fù)強調(diào)的簡單性一定要結(jié)合場景來看脫離場景談簡單沒有意義。4. 語言簡單項目為什么還是一團亂麻4.1 復(fù)雜度的三個隱形來源框架、抽象、偽擴展不少開發(fā)者會有同樣的困惑語言明明很簡潔團隊也本本分分地寫為什么項目一段時間后還是變得難以維護我在復(fù)盤很多項目后發(fā)現(xiàn)復(fù)雜度的真正來源往往不是語言本身而是語言之外的三個地方。第一是越來越重的框架。很多框架的初衷是“約定優(yōu)于配置”但框架為了滿足所有場景最后往往變成“配置比功能還多”。一個簡單的 API 接口用框架可能需要理解依賴注入、AOP、生命周期、中間件鏈路等一堆概念而這些概念在被使用的項目里九成是多余的。第二是類型體操和過度抽象。有些團隊為了“架構(gòu)整潔”規(guī)定所有實體類必須先寫接口再寫實現(xiàn)一個只有兩個字段的數(shù)據(jù)結(jié)構(gòu)也要掛一層工廠。我在一個項目里見過所有 service 都同時存在接口和實現(xiàn)類而接口從創(chuàng)建到現(xiàn)在只有一個實現(xiàn)沒有人能說出多這一層有什么實際收益只是“規(guī)范就是這么定的”。第三是偽擴展。寫代碼的時候總想“以后肯定要支持新場景”于是為了想象中的未來造了大量抽象層。實際結(jié)果是未來沒來抽象層成了所有人的負擔(dān)。4.2 在代碼評審階段守住“簡單”的檢查清單既然復(fù)雜度的根子在人而不是語言那么守住簡單性的關(guān)鍵動作也應(yīng)該落在人和流程上。我在自己的團隊里推行了一套“簡單性檢查清單”代碼評審時逐條過不需要完全滿足所有項但只要有明顯違反項就必須停下來討論原因。這個改動用到的抽象層級是否超過三層如果超過說明可能存在不必要的中間層。是否優(yōu)先使用了語言自帶的庫和能力有沒有因為不熟悉而自行造輪子函數(shù)和變量的名字不看注釋能不能理解業(yè)務(wù)意圖這次改動里是否有“為了以后可能用到”而寫的代碼如果以后沒用到這些代碼就是負債。如果用更少的代碼也能實現(xiàn)同樣的功能為什么沒有這么做有沒有刻意炫技的成分修改局部功能時需要改動多少個文件改動面是否和邏輯影響面匹配迭代操作里是否存在被隱式共享或修改的全局狀態(tài)注釋說明了“為什么”還是在用注釋湊字面解釋“做了什么”這個清單看起來非常基礎(chǔ)但正是這些基礎(chǔ)規(guī)則最能拉住復(fù)雜度。我見過太多項目從“控制器只做路由分發(fā)”到“控制器里寫滿了業(yè)務(wù)邏輯再到服務(wù)層再到倉儲層再到事件總線”的變化過程每一步單看都有道理合在一起就是無人能獨力走通的迷宮。所以每當(dāng)我聽到有人說“這門語言不夠強大所以項目才復(fù)雜”的時候我都會搖搖頭。語言在最壞的情況下也只是提供了快速變復(fù)雜的工具真正決定復(fù)雜度的永遠是寫代碼的人一個又一個“多考慮一下”“先加一層再說”的選擇。5. 常見誤解與避坑指南簡單語言不是萬能但它值得敬畏5.1 幾個流傳很廣的錯誤認知圍繞“計算機語言設(shè)計要簡單”這句話我在社區(qū)和交流中看過太多跑偏的解讀。下面這些誤解幾乎每年都會遇到這里一并列出來免得新人繼續(xù)被帶偏。誤解實際情況簡單 簡陋簡單是刻意取舍簡陋是沒能力實現(xiàn)完整功能兩者有很大區(qū)別簡單 沒有類型靜態(tài)類型和簡單并不矛盾Go 的靜態(tài)類型設(shè)計就足夠克制簡單 不用設(shè)計模式少用不必要的模式是對的但核心場景該用還是要用簡單語言做不了大項目Lua 能支撐幾十萬行的游戲邏輯簡單與否和項目規(guī)模沒有必然關(guān)系簡單語言性能必然差很多用 Lua 綁定的游戲在線服務(wù)性能極好性能瓶頸通常在算法而非語法5.2 我踐行的三條“簡單”原則聊到最后我想分享三條自己在實踐中反復(fù)驗證過的原則也算是對標題這句話的落地方案。第一寫代碼之前先寫清楚“這段代碼要解決的直接問題”是什么。如果問題本身不明確那么無論用什么語言、什么架構(gòu)最終都會做出一個復(fù)雜的空殼。第二遇到寫得晦澀但“很厲害”的代碼先把它拆平再討論而不是因為看不懂就默認它是對的。我處理過太多“為抽象而抽象”的代碼拆掉之后功能完全不受影響可讀性卻提升了一個量級。第三我追求的不是“代碼最少的簡單”而是“最不容易讀錯的簡單”。有些時候多寫兩行看似冗余的代碼卻能把語義表達得無比清楚這種簡單比拼命壓縮行數(shù)的簡單更有價值。這條原則我尤其想分享給剛?cè)胄械呐笥选N乙娺^太多項目不是死在缺少功能上而是死在復(fù)雜度失控上。今天多一層不必要的封裝明天多一個隱式的全局狀態(tài)后天多一處“等以后再用”的分支三五年后這個項目就變成了所有人都不想接手的樣子。所以在一次次踩過這些坑之后我越來越敬畏“計算機語言的設(shè)計應(yīng)當(dāng)盡可能地簡單”這句話。它不只是給語言設(shè)計者看的也是給所有寫代碼的人看的——少一點造作多一點克制代碼總會回報你。