如何用語文知識改善程式碼可讀性

doodlewind發表於2017-11-30

我們經常能看到許多技術文章從工程角度介紹各種編碼實踐。不過在電腦科學之外,程式語言和自然語言之間同樣有著千絲萬縷的聯絡。下面我們會從高中水平的語文和英語出發,分析它們與程式碼可讀性之間的關係。如果你看膩了各種花哨的技術新概念,或許迴歸基礎的本文能給你一些啟發?

程式語言與高考作文

大師所編寫的程式碼與其說是給計算機看倒不如說是給人看的。真正的大師級程式設計師所編寫的程式碼是十分清晰易懂的,而且他們注意建立有關文件。

——《程式碼大全》

不妨思考一下,我們對某段程式碼【十分清晰易讀】的評價,比起對某篇文章【寫得通俗易懂】的評價,是否具有相近的評價標準呢?進一步說,程式語言的程式碼和自然語言的文章之間,是否存在著某些技術之外的共通性呢?這裡我們拿出和程式碼一樣死板的高考作文作為對比,不難發現一些有趣的相似之處:

  • 程式碼很難正確預測需求的變更,而高考作文也很難從題目提煉出主題立意。
  • 程式碼編寫前要做好架構設計,而高考作文落筆前也要好好構思。
  • 程式碼要做好模組化、元件化,而高考作文也要求段落劃分恰當、銜接緊湊。
  • 程式碼的 Warning 越少越好,而高考作文的語病也是越少越好。
  • 程式碼要求排版、縮排格式正確,而高考作文也要求字跡工整。
  • 程式碼要求儘量少複製貼上,而高考作文更是嚴禁抄襲。
  • 程式碼寫得好的人很少,而高考作文寫得好的人可能更少。
  • ……

是不是有著不少相近之處呢?不過,高考作文的記敘、議論、抒情等文體已經是人類思維的高階抽象,尤其是抒情文這類涉及感情的文體,其內容與理念是很難和講求邏輯的程式程式碼做類比的。並且,編寫作文所用的漢語也更不是主流程式語言所用的英語,這也就意味著從中文作文的角度著手分析可能過於巨集大且不夠貼切。因此,下面我們會改從英語的角度來探討程式碼與語言之間的關係。

詞性與關鍵字

中英文裡都有詞性的概念,詞彙可以分類為名詞、動詞、形容詞、連詞、代詞等不同詞性。而在計算機語言中,內建的【詞彙】就是 for / if / else 這些關鍵字了。那麼這些關鍵字的詞性,和計算機語言的性質之間有什麼關係呢?

實際上,不同用途的計算機語言,其關鍵字中對詞性的選擇會有很大的不同。請注意,程式語言其實只是計算機語言的子集。比如,經典的前端三件套 HTML + CSS + JavaScript 中:

  • HTML 是標記語言
  • CSS 是樣式語言
  • JavaScript 是程式語言

它們都歸屬於計算機語言,但它們各自所用的關鍵字,在詞性上有什麼區別呢?

  • 提到 HTML,我們首先會想到 <head> / <body> / <img> / <table> 這些標籤。這些標籤的名稱都是名詞
  • 提到 CSS,它的典型程式碼就是形如 .xxx { background: black; } 這樣的規則。這裡,規則的名稱基本都是形如 background / position / width / color名詞,而規則的值則常見各種形容詞
  • 提到 JavaScript,除了 function / var / class 這三個名詞以外,它的控制流邏輯幾乎都是由 if / else / for / while 這些虛詞控制的。並且,還有大量 return / break / new 這樣的動詞

我們可以發現,標記語言和樣式語言中,關鍵字幾乎完全由實片語成,完全不需要虛詞的起承轉合。而實詞能夠表達什麼呢?它能夠表達一個東西是什麼。所以,HTML 和 CSS 中,你需要告訴機器的是你想要的是什麼,而不關心怎麼去實現。比如,你告訴 HTML 解析器這裡有個 <img> 圖片,而無需操心圖片的格式、如何載入等細節;你告訴 CSS 引擎去把標題顏色渲染為紅色,但無需關心佈局的如何計算、GPU 如何渲染等實現方式。因此,在電腦科學中我們把 HTML 和 CSS 歸為宣告式的語言,而這類語言的一大特色就是其關鍵字幾乎全部是實詞

宣告式的語言一般而言比較簡單易懂(類比一下,你覺得最難維護的程式碼是 HTML 和 CSS 嗎?),而三件套中剩下的 JavaScript 顯然不是這樣。為了搞懂它所用關鍵字詞性和它作為程式語言之間的關係,我們有必要更詳細地對它的常見關鍵字做一個分類:

名詞
function var class

動詞
import export extends return break continue
delete switch new try catch throw yield

介詞
for in else

連詞
if while

代詞
this複製程式碼

聯想一下程式語言日常的使用場景:告訴瀏覽器要先請求某個介面、拿到資料後如果格式怎樣怎樣就做什麼什麼事情、如果點選確定那麼發一份新資料看後臺回覆了什麼……這些內容所編寫的程式碼都是在描述問題怎麼做而非問題是什麼。所以,程式語言需要大量的虛詞,來用分支、迴圈等方式表達等各個語句間的邏輯關係。這種程式碼的【文體】就是所謂的程式式程式設計了。

除了出現許多表達控制流的虛詞以外,程式語言的一大特色在於它具備大量的動詞作為關鍵字。如果說 function / var / class 能夠讓我們定義基本資料概念的話,大量的動詞關鍵字則提供了對這些概念的操作能力。比如,我們會用 import / export 來操作 模組 這個概念模型;用 try / catch / throw 來處理 異常 這個概念模型;用 new / extends 來處理 這個概念模型……所以,過程式的程式語言中需要大量的動詞,來表達對資料的操作

對動詞的使用並不僅僅體現在關鍵字中,在實際的編碼實踐中也會大量運用。比如,Python 2 中的 print 語句在 Python 3 中變成了 print 函式,不就說明日常編寫的函式和語言關鍵字之間是可以互相轉化的嗎?所以,在我們編寫對資料的處理程式碼時,相應的程式碼也應當能夠用命名為動詞的函式來封裝。當然,真實世界中的函式定製型一般非常強,比起程式語言中的精粹關鍵字來要具體的多,因此函式名多半不能簡單地用一個動詞來表達,這時候用一個形如 getElementById動賓短語結構來命名函式,就能夠達到很好的效果了。

在現代的程式語言中,除了變數、類對應的名詞;函式、方法對應的動詞、控制流對應的介詞、連詞以外,還有一類非常特殊的存在:this 對應的代詞。代詞在程式語言中起到了什麼作用呢?自然語言中,代詞可以在語境中自然地指代先前提到的概念,而 this 則用來指向某個上下文中的引用,在概念上是不是非常接近呢?

不過遺憾的是,從類比自然語言的角度來看,JavaScript 中的 this 初始設計是十分失敗的。在早期的前端開發中,this 經常不能夠在程式碼的【語境】中指向你所認為自然的地方,而是有各種奇怪的規則來指向不同的上下文。對這類語言機制上的缺陷,社群也做了不少改進,來讓使用了 this 的程式碼更易寫易讀。這其實也可以理解為自然語言的可讀性對程式語言設計的影響吧。

句型與表示式

自然語言中,我們可以將詞彙整理為句子,而句子則具有不同的結構,如陳述句、祈使句、疑問句、感嘆句等。

類比到程式語言中,在一門語言的新手課程裡,一般會提到 Statement 語句和 Expression 表示式的概念。比如,if (color === 'RYB') fxck(); 整體就是一個語句,而其中的 color === 'RYB' 則是一個表示式。

程式語言的語句、表示式比起自然語言的句子,它們之間有什麼關係呢?祈使句、疑問句、感嘆句都夾雜了一定的感情,和我們的主題不太相關。讓我們從自然語言中最簡單的陳述句語序來做些探索吧。我們選出其中兩種最具有代表性的結構,即主謂賓結構和主系表結構:

S + V + O 主謂賓結構

孩子去上學。

這就是一個非常容易理解的主謂賓結構了。這個結構也非常容易對應到程式語言裡的程式碼:

child.go(school);

主語對應一種資料模型,謂語對應函式方法,賓語對應函式的引數。沒有問題,非常清晰易讀吧?

S + V + P 主系表結構

學校是黑色的。

主語 + 系動詞 + 表語的結構同樣非常易讀。但這裡存在著一個非常大的陷阱:自然語言不區分語句和表示式,而上面這句話既可以理解為語句,也可以理解為表示式:

  • school = 'black'; 是一種語句型別的程式碼實現,語句沒有返回值。
  • school === 'black' 是一種表示式型別的程式碼實現,表示式會返回 truefalse

這樣就出現了非常大的歧義了:這句話在翻譯為程式碼的時候,到底指的是 把 school 賦值為 'black',還是 判斷 school 是否為 'black' 呢?在控制流裡,這樣的歧義就會造成問題:

if (school = 'black') fxck()
if (school === 'black') fxck()複製程式碼

在表達 如果學校是黑色的,那麼 xxx 時,就會在程式碼裡造成混淆。上面的程式碼裡,前一種不管學校黑不黑都會 xxx,而後一種才是合理的實現。

從這個例子中我們可以發現,在將可讀的自然語言轉換為程式碼邏輯時,自然語言的簡單陳述句可以對應到程式語言的語句上,而表達邏輯的複合句中,從句則更接近表示式的概念。程式設計的一大挑戰就是去理清自然語言中模糊不清的邏輯,這需要對程式語言的學習和不斷的訓練才能更好地做到。

時態與同非同步

詞彙可以組成句子,而英語中的句子是存在時態的概念的。巧合的是,資料的狀態也是程式執行時非常重要的概念。在這裡,我們也能建立很好的類比關係。

同步和非同步,在真實世界的程式中非常常見。比如使用者在頁面上點選確定按鈕向後臺提交資料的時候,網路請求和響應就需要時間來傳輸,請求的結果就是非同步展示的。那麼,同步與非同步能夠類比到自然語言中的什麼時態呢?

同步程式碼不存在時態的問題,你大可以用一般現在時來命名變數和函式,整個執行流程會十分清晰。但牽扯到非同步時,你就會發現在你訪問某個變數的時候,它可能還沒有值。這時候怎麼處理呢?

Promise 物件是處理非同步邏輯的一大利器。一個 Promise 具有 pending / resolved / rejected 三種狀態,我們可以用 resolvereject 來在狀態間遷移。這裡我們表達操作的命名仍然是動詞,但這時我們可以注意到,不同的狀態是用現在進行時現在完成時來命名的。更一般地,我們可以抽象出這樣的規則:

  • 表達【進行中】的狀態變數用現在進行時命名。
  • 表達【已完成】的狀態變數用現在完成時命名。

這樣一來,我們就能夠把自然語言中對時態的思維模型,平滑地遷移到程式碼裡表達非同步的狀態中,從而讓程式碼更加易讀了。

文法與編譯器

上面的諸多內容其實都僅僅是 Grammar 語法層面的內容,但【語文】的外延是【語言學】這一學科,其研究領域遠不僅僅是高中語法知識這麼簡單。可以非常肯定地說,作為文科的語言學,對程式語言的設計和實現都有著非常重要的影響。這麼說有什麼根據呢?讓我們從語言學中的一個分支【句法學】說起吧。

句法即 Syntax,編譯器的常見報錯 SyntaxError 指的就是句法錯誤。句法學的研究領域中,涉及到了對句子的結構分析。早期的研究者們提出過兩種分析法,即【雙切分法】和【方括弧法】。比如下面的句子:

The teacher abuses a child.複製程式碼

按照雙切分法,我們先把整句話一分為二,然後把謂語分開,最後分解名詞短語,就可以一步步地得到這樣的結果:

第一次切分
The teacher / abuses a child.

第二次切分
The // teacher / abuses // a child.

第三次切分
The // teacher / abuses // a /// child.複製程式碼

這樣我們就能夠拆解出句子的主謂賓結構了。

而方括弧法的解釋方式則是這樣的:

[3 [1 The teacher] [2 abuses [1 a child]]]複製程式碼

我們先為名詞短語 The teachera child 新增方括弧,然後組成更高層的動賓結構,最後合成為句子。

那麼這兩種方法和程式語言有什麼關係呢?從上例中我們可以看到,雙切分法的處理方式是自頂向下,而方括弧法的處理方式是自底向上。如果瞭解過編譯原理的同學,看到這裡應該會立刻想起語法分析器中的 LL 演算法和 LR 演算法吧?LL 演算法遞迴向下地處理程式碼語句,而 LR 演算法則是自底向上地歸約詞法元素。所以,編譯器前端在將程式碼字串轉換為語法樹的時候,語法分析演算法的執行方式和語言學中的方法論是共通的

除了結構分析外,句法學對程式語言的一大貢獻在於它提出瞭如何定義一門語言的方式。在句法學的課本中,會提及 Chomsky 在 1957 年提出的《句法結構》一書,這本書中提出了生成文法的概念,能夠抽象地用數學符號定義任意一門語言。比如一條表明【名詞短語(Noun Phrase)包含形容詞(Adjective)和名詞(Noun)】的句法規則,形如:

NP → A, N複製程式碼

