顯示具有 巴科斯範式 標籤的文章。 顯示所有文章
顯示具有 巴科斯範式 標籤的文章。 顯示所有文章

2026年9月10日 星期四

語言的語言:BNF 從哪裡來,又怎麼用 - 2026.09.10

 底下這篇,為 Grok AI 所撰述。

--

語言的語言:BNF 從哪裡來,又怎麼用

2026.09.10


數學家習慣先把符號講清楚,再開始推。工程師比較急:先把程式寫出來,語法「大概長這樣」就交給同事用英文猜。猜到第三次,會議室裡會出現一種很特別的安靜——每個人都以為自己懂了,每個人懂的 都不一樣。

1950 年代末,寫程式語言規格的人正面對這件事。他們需要的不是另一種程式語言,而是一種專門用來描寫語言的語言。後來大家叫它 BNF:巴科斯範式(Backus Normal Form),也常寫成巴科斯–諾爾範式(Backus–Naur Form)。

Matt Might 有一句話把這件事講得很乾淨:Grammars are the language of languages. 文法是語言的語言。本篇就沿著這句話走:BNF 為什麼會出現、規則怎麼讀、後來又長成什麼樣子。不打 算把編譯器課本搬進來。目標比較謙虛——看完之後,再遇到一串尖括號和 ::=,不會覺得那是從別的星球寄來的符咒。

先有語言,才發現語言自己不會說話

FORTRAN 已經證明一件大事:人可以不直接對機器下指令,改寫比較像公式的東西。問題是,FORTRAN 的「合法寫法」大多還是用普通英文交代。英文很適合講故事,不太適合當合約。同一個句子,三位實作者可以讀出三種邊界情況。

1958 年,歐美兩邊的委員會想做一套不綁特定機器的算法語言,先叫 IAL(International Algebraic Language),後來改名 ALGOL。理想很大:一份規格,大家讀完應該寫出同一種語言。理想一落地,就撞上更老的問題——規格本身用什麼寫?

John Backus 當時已經因 FORTRAN 出了名。1959 年 6 月,他在巴黎的 UNESCO 資訊處理會議上提出一篇論文,標題很老實:The Syntax and Semantics of the Proposed International Algebraic Language of the Zurich ACM-GAMM Conference。重點不在再發明一套語句,而在發明一套「後設公式」(metalinguistic formulas),專門描寫哪些符號序列算合法程式。

他的靈感比較靠近 Emil Post 的產生式,而不是語言學課堂上的樹狀圖。想法卻很樸素:左邊放一個名字,右邊寫這個名字可以長成什麼;需要分支就寫「或」。人讀得懂,機器原則上也能跟著長。

Peter Naur 讀這份報告時,第一反應並不浪漫。他後來回憶,自己先是失望:蘇黎世會議以為大家已經講清楚的事,Backus 寫出來的版本卻對不太上。換句話說,連委員會內部都還沒共用同一張圖。Naur 後來說,他是過了一陣子才穿透那套形式語法,然後立刻改觀——這種記法正好是他想用來寫 ALGOL 的工具。

於是 ALGOL 60 報告裡,語法不再只靠散文。Naur 當編輯,把 Backus 的符號改成當時打字機比較友善的樣子:定義號變成 ::=,「或」變成 |,被定義的東西用 <...> 包起來。1960 年的報告一出,BNF 就不再只是一篇會議論文裡的私房記號,而成了公共契約。

為什麼需要 BNF?一句話:英語會讓人以為自己同意了。BNF 逼你把「可以長成什麼」寫成可核對的規則。ALGOL 想當國際語言,先得讓規格自己講同一種話。

名字怎麼從 Normal 變成 Naur

一開始它叫 Backus Normal Form,巴科斯範式。Normal form 聽起來很數學:範式、標準形,好像世界上只該有一種正確寫法。問題是,BNF 並不是那種東西。同一套語言,你可以寫得很肥,也可以寫得>很瘦;兩份文法可以生成同一堆句子,長得卻完全不像。

1964 年,Donald Knuth 在《Communications of the ACM》投了一封不長的信。他建議改叫 Backus–Naur Form。理由很乾脆:第一,Naur 把這套記法寫進 ALGOL 60,功勞不該被「範式」兩個字吞掉;第 二,縮寫仍然是 BNF,舊文獻不用作廢;第三,它根本不是 normal form。

建議被接受了。所以你現在會同時看到兩種中譯:巴科斯範式、巴科斯–諾爾範式。指的是同一件事。比較像有人同時有戶籍名和筆名,信件都能送到。

