前言
與人類社會的曆史相比,計算機的曆史非常短暫,上世紀五、六十年代都能稱為遠古時期了。但計算機的曆史又很神奇,早期的思想往往都很超前、很先進。比如EJB技術雖然是1998年提出的,但它的設計很超前,諸如微服務等後面出現的技術都或多或少借鑒了它的思想。通過了解計算機技術的發展曆史,往往能從中找到很多有創意的想法,能幫我們解決當下的問題。是以,今天想來掰扯一下Emacs和Vim這兩款經久不衰的老古董軟體的曆史八卦,看看有沒有值得借鑒的地方。
Vim的八卦
Vim族譜
首先從Vim編輯器的起源說起,下圖Vim的族譜:

Vim的前身是ed編輯器:
- ed是UNIX系統上最古老的程式之一,從第一版本開始就入駐了,作者是Ken Thompson(UNIX作者之一)。它提供了面向行(Line)的基本編輯指令。
- ex是ed的超集,是Bill Joy(Sun公司創始人之一)在開發BSD時增強了ed,于是取名叫ex。但ex仍然是面向行的編輯器。
- Bill Joy後續又為ex提供了可視化界面(Viusal Interface),提供全屏編輯能力,是以命名為vi。
- 為了将vi移植到Amiga機器,Bram Moolenaar開發了Vi IMitation(Vi仿制品)。随着功能的不斷增加,名字也更新為Vi IMproved(Vi改良版),即Vim。
ed編輯器
ed與VSCode、Sublime Text等現代編輯器有很大不同,如前文所說,它是一款行編輯器(此處已幫大家劃重點),即編輯的對象是一整行文本。
ed分指令模式與編輯模式。啟動ed後,預設進入指令模式,等待使用者輸入一條條指令。ed通過執行這些指令,最終達到編輯檔案的目的。使用Mac電腦的同學可以試試在終端裡執行ed。ed指令的格式是
[尋址][指令]
:
- 尋址:選中待操作的目标行。ed提供了三種尋址方法:
- 行号:從1開始的整數;$代表最後一行。
- 模式:選中與正規表達式比對的行。
- 預設從目前行開始,選中第一個比對的行。如
。/re/
- 添加字首
,則做全局比對。如g
g/re/
- 預設從目前行開始,選中第一個比對的行。如
- 範圍:由兩個位址組成的尋址範圍,
。如[位址],[位址]
/BEGIN/,/END/
- 指令:用單個字元表示。以下是最常用的指令:
-
:展示,輸出目标行。p
-
:插入,将内容插入到目标行的上一行。i
-
:追加,将内容追加到目标行的下一行。a
-
:更改,替換目标行的内容。c
-
:删除,删除目标行。d
-
:替換,用正規表達式替換比對行内容。s
-
其中
i
、
a
c
指令會使ed從指令模式進入編輯模式,在編輯模式中輸入一行
.
則傳回指令模式。以下是ed編輯的幾個示例:
- 删除所有空行:
。用字首g/^$/d
全局搜尋正規表達式g
,并執行删除指令。/^$/
- 輸出所有包含“re”的行:
。同樣全局搜尋正規表達式g/re/p
,并執行展示指令。因為該功能實在太常用了,是以還特地開發了一個指令“grep”。/re/
編輯器思維
如前文所說,ed編輯器與現代編輯器很不同,它其實是一個編輯指令解釋器;但ed編輯器又與現代編輯器很相同,所有編輯器的本質都是在不斷執行“尋址”與“指令”,不同類型編輯器之間的差異隻是編輯的對象不同:
- ed是文本行編輯器:編輯的對象是文本行。
- Microsoft Word是文檔編輯器:編輯的對象是章節、段落、詞句等文檔元素。
- Sketch是圖形編輯器:編輯的對象是點、線、面等圖形元素。
- IntelliJ IDEA包含Java代碼編輯器:編輯的對象是類、方法、語句等Java語義元素。
- jQuery是DOM編輯器:編輯的對象是DOM元素。先用CSS Selector尋址,選中要處理的DOM元素;再用連綴表達式執行一系列編輯動作。
- ……
由此可見,編輯器思維無處不在,隻要符合“尋址+指令”模式都可稱作編輯器,是以萬物皆可編輯!編輯器思維或編輯的本質,用開發者更熟悉的話術來講就是CRUD:
若以後有人質疑開發同學隻是在做簡單的增删改查,請勇敢地告訴他們:其實我是在做一個垂直領域的編輯器!
若意識到自己在做的其實是一個編輯器,就能利用編輯器思維快速發現系統能力的短闆。以商品管理系統為例,若商品管理隻提供通過ID查詢商品的功能,就猶如ed編輯器隻支援用行号來尋址一樣,使用就非常不友善,可以借鑒ed通過正規表達式的模式比對尋址能力,提供通過商品名稱等資訊來比對商品、甚至通過商品照片來比對相似商品的能力;類似的,建立商品能力也可以借鑒編輯器複制粘貼的能力,提供用相似商品快速建立商品的能力,甚至還可以提供從其他平台搬家的能力。
ed的族譜
前文隻介紹了ed互動式編輯的功能,其實ed還支援腳本化編輯,就是将輸入到終端的編輯指令儲存成一個腳本檔案,供後續反複運作。好處是可以用相同的編輯指令批量編輯任意多個檔案。
上圖是ed編輯器的族譜,後續的衍生程式都是選擇并增量了ed的部分能力。比如:
- ex、vi、vim這條分支選擇了互動式路線。
- grep、fgrep、egrep選擇了模式比對路線。
- sed、awk選擇了腳本化路線。
Emacs的八卦
從Vim陣營叛逃
我曾經是一名Vim重度使用者,因為在大學利用的作業系統是Debian Linux,無論是寫C代碼還是Java代碼,都是在Vim裡一把梭。好處是閉卷筆試時可以直接默寫,而用Eclipse的同學基本是記不住JDK API的全名。:-p
畢業後進了一家外企,不得不開始使用Windows XP系統,某天在記事本裡寫東西,發現自己會經常無意識地按一下Esc鍵。用過Vim的同學肯定知道,這是在切換模式。這我意識到:Vim這種多模式的設計非常反人類。Vim啟動時預設進入的不是編輯模式,當新手使用者什麼都還沒學會時,他沒辦法把Vim當成普通的記事本來用。曾有一則關于Vim的笑話,說如何獲得一串随機碼,答案是讓Vim新手嘗試退出Vim。
這種方式不符合我的口味,我嘗試去尋找新的編輯器——當什麼都沒學會的時候,可以當成最普通的記事本來用;當需要進階功能時,再通過快捷鍵等方式呼喚出來。結果發現Emacs恰好符合這個要求,是以從2010年開始我就從Vim陣營叛逃到了Emacs陣營。我認為這個使用方式的差異是Vim與Emacs最本質的差別:Vim會強迫使用者從一開始就按照它的規則來做事情;而Emacs則相對不需要過多前置知識。網絡上曾流傳過一張編輯器的學習曲線,還蠻貼切的:
插圖源自:
https://coolshell.cn/articles/3125.htmlEmacs的起源
Vim的前身ed源自UNIX系統,而Emacs的前身TECO源自UNIX系統的前身——Multics系統。
上世紀70年代,GNU的創始人Richard Stallman在MIT的AI實驗室打工時,發明了TECO編輯器,運作在PDP-10機器上。與ed類似,TECO也是指令解釋器——接收并執行編輯指令——并且也采用單個字元作為指令名稱,比如“l”是移動一行,“5l”是移動5行。MIT那群大佬們想用TECO指令完成一些複雜的編輯工作,于是加入了分支判斷、循環等功能;但由于先天不足,TECO最開始設計的時候,沒有把指令設計成一套完備的程式設計語言,導緻後續改進也很困難,比如指令名稱隻能是單個字元,很快字元就不夠用了。
所謂基礎不牢地動山搖,大夥兒都認為需要用一套嚴謹完備的程式設計語言替代TECO的半成品腳本語言。于是有一位叫Bernie的教授在Multics系統上用MacLisp重寫了TECO,并命名為Emacs,還為它寫了詳細的手冊,教大家如何擴充這個編輯器來滿足自己的工作需要。結果,這個版本的Emacs取得巨大成功,連Bernie的秘書——一個号稱自己不懂程式設計的人——都在照着手冊,有模有樣地寫Lisp代碼來擴充編輯器功能。這件事兒在實驗室引起轟動後,Bernie為此做了一個總結:如果有一個應用——一個能幫你做點有用事情的程式——内嵌了Lisp,并且能通過Lisp程式擴充它的功能,對于學習程式設計而言,這是一種非常不錯的入門方式!那些自認為不會程式設計的人,這種方式會給他們編寫小但有用的程式的機會,讓他們在實踐中不斷成長,直到他們發現自己就是在程式設計。
Stallman他們覺得這個想法簡直屌炸天!同時他們想把這個好用的Emacs版本遷移到Multics系統之外的其他系統,但當時隻有Multics系統上有完備的Lisp環境——既有編譯器又有解釋器——諸如UNIX等系統上都沒有。
這裡還有一個小插曲,Java之父James Gosling當年還寫了一個能跨平台的Emacs版本,叫Gosmacs。本來社群想來一起完善這個版本,結果Gosling把它賣給了一家商業公司,同時它底層的Lisp不是一個真實完備的Lisp,而是一個叫Mocklisp的假Lisp,隻是文法上和Lisp長得像而已。是以社群最終放棄了這個選項,決定從頭開始做一個全新的Emacs,也就是GNU Emacs。Stallman先用C語言開發一個跨平台的Lisp解釋器——Emacs Lisp,再用Lisp實作編輯邏輯。這樣既能在所有平台上用統一的Lisp方言來寫Emacs擴充,又能兼顧性能。
GNU Emacs有一段時間發展比較之後,因為Stallman自己一個人忙不過來,是以社群又建立了一個分支叫XEmacs,增強了字型抗鋸齒等功能。後來GNU Emacs的維護又變得積極了,把很多XEmacs的特性合并回GNU Emacs,是以現在XEmacs差不多是廢棄狀态,主流版本還是GNU Emacs。
系統設計
編輯器聖戰
程式員的世界裡充滿了鄙視鍊,有編輯器鄙視鍊、程式設計語言鄙視鍊、作業系統鄙視鍊……為什麼這些聖戰永遠打不完,到底是像《格列夫遊記》裡小人國因争論剝雞蛋先打破大頭還是小頭而發動了戰争,還是真的魚和熊掌不可兼得?
前文提到,Vim喜歡強迫使用者按照它的套路來做事。Vim從ed繼承了行編輯器的特性,底層模型是基于“行”的,是以會強行要求所有被編輯的對象适配成它的底層模型。你用Vim寫Java代碼,你編輯的是文本行;你用Vim寫一篇部落格,你編輯的是文本行;你用Vim寫一篇論文,你編輯的還是文本行;無論你編輯的是類、函數、段落、目錄還是任何其他内容,都要先在腦海中翻譯成對應的
dd
yy
等面向行的編輯指令。
Emacs則是允許使用者先把Emacs改造成目标對象的個性化編輯器,能認識目标模型,比如段落、章節、目錄等。用一句時髦的話講就是Emacs有行業Know-How。同樣的例子:用Emacs寫Java代碼,你編輯的是類、方法、語句……;你用Emacs寫一篇部落格,你編輯的是段落、句子……;你用Emacs寫一篇論文,你編輯的是目錄、章節、正文、索引……。
兩種設計方法
造成上述差異的原因是背後兩種不同的設計方法,分别稱作自頂向下(Top Down)與自底向上(Bottom Up):
方法 | 自頂向下 | 自底向上 |
---|---|---|
描述 | 将大任務逐級拆分到顆粒度合适——足夠小、又能做些實際的事情——的小任務 | 完善底層程式設計語言等——讓底層基建不斷逼近業務領域——來适應任務 |
優點 | 難度較低,目标明确,疊代快速 | 功能完整,适應性強 |
缺點 | 與目前需求耦合過緊,應對變化能力稍弱 | 難度較高,進展較慢 |
用Vim編輯屬于自頂向下方法——将編輯任務持續拆分,最終拆解到面向行的編輯指令;就像Java日常開發,會逐級拆分,最終拆解到JDK的API。用Emacs編輯屬于自底向上方法——先完善底層Emacs Lisp語言,逐漸抽象出面向業務的領域特定語言,最終用DSL完成編輯任務;例如要編輯Markdown文檔,就會提供諸如移動到下一個段落、下一個清單項、表格下一個單元等面向Markdown領域的特定編輯操作。
這兩種設計方法的差異并不意味着隻是換個順序寫代碼,而是系統抽象過程的差異,最終展現在系統擴充性的差異上。我個人把系統的可擴充分成4個等級:
- 寫死:系統運作時,資料和行為都已寫死,不能變化。
- 可配置:系統運作時,資料可動态變化,但行為固定不變。
- 可控制:系統運作時,資料可動态變化,并且由多種預定義的行為可供動态選擇。
- 可程式設計:系統運作時,資料可動态變化,同時行為可在運作過程中動态新增,即使用者可重新系統行為。
自頂向下的極端是寫死,會過早地把功能限制在目前的需求裡,後來的需求隻能盡量逼近初始模型;自底向上的極端是可程式設計,容易過渡設計,為未來不可能變化的場景提供靈活性,甚至會變成一門通用的程式設計語言。
兩種設計方法沒有絕對的對錯,都有各自适用的場景,單一地采用任何一種方法都會有問題,需要根據實際情況在快速實作和系統擴充性之間做權衡。也正因為沒有對錯之分,是以編輯器的聖戰永遠也打不完。