這樣,語法樹中 NP: damn school 的節點就能被拆分為 A: damnN: school 的子節點了。推廣到計算機語言,這個文法同樣適用。比如這條規則:【一個 HTML 標籤(Tag)要包含開始標籤(TagOpen)、值(Value)和結束標籤(TagClose)】:

Tag -> TagOpen, Value, TagClose複製程式碼

通過這樣的句法規則,我們就能把 HTML 樹中形如 <p>123</p> 的字串拆分為 TagOpen: <p>Value: 123TagClose: </p> 的三個子節點了。在現代的 LLVM 編譯器前端中,我們只需要提供這樣的句法規則,就能夠定義出自己的一門新計算機語言了,是不是完全相通呢?所以,Chomsky 文法在《編譯原理》中也有詳細的介紹,這也是一個電腦科學中橫跨文理的概念了。

值得一提的是,實現一個語法分析器的輪子是件很有趣的事情。筆者在大學時的編譯原理大作業中,實現的就是一個 JavaScript 版的 LALR 語法分析器。這個過程能讓你深刻地認識到弱型別語言到底有多坑…歡迎有興趣的同學嘗試?

語義與作用域

在語言學的範疇中,還有一個和編碼密切相關的地方出現在【語義學】中。不太準確地說,這個學科研究的是【詞彙到底有什麼涵義】的問題。比如,一個 望遠鏡 在什麼時候會讓人覺得噁心?這其實可以對應到程式語言中另一個非常重要的概念,即作用域

語義學中認為,名詞指稱事物、動詞指稱行為、形容詞指稱屬性、副詞指稱方式,故而詞語具有指稱的功能。詞語的指稱分為有指和無指兩種,比如 school 能夠指代明確的物件,是為有指;而 if 指稱的是抽象的條件和假設關係,是為無指。有指可以進一步分為衡指和變指兩種。比如,長城 就是一個所指物件不變的衡指,而 he 就是所指物件因場合而變的變指。在不同的語境下,詞彙的實際指稱會發生改變。

衡指和變指的概念,是不是和程式語言中的全域性變數很接近呢?比如,document 就是一個總是指向 DOM 的全域性變數,而 this 的指向就會因程式碼所在的上下文而變。在語言中明確而無歧義的指代關係,恰好能夠和程式語言中嚴謹的作用域相類比。

自然的指代關係,能夠讓我們在寫文章時流暢許多。而程式碼中的區域性變數,只要處在受限的作用域(如函式作用域)中,就可以有精煉易讀的命名。如果沒有作用域機制,我們就必須用笨拙而沉重的命名約定來防止指代不清和重名(例如早先前端的 BEM 命名法)。

所以,我們不妨把作用域機制也當做現代程式語言為了【更接近自然語言】所作出的努力。在為變數起名的時候,我們更可以從語義學的角度來考慮這個變數名的指稱是什麼,具備怎樣的涵義。

總結

從自然語言來類比程式語言,我們確實能發現很多從技術角度被忽略的地方:

  • 程式碼中詞彙的詞性和程式語言機制密切相關。
  • 程式碼語句和自然語言句型間有著細微的歧義。
  • 程式的非同步狀態處理可以類比時態。
  • 原始碼的解析方式其實來源於語言學中的句法學。
  • 基於作用域機制的變數命名可以參考語言學中的語義學。
  • ……

可以看到,程式設計並不是理科生的枯燥工作,它和人文學科之間的關係同樣緊密。

不過,有的同學可能會有疑問:寫了這麼多,到底該怎樣能寫出更好的程式碼呢?這不是一篇文章能夠解決的問題。寬泛地說,這仍然需要多做、多思考、多向更好的程式碼學習。如果本文能夠激發你對於程式設計和人文間某些交叉點的興趣,那就足夠啦。

在寫作本文的過程中,筆者還有了一個額外的發現,那就是如果人工智慧會取代人類,那麼程式設計恐怕也是最後幾個被取代的崗位。從上面的論述中我們可以發現,好的程式碼需要清晰的模組拆分和流暢的表達,而這兩點實際上都和人文科學有著莫大的關係。這個角度上說,程式設計並不是重複性的工作,而是智慧的沉澱。

由於作者只是電腦科學和語言學的【使用者】而非【研究者】,因此本文的內容其實非常粗淺,對於錯漏,希望這兩個領域中更加專業的同學斧正。

最後,這個專欄後續還會不定期更新一些將程式設計與真實世界現象相結合的雜文。有興趣的同學,歡迎關注作者的掘金或 Github 哦?

相關文章