再往前追,還有人主張記更早的祖先。1967 年 P. Z. Ingerman 投書,說公元前的波你尼(Pāṇini)為梵語寫過一套產生規則,力量上已接近後世的上下文無關文法。有人因此提議叫 Pāṇini–Backus Form。這個名字沒有成為主流,但提醒一件有用的事:BNF 不是從石頭縫裡蹦出來的。人類很早就發現,語言可以用「名字展開成片段」來描寫;電腦只是第一次迫切需要一份大家都能簽字的展開表。

BNF 到底在寫什麼

BNF 描寫的是上下文無關文法(context-free grammar)。名字聽起來嚇人,意思卻很日常:一條規則要不要用,只看你現在手上這個名字,不看它左邊鄰居是誰、右邊隔壁姓什麼。

Matt Might 把一條規則拆成兩截:一個名字,以及這個名字的展開。數學書裡常寫成箭頭

名詞片語 → 冠詞 名詞

於是「the dog」可以被承認是一個名詞片語。換成程式,同一手勢變成

運算式 → 運算式 + 運算式

BNF 只是把「可以展開成」寫成比較像規格書的符號:

<name> ::= expansion

::= 讀作「可以是」或「可以換成」。有人叫左邊那個名字非終結符(non-terminal):它還不是成品,只是一張待填的標籤。真正出現在程式裡、不能再拆的字面量,叫 終結符(terminal),例如 "+""begin"、數字 7

經典寫法裡,非終結符一律用尖括號< >包住,左邊右邊都包,以免跟語言裡自己的符號打架。展開式靠兩種動作拼起來:

  • 並置:兩個東西寫在一起,就表示先出現這個、再出現那個。
  • 選擇:直杠 | 表示「左邊或右邊,挑一個」。

就這樣。沒有迴圈關鍵字,沒有「重複三次」的專用符號。若要重複,就讓規則叫自己的名字——遞迴。ALGOL 60 報告裡,無號整數幾乎是教科書第一題:

<digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

<unsigned integer> ::= <digit>
                     | <unsigned integer> <digit>

第一條說:一位數就是那十個符號之一。第二條說:無號整數要嘛是一位數,要嘛是「已經是無號整數,後面再貼一位數」。於是 7 合法,70 合法,708 也合法。你沒寫「任意多位」,規則自己會長出任意多位。

這就是 BNF 最可愛、也最容易讓人一開始不放心的地方:它幾乎不描述「做多少次」,只描述「還能再貼一層嗎」。層數交給展開過程去數。

怎麼讀:從一條算術式開始

Matt Might 文裡那份「經典、而且刻意寫得不含糊」的運算式文法,值得整份看一次。符號換成交際上最常見的 BNF 寫法:

<expr>   ::= <term> "+" <expr>
           | <term>

<term>   ::= <factor> "*" <term>
           | <factor>

<factor> ::= "(" <expr> ")"
           | <const>

<const>  ::= integer

四個名字,分工清楚。<expr> 管加減這一層,<term> 管乘除這一層,<factor> 管括號或數字。乘被放在比加「更裡面」的名字裡,於是 3 * 7 + 1 不會被讀成「先加再乘」——不是因為我們寫了「乘優先」,而是因為文法的分層已經把優先序編進去了。

現在問一個很小的問題:3 * 7 為什麼算合法運算式?Might 的答法是一路展開,像拆禮物:

  1. <expr> 可以是 <term>
  2. <term> 可以是 <factor> "*" <term>
  3. 右邊那個 <term> 再收成 <factor>
  4. 兩個 <factor> 都收成 <const>
  5. 兩個常數分別是 37

走完這條路,紙上剩下的正好是 3 * 7。沒多符號,也沒少符號。這就叫「這個句子由文法生成」,或反過來說「這串字可以被這份文法接受」。

讀 BNF 時,手邊最好準備兩種動作:

  1. 往下長:從起始符號出發,把非終結符換成右邊的展開,直到只剩終結符。這是在問「這份語言裡有哪些句子」。
  2. 往上收:拿一串現成的字,問能不能找到一棵對得上的展開樹。這是在問「這句話合不合法」。編譯器前端做的,大致就是第二種,只是它還要在對上之後幫你長出語法樹。

第一種像依照食譜做菜。第二種像吃到一道菜,倒推它用了哪一頁食譜。同一份 BNF,兩種讀法都成立。

自己寫一份:從電話號碼到命題公式

寫 BNF 最穩的順序不是先追求漂亮,而是先問三個問題:最小的磚塊是什麼?磚塊怎麼拼成一塊牆?牆要不要允許再往上加一層?

先來一個生活裡的例子。假設我們只承認台灣常見的市話寫法:區碼兩到三位,後面一串數字,中間可以有連字號。

<digit>      ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
<digits>     ::= <digit> | <digits> <digit>
<area-code>  ::= <digit> <digit>
               | <digit> <digit> <digit>
<phone>      ::= <area-code> <digits>
               | <area-code> "-" <digits>

02-23622737 對得上。02-ABC 對不上,因為 A 不是 <digit>。文法在這裡扮演的不是審美委員,是門房:只回答「進不進得來」。

再寫一個比較接近數學筆記本的例子——命題公式。原子命題用字母,連詞只留否定、合取、析取、蘊涵,必要時加括號:

<atom>    ::= "p" | "q" | "r"
<formula> ::= <atom>
            | "~" <formula>
            | "(" <formula> "/\\" <formula> ")"
            | "(" <formula> "\\/" <formula> ")"
            | "(" <formula> "->" <formula> ")"

於是 p 是公式,~p 是公式,(p /\ q) 是公式,(p /\ q) -> r 還差一對括號——照這份文法,外層蘊涵也得包起來,寫成 ((p /\ q) -> r)。這不是文法在挑剔,是它拒絕用「你應該知道我的意思」來補句子。

如果你之後要在證明助手裡自訂一套對象語言,BNF 往往是紙上的第一張圖。機器裡真正出現的是歸納型別:每個 | 對應一個建構子,遞迴的名字對應型別自己出現在自己的欄位裡。BNF 管 「字長什麼樣子」,歸納型別管「資料長什麼樣子」。兩張圖對得上,後面的推論才不會建築在口語的縫隙上。

寫 BNF 的小癖好:起始符號先定下來;終結符能加引號就加引號;同一個概念不要用兩個名字;遞迴發生在你真正想重複的地方,而不是因為一時想不出怎麼寫「一列東西」。

一份可以描寫自己的文法

Might 文裡有一段很好玩:BNF 規則自己也可以有文法。稍微整理成比較好讀的樣子:

<rule>      ::= <name> "::=" <expansion>
<name>      ::= "<" <identifier> ">"
<expansion> ::= <expansion> <expansion>
              | <expansion> "|" <expansion>
              | <name>
              | <terminal>

這不是在玩套娃。它在提醒:BNF 也是一種語言,所以它自己也有語法。後設語言並不神秘,只是把「我們用來講話的那層」也寫成規則。當然,這份小文法有點太寬——它沒處理括號優先,也沒禁止無限往 左長。規格書若要給機器吃,後面通常還會再收緊。

識別名、數字、空白這些「字詞一層」的東西,BNF 常常不硬扛。Might 的說法很實在:這類終結符的集合,多用正規表示式或乾脆用散文定義。BNF 擅長的是樹狀結構,不是「這串空白要不要折起來」。

之後的發展:大家嫌它不夠寫,就開始加配件

經典 BNF 故意很瘦。瘦的代價是羅嗦。要寫「可有可無的負號」或「零個或多個參數」,你得額外發明一個名字,再讓它遞迴一次。人一眼能懂的事,紙上卻要繞一圈。

於是配件開始出現。

1977 年,Niklaus Wirth 在 CACM 登了一篇短文,標題幾乎是抱怨:What Can We Do About the Unnecessary Diversity of Notation for Syntactic Definitions? 程式語言愈出愈多,語法記 法卻每人一套,差異小到讓人生氣,又大到讓人每次重學。他提出的折衷,後來被稱為 EBNF(Extended BNF)。常見的加料是:

寫法 意思
[ x ] x 可有可無
{ x } x 可重複零次或多次
( x | y ) 先把選擇括在一起,再跟別的東西並置

於是 Might 文裡那兩個例子,讀起來就不再需要額外的輔助名字:

<term> ::= [ "-" ] <factor>
<args> ::= <arg> { "," <arg> }
<expr> ::= <term> ( "+" | "-" ) <expr>

EBNF 並沒有讓語言變得「更強」——能生成的句子,經典 BNF 理論上也能生成。它讓人少發明幾個只出現一次的臨時名字。ISO/IEC 14977:1996 後來把 EBNF 收成國際標準;2023 年還確認過一次,證明這 套配件夠用,也證明標準文件的壽命可以比許多程式語言長。

網際網路這條線走了另一個方言。ABNF(Augmented BNF)出現在 IETF 的 RFC 裡,現在以 RFC 5234 為主。協定規格在乎的是位元組、大小寫、重複次數的上下限,所以 ABNF 讓你寫 2*3DIGIT 這種「最少兩位、最多三位」的數字,註解用分號,規則名通常不分大小寫。讀 HTTP、郵件、URI 的規格,看到的多半是它,不是 ALGOL 報告裡的尖括號。

同一時期還有別的親戚。Wirth 自己在 Pascal 文件裡用過語法圖(鐵路圖):眼睛沿著軌道走,比純文字更直觀。ALGOL 68 嫌 BNF 不夠細,改用 van Wijngaarden 的兩層文法,精確很多,讀起來也累>很多。後來的程式語言手冊,有人繼續寫 BNF/EBNF,有人改用 PEG、有人直接把 parser combinator 當成規格。萬變不離那個核心:先有一份大家認帳的結構描寫。

再往實作走,BNF 幾乎變成編譯器的出生證明。yacc、bison、ANTLR 這類工具吃進去的,就是某一種方言的文法;吐出來的是剖析器。你在紙上寫 |,工具在程式裡寫分支。人負責把語言想 清楚,機器負責不讓非法句子混進後門。

BNF 不負責的事

讚美寫完,該把邊界講清楚。否則 BNF 很容易被當成萬能符。

第一,它只管語法,不管意思。你可以寫出一份承認 1 / 0 的運算式文法。文法說這串字合法;數學說這串字還沒有值。ALGOL 報告自己也把語法和語意分開寫,不是偶然。

第二,它預設規則不看上下文。變數「先宣告再使用」、同一識別名不能宣告兩次、型別要兩邊對上——這些都不是上下文無關文法擅長的活。程式語言通常的分工是:BNF 先把樹長出來,比較勢利的檢查留 給之後的靜態分析。

第三,同一份語言可以對應很多份文法,有的友善、有的會打架。經典例子是「懸空的 else」:一個 else 該掛在哪一個 if 上,兩棵樹都合法,程式的意思卻不同。這時問>題不在 BNF 這套記號,而在你寫出來的規則允許太多故事同時成立。解法是改規則,或另外規定「就近搭配」這種閱讀慣例。

第四,BNF 的方言很多。有人保留 ::= 和尖括號,有人改成 = 然後替非終結符去掉括號;有人用引號包終結符,有人靠字體區分。讀一份新規格,先花三十秒看它的圖例,比 爭論「正宗 BNF 長什麼樣子」有用。

怎麼用,才不算白認識它

認識 BNF 之後,比較實用的不是立刻去發明語言,而是先學會當一個有禮貌的讀者。

拿到一份規格,先找起始符號,再找終結符表。然後挑一句你以為自己懂的句子,用手走一次展開。走得通,這份文法才開始對你說話;走不通,多半不是你比較笨,是規則在某個 | 後面藏 了例外。

若要自己寫,從最小的合法句子開始,再加一層結構。先讓 p 通過,再讓 ~p 通過,再讓帶括號的合取通過。每加一條規則,就準備一句應該被拒絕的反例。p /\ q /\ 這種尾巴空著的東西如果也能混進來,不是語言變寬了,是門房打瞌睡。

最後才考慮要不要換成 EBNF。配件能讓規則變短,也能讓人忘記遞迴到底發生在哪裡。短不是目的;可核對才是。

Might 的文章把常見記法收成一份對照,讀完確實能「在野外辨認文法」。本篇多走的那一截,是它背後的社交史:BNF 出現,不是因為有人突然想玩符號,而是一群人必須在紙上停止互相誤會。程式語言 後來長出這麼多方言,規格書卻還在用差不多的尖括號——這大概是那次誤會被真正修好的證據。

語言會繼續增加。描述語言的語言,從 1959 年到現在,核心還是那一行:

<name> ::= expansion

左邊是我們想談的東西,右邊是它被允許長成的樣子。中間那支 ::=,只是把「我以為你懂」換成「你也可以檢查」。對寫規格的人來說,這已經很接近文明了。


主要依據:Backus 1959 年 UNESCO 論文與 ALGOL 60 報告的語法一章;Naur 後來關於 ALGOL 編輯過程的回憶;Knuth 1964 年 CACM 投書;Wirth 1977 年關於語法記法的短評;以及 Matt Might, The language of languages 對 BNF/EBNF/ABNF 的整理。歷史細節若有出入,以原始報告與信件為準。

熱門文章