編程珠璣番外篇

1.Plan 9 的八卦

在 Windows 下喜歡用 FTP 的同學抱怨 Linux 下面沒有如 LeapFTP 那樣的方便的工具. 在蘋果下面用慣了 Cyberduck 的同學可能也會抱怨 Linux 下面使用 FTP 和 SFTP 是一件麻煩的事情. 其實一點都不麻煩, 因爲在 LINUX 系統上壓根就不需要用 FTP. 爲什麼呢? 因爲一行簡單的配置之後, 你就可以像使用本機文件一樣使用遠程的任何文件. 無論是想編輯, 查看還是刪除重命名, 都和本機文件一樣的用. 這麼神奇的功能到底如何使用呢, 待我一一道來.

首先, 如果你的內核版本是 2.6.14 或者更新(uname -r 可以查看你的內核版本), 你的機器上的已經有了這個支持了. 在這種情況下, 你可以使用:

sudo modprobe fuse

激活內核模塊. 然後, 比如說, 你想SSH遠程 remote.com 上的 /dir 目錄, 作爲本機的 ldir 目錄, 你可以使用:

sshfs [email protected]:/dir /ldir

然後, 您就可以完全在 ldir 目錄下操作任何的遠程機器上的文件了. 是不是很簡單 :)

如果要使用 ftp, 我們要使用一個叫做 curlftpfs 的開源軟件. 幸運的是, 你可以直接使用 apt-get install curlftpfs 直接得到他.

安裝結束後, 我們依然使用

curlftpfs ftp://user:[email protected]/dir ldir/

就可以把遠程的目錄當成本地的目錄一樣使用的. 拷貝啊重命名啊都可以. (當遠程的內容直接能被你訪問的時候, 誰拷貝啊 :)

說到這裏, 聰明的讀者肯定要說: 靠, 遠程的文件都能當成本機的用, 那太好了, 整個互聯網就是我的磁盤嘛. 你還真說對了, 對於一個用戶來說, 協議是不重要的, 重要的是數據. Gmail 不是有N個Gb的大小麼, 讓我們直接把 gmail 當磁盤好了. 於是有了 Gmail FS. 這樣, 你可以給你的郵件標題做支持正則表達式的查找噢 :)

除此, 還有 wikipediaFS, 可以用自己喜歡的編輯器直接編輯維基百科的文章. 還有 flickrFS. 直接用自己喜歡的編輯器可以編輯圖像元信息, 還能 chmod 754 讓圖像只能被朋友訪問噢 :)

以上的這些酷實現, 都是 FUSE 這個內核模塊作爲基礎支持的. 那麼, 這個牛逼的網絡也是文件的點子是哪個牛逼的人想出來的呢? 答案是 UNIX 的發源地, 貝爾實驗室. 確切的說, 是在設計 UNIX 的繼承者– Plan 9 的過程中提出來的. (小八卦: UTF-8, 就是世界上現在最通用的字符編碼體系, 就是 Plan 9 操作系統的一個副產品).

一切都是文件這個點子倒不是什麼新的點子, 在 The Unix Programming Environment 這本聖經裏面就旗幟鮮明的打出 “UNIX下一切都是文件”的旗號. 但是畢竟旗號是旗號, 現實是現實. 關於文件的抽象在其後的發展中發現了這樣那樣的侷限, 於是惡名遠揚的 ioctl 函數被引進系統. 所以口號是口號, 實現是實現. 顯然當初設計 UNIX 的幾個哥們很不滿意, 所以搞了一個 Plan 9 (搞 Plan 9 的人就是寫 The Unix Programming Environment 的人). 在 Plan 9 系統中, 一切都是文件, 顯卡是文件, 內存是文件, 進程是文件, 網絡是文件…

等等, 熟悉 Linux 的用戶要說了, 在 Linux 下面, 進程的確是文件啊. ls /proc 就能看到當前所有的進程, 不也是文件麼. 是呀, Linux 這個就是和 Plan 9 學來的. 但是 Linux 學得不徹底, 因爲這個文件系統的訪問接口並不完整: 你不能通過 rm 一個文件殺死一個進程. 也不能通過 cp 拷貝出一個進程. 而在 Plan 9 上, 你不光能通過普通的文件操作命令去控制進程, 你還能 mv 一個進程. 我們剛纔說了, 進程就是文件, 還能把其他機器上的文件當成本機一樣用. 屏幕前聰明的你肯定一下子悟到了: 一定這居然是一個分佈式的操作系統啊! 在這個系統裏, 進程可以被其他計算機看到, 並且控制. 而管道可以橫跨不同機器的不同進程, 這簡直是把單機的概念都給抹殺了, 簡直就是”網絡就是計算機啊”.

熟悉互聯網的讀者都知道最近炒得很熱的雲計算啥的, 對用戶無非就是互聯網作爲硬盤, 對公司無非就是分佈式的操作系統協同工作, 在屏幕前的您肯定如我一樣, 一拍大腿說: 靠, 貝爾實驗室養得不是計算機科學家, 而是一大批未來學家啊. 30多年前人家搞互聯網, 而今我們用互聯網. 20年前人家用GUI, 如今我們用GUI. 10年前人家搞雲計算, 而今我們炒概念閒扯淡. 差距啊.

說到這裏就到了今天技術八卦的尾聲了: 當年設計 Plan 9 的牛人 bwk 和 Rob Pike 等哪去了?
bwk (別查了, 就是那個寫 C語言書的, 寫AWK的) 年紀大了去普林教書了(人家本科生能不牛逼麼). 而 Rob Pike 年輕, 風華正茂正當時. 您想,對於那些做雲計算的大公司來講, 這麼牛逼的人哪能落入對手手中? 您這麼想的, Google 也是這麼想的. Rob 同學現在在 Google 研究院很爽的做着主力工程師呢. (小八卦, 他是Google 中唯一享有單字母郵箱的: [email protected]).

===================================

2.計算機圖書排版的八卦

大家都知道, 計算機科學家超級愛動手自己開發工具, 而且對美有超乎常人的需求. Knuth 爺爺當年覺得自己辛辛苦苦的好書被排版成地攤上的廁紙一樣, 一怒之下自己搞出了紅遍大江南北的 TeX. 從此整個世界都清淨了. 排版是計算機科學家研究的一個很好玩的領域, 這篇文章就談談我所知道的關於排版的八卦.

先說 Knuth 爺爺的吧. 首先, 是在設計 TeX 的過程中, 這位老爺爺研究了很多著名的字體, 成了名動一時的字體專家, 據說和喬布斯並稱爲加州最懂字體設計的兩個搞IT的 (我瞎說的). 研究字體之餘, 他就研究收集各大書法家的作品, 然後這位老爺爺又是一個基督教徒, 所以乾脆用它的收藏出了一本書, 叫做 <3:16>. 這本書特別牛逼, 是一本用計算機科學研究上帝存在的. 而且發揮計算機科學的小幽默, 取聖經每章的第3節第16小句, 還證明了這個和隨機一樣好.

還是克爺爺, 寫完TeX之後不過癮, 要寫本書來沖沖喜, 於是寫出了極其牛B名字的 The TeXbook. 一語雙關, 表現了牛人一貫的狂妄. 寫完這個他又想寫寫自己的字體和繪圖系統設計(metafont 系統) 所以乾脆出了五卷書, 行話稱作ABCDE, 也是用名字來表明: 看, 基本的入門書, 你非看不可.

跑題一下: Knuth 爺爺最喜歡讓人家看到他提出的名字就腿發軟. 比如他提出了一個叫做 Literate Programming 的東西, 並且很不懷好意的對 Dijkstra 說, 小樣, 當年你說 structured programming 的時候我非要用 goto, 結果人家都說我是 unstructured programming (沒結構的編程), 現在我要提出一個叫做 literate programming 的東西, 你要是不跟着我混, 人家就會叫你 illiterate programming (沒文化的編程). 在這麼邪惡的名字下, 全世界程序員只好個個聽這個老頭的話, 乖乖的使用文檔和程序融爲一體的”有文化的”編程習慣.

其實克爺爺屬於斯坦福家族的. 在70-80年代, 世界上還有一個NB的研究機構: 貝爾實驗室. 貝爾實驗室自己也開發了自己的排版工具: Troff. 開發者是著名的K, 就是 K&R 裏面的那個K. 這個 Troff 也是一個牛到極點的排版軟件, 比如說, 當年那些科學家都對出版社的排版不滿意, 所以都威脅出版社說: 我自己來排版, 你們只管印刷就行了. 就是因爲這幫科學家開了這個傳統, 所以後來出版商遇到想自己排版的, 都用巨崇拜的眼光打量着你.

說到 troff, 以下大名鼎鼎的書都是用 troff 排版的:

Advanced Programming in the UNIX Environment
The AWK Programming Language
The C Programming Language
Compilers: Principles, Techniques, and Tools
Computer Networks, 3rd Ed.
Computer Networks And Internets, 3rd Ed.
The Design and Implementation of the 4.4BSD Operating System
Effective TCP/IP Programming
The Elements of Programming Style, 2nd Ed.
Internetworking With TCP/IP Volume 123
More Programming Pearls
The Practice of Programming
Software Tools
Unix Network Programming
The UNIX Programming Environment
Programming in C++

所以說, troff 排版的無爛書. 當然, TeX 家族也不是吃素的, SICP, TAoCP, CLRS 都是用 TeX 搞出來的. 陶哲軒也說, 鑑別民科文章第一步就是看是不是用TeX排版的. 可見排版排得專業, 也是好文章的一個先決條件.

我覺得可以把以上的結論概括成 徐氏排版定理, 如果一本書, 不是以上所說兩個軟件排版的, 又不是 O’Relly 出版的, 那是好書的概率也就不怎麼大了. 作爲一個作者來講, 一定要記得用 troff 或者 latex 排版 :)

troff 和 latex 都是一脈相承的, 理念也差不多, 所以牛B的開發人員兩頭都在玩, 比如一個叫做 Werner Lemberg 的牛人, 就是 troff 的開發人員, 同時還跑到 TeX 那裏開發了支持中日韓的 CJK 包. (大家都知道, 軟件的中文支持從來都不是中國人開發的)

史上最牛的程序員 Bill Joy 同學據說用了一個週末就寫出了 vi, 所以大家都懷疑, 他用了半個小時的時間寫了 BSD 上的 troff. 他寫的這個程序, 被SUN用着, 一直用到今天.

最後強行插播一條廣告: 我最近要寫一本小冊子, 叫做 Motifs in Computer Science (原名叫 Meta Ideas in Computer Science). 一定保證用 LaTeX+Troff+reStructuredText 排版, 按照我的 Troff/Latex 排版無爛書結論, 這本書也不是太爛. 歡迎捧場.

再補充一則八卦: 話說當年 PDP-11小型機特別貴, 但是貝爾實驗室的科學家又想要用. 怎麼辦呢? 於是, 他們發揮了科學家愛忽悠的能力, 去和經理說: 你看, 我們文檔的排版很爛吧(當年還是打字機時代), 你們投資一下搞一個小型機回來, 我們保證給你們開發一個在這個機器上用的文檔排版系統. 經理一聽, 大筆一揮說: 買之!. 科學家一聽都樂了, 哈哈, 我們有新玩具了. 然後, 他們就開始在 PDP11 上開發 UNIX 了. 經理也不懂, 看他們搞的好玩, 就不時來問問: 老大們, 排版系統怎麼樣了? 貝爾的科學家一邊敷衍敷衍, 一邊繼續搞 UNIX 和 C 語言. 等這兩樣都搞好了, 瞬間就寫了一個排版軟件, 就是 nroff. 經理可樂了, 說, 哎, 我們終於投資有回報了啊. 科學家也樂了, 因爲若干年之後, C 和 UNIX 紅遍大江南北, 因此兩人拿下圖靈獎. 所以說, 做研究這東西, 一定要先把基金忽悠過來, 然後想幹啥幹啥, 最後結果反而超出預料. (貝爾實驗室的人居然研究宇宙背景輻射拿諾貝爾獎, 這種寬鬆寬容的基礎研究在其他地方是很難遇到的).

==================================

3 — 關於程序優化的八卦

<代碼大全> (Code Complete) 是一本很好的書. 我建議像我這樣寫的程序總行數不超過50萬的程序員應該買一本放在案頭 (當然<代碼大全> 不如 <Software Tools> 這本書好, 這個我以後有機會寫文章細談) 如果你天天編程序, 我建議你買一本. 如果你已經有了<代碼大全>, 我誠心建議你趕快翻開此書, 撕去第26章. 因爲代碼大全的其他章節可以讓你成爲優秀的程序員, 唯獨第26章, 讀了之後立即從優秀程序員變成最差的程序員.

爲啥? 因爲第26章講的, 都是怎麼調節代碼使得代碼跑得更加快的技巧, 而這些技巧, 幾乎都是讓一個好程序變成差程序的技巧, 是教你不管三七二十一先對程序局部優化的技巧. 而局部優化是讓程序變得糟糕的最主要的一個原因. 用高爺爺的話說, 提前優化是萬惡之源 (Premature optimization is the root of all evil). 這些技巧, 就是帶你去萬惡之源的捷徑.

代碼優化究竟是什麼洪水猛獸, 又究竟有多少偉大的程序員因爲代碼優化聲名掃地, 請看本期關於代碼優化的八卦.

話說當年在貝爾實驗室. 一羣工程師圍着一個巨慢無比的小型機發呆. 爲啥呢, 因爲他們覺得這個機器太慢了. 什麼超頻, 液氮等技術都用了, 這個小型機還是比不上實驗室新買的一臺桌上計算機. 這些傢伙很不爽, 於是準備去優化這個機器上的操作系統. 他們也不管三七二十一, 就去看究竟那個進程佔用CPU時間最長, 然後就集中優化這個進程. 他們希望這樣把每個程序都優化到特別高效, 機器就相對快了. 於是, 他們終於捕捉到一個平時居然佔50% CPU 的進程, 而且這個進程只有大約20K的代碼. 他們高興死了, 立即挽起袖子敲鍵盤, 愣是把一個20K的C語言變成了快5倍的彙編. 這時候他們把此進程放到機器上這麼一實驗, 發現居然整體效率沒變化. 百思不得其解的情況下他們去請教其他牛人. 那個牛人就說了一句話: 你們優化的進程, 叫做 System Idle.

所以說. 優化這東西, 一定要有一個全局的思路, 否則就是純粹的無用功, 有時候還是負功. 在<編程珠璣 II> 第一章, Jon Bentley 就着重提醒了代碼 profiling 的重要性. 說到 profiling 這個詞, 就不能不再次提到萬衆敬仰的高爺爺. 高爺爺在1970年的暑假, 通過撿Stanford 大學機房扔出來的垃圾(其實是含有程序的磁帶), 寫出了一篇震古爍今的論文 “An empirical study of FORTRAN programs” (FORTRAN 程序的實證分析). 除了抱怨寫程序的人不看他的 TAoCP 之外(因爲一個程序用了被高爺爺定性爲史上最差的隨機數發生器算法, 有興趣的可閱讀 TAoCP vol2), 這篇論文主要說了三個劃時代的東西:

1. 對程序進行 profile 是每個編程系統的居家旅行必備.

2. 在沒 IO 操作的情況下, 一個程序中 4% 的代碼佔用了超過50% 的運行時間.

3. 97% 的情況下對程序進行提前優化是萬惡之源.

這三個道理, 用大白話說, 就是: 1 程序都存在熱點, 有優化的空間. 2. 但是97%的情況下程序員優化的都是錯的地方, 反而把程序優化糟了. 3. 想要做優化, 第一步就要先知道程序在什麼地方耗時間而不是靠猜.

說到熱點, 順帶拐八卦一下Java的速度. Java 1.5 的虛擬機的關鍵技術, 就是叫做 Hotspot (熱點). 傳統上, 大家都認爲 Java 比C 要慢. 其實不然. Jython 的作者 Jim Hugunin 就曾經說過, 其實兩者差別不大 (http://hugunin.net/story_of_jython.html). 也有一些其他的測評說, Java 比 C 要快. 原因就在於, Java 虛擬機能夠找到熱點, 對熱點專門做優化. 而C程序編譯好了, 即使有熱點, 也只能靠CPU去優化了. Java 的優化比 CPU 要深且更全局.

言歸正傳. 關於 FORTRAN 的 profile 的傳統被繼承了下來, 基本上現在任何的過程式主流編程語言都支持 profiling 工具. 關於 profile 怎麼做的問題, 等我有空了好好寫文章介紹. (因爲我發現, 除了編程珠璣, 沒有一本書提到過).

做程序優化的八卦就太多了, 說一個Beautiful Code 上的吧. 話說世界上做線性代數的庫叫做BLAS, 基本上是工業標準. 因爲線性代數運算太重要了, 所以各大處理器廠商都有 BLAS 的實現. Intel 的叫 MKL, AMD 的叫 ACML. 矩陣乘法實現的好壞, 直接決定了處理器的性能測試的分數(因爲現代測處理器的速度的程序, 比如LAPACK指數, 基本上都是用 BLAS 裏的矩陣乘法做基準). 去年 nVIDIA 高調宣傳自己的 CUDA 系統比CPU廠商快10倍到100倍, 藉此打開了GPU計算的大門(令人髮指的達到500GFlops, Intel 最新的只有50GFlops). 其中 CUDA 可以理解爲是 BLAS 在 nVIDIA 平臺上的實現. 自從nVidia 推出 CUDA 以後, 儼然不把 intel 這些廠商放在眼裏, 心想, 小樣, 你們還是做通用處理器吧, 浮點乘法這些高級的東西, 還是放在顯卡上比較好. nVidia 和 IBM/SONY 的陰謀很不小呢. 要是浮點計算比 Intel 快這麼一兩個數量級, 以後世界上前五百名超級計算機就全部變成什麼用光纖網連起來的 PS3 機羣, nVidia 顯卡機羣之類的. 人家外行見了計算機科學家肯定要問: 你們搞高性能計算機究竟是搞計算還是打網絡遊戲啊??

還是言歸正傳(我怎麼老走題?), 簡要的說一下 BLAS 優化. 單處理器上對 BLAS 的優化主要體現在對 cache 的高效使用. 矩陣乘法中, 如果矩陣都是按照行存儲, 則在A*B中, 對B的訪問是按列的. 假設B一行有N個元素, 那麼在存儲器中, 兩個同列不同行的元素所在的存儲單元相差N. 因此, 對B的訪問並不是局部化的. 因爲訪問不局部化, 所以每次乘法, 都需要從內存中調一個 cache 單元到CPU. 這個極大的降低了處理器的執行速度. 因此, 矩陣乘法的優化的核心, 在於局部化B的訪問. 反過來, 如果矩陣按照列存儲, 則要局部化對A的訪問. 關於怎樣局部化訪問還能獲得正確的乘法, Beautiful Code 一書的第14章有非常好的講解, 我就不廢話了. 總之, 矩陣乘法局部化的好壞, 取決於一個機器的 cache 的大小.

多處理器和向量處理器就又不一樣了. 要想利用好, 就要把計算任務平均的獨立的分到不同的處理器上. 所以, 在這裏, 優化就變成了分解成若干的小問題. 因此, 分治算法成了主流. 具體矩陣乘法怎麼分塊, 大學數學都講了, 我就不廢話了. 事實上, 之所以 nVidia 能和 Intel 幹, 就是因爲顯卡上令人髮指的有 64 個計算核心, 而 Intel 最牛X的才 4個. 那爲啥 Intel 自己不多做幾個核心呢? 因爲 Intel 自己把自己帶溝裏去了 — Intel 處理器太複雜支持的功能太多了, 一塊硅片上根本放不下很多核心. 而 nVidia 一直就是專用處理器, 每個核心功能簡單, 可以做到很小.

Beautiful Code 第14章就是講了隨着計算機體系結構的變化, BLAS 是怎麼進化的. Tanenbaum 曾經說過, 隨着一個科技的出現, 某個 idea 可能就銷聲匿跡了. 但是說不定下一波科技再來的時候, 這個 idea 又復活了. BLAS 從串行, 到向量, 再到串行(帶cache的RISC), 再到向量(Cell), 就是一個絕好的例子. 對這個進化史感興趣的讀者不可錯過這一章妙文.

==================================

4. Linux 下的 Facade 程序

Linux 下的命令行工具大致有兩個流派, 一是以小而精見長的, 只能提供一個簡單的小功能. 比如 yes 這個命令, 除了輸出一大串永不停止的 y 之外毫無用處. 這個工具看上去土, 很沒用處的樣子. 碰到要你一路回車法的時候, 這個工具就大大的有用. 所以我每次幫人使用一路回車法裝 windows 的時候, 就懷戀 Linux 下的這個 yes. 過一個管道, 就省去了在電腦面前按下幾百次 y 的繁複工作. 

還有一種工具, 是我今天要說的重點. 這種工具一般是一個簡單的命令行調用, 卻有着幾十種甚至上百種不同的參數的組合, 用這些參數能搭配出誰也沒用過的功能. 以 gcc 爲例, 居然有兩百多個不同的命令行參數, 範圍涉及到程序編譯, 連接設置, 庫設置, 優化, 報錯信息, 調試信息等等, 任何一個正常的人想要窮盡學完這些參數都是不可能的. 同樣的庫還有 convert (圖像轉換的), ffmpeg (視頻處理的), curl (內容抓取的). 看上去這些參數指示的功能亂七八糟的堆砌在一起的樣子, 仔細一想這些功能的確是相互關聯的, 所以被放到了一個工具之下. 這些工具和上面的工具的哲學是反其道而行之的: 集一大類功能於一個工具, 任何類似的操作都能通過這個一個命令+不同的參數來完成, 而非”do one thing, do it well”. 這些工具和傳統意義上的 UNIX 工具哲學是不大像的. 爲了區分他們, 我把它們叫做 Facade 工具, 因爲這些工具的設計哲學很類似於 Design Pattern 裏面的 Facade Pattern (Facade 模式的核型是用一個統一的接口管理對一個系統的訪問. 比如 gcc 就是對整個編譯系統的接口, ffmpeg 就是對整個視頻處理系統的接口, display 就是對整個 X 顯示系統的接口等等.)

之所以區分這兩者, 是我體會到: 在具體的學習過程中, 對付兩者的學習方法是截然不一樣的. 學習小工具, 基本上就是學一個簡單的名字到功能的定義, 加一些簡單的參數. 除了名字比較彆扭外, 使用很方便, 學習曲線不陡峭. 學習的要點不在於這些小工具本身, 而在於利用管道和其他工具通信(小工具從來就不是單獨使用的, 比如 yes, 比如 tr, 我幾乎沒見過不用管道的情況下用他們的); 和上面相反的是, 我幾乎沒見着 Facade 工具用在管道里面的.

原因是 Facade 工具基本上是一個自成體系的完整的操作方式, 就像一個新的領域的一種新的”語言”一樣. 因此, 不掌握一點基本的編譯知識, 就不可能把 gcc 玩轉, 因爲那些參數的含義的理解, 都是需要相應知識的. 我也常常看到不少 做 Web 程序的哥們對 curl 的每個邊邊角角都很熟悉, 但是對 gcc 不太熟, 這也是很正常的, 因爲 Facade 程序本來就是屬於面向一個特定領域的工具. 

我在學習這兩種截然不同的工具的時候也曾感到過困惑: 怎麼有的程序這麼多參數, 全學會怎麼可能. 在浪費了不少時間亂看這些 Facade 程序的 man 文件之後, 我認識到: 除非我寫操作系統, 要讓我的程序編譯的時候有幾百個參數, 否則, 簡簡單單的用 gcc 常用參數就能解決99%的問題了. 我覺得, Facade 程序的要點正是在於, 用一些簡單的參數組合(更多情況下其實不要參數) 就可以完成 90% 的常用例子. 至於剩下的 10%, 遇到了再去查文檔就行了. 同時, 對於不在自己”常用工具集”中的一些 Facade 工具, 認真學習他們的用法是一件非常耗時且幾乎沒有任何收穫的事情, 而且學到的也不會被實際用到. 所以, 千萬不要被”獲取新知識的成就感” 給矇蔽了, 去鑽研那些瑣碎的邊邊角角. 

對於小工具, 卻要反過來. 我覺得在學習小工具 (尤其是 coreutils 裏面的所有命令) 的時候, 最好要做個有心人, 把大部分參數弄清楚記住 (本來參數也不多). Linux 下的小工具基本上是千錘百煉經過無數進化的, 應該說每個選項都是很常用的. 搞明白這些選項, 可以極大化發揮這些小工具的優勢, 還能提高自己的生產率. 舉個例子: 比如說 ssh 這個程序, 90% 的哥們就是用他來登錄服務器, 然後運行服務器上的某個程序. 其實 ssh 的文檔寫得很清楚, 你可以把 ssh 後面接一個命令文件. 比如說 

ssh [email protected] ls

就可以直接顯示服務器上的目錄了. 還可以拓展一下, 

ssh [email protected] < script.py

就可以直接把本機上的 script.py 放在服務器上跑, 無需把文件先拷貝過去. (走題一下: 跨平臺的腳本語言的好處就在這裏. Apache 的 Hadoop 是 MapReduce 的一個開源實現, 他的任務控制器就是採用我說的這種方式來調用各個機器上的Mapper 或者 Reducer 工作的). 因此, 掌握 ssh 的加命令的用法, 在我看來, 是值得的.

很多小工具都有這樣不太鮮爲人知的用法, 熟稔這些用法, 我覺得是值得的, 況且這也不需要花多少時間, 只要打印一份文檔每天睡前看半頁就行了.  我以前還有整理了不少這類平時大多數人注意不到的小命令的一些”黑魔法”. 我覺得這些黑魔法一點都不是什麼奇技淫巧, 而是實實在在能提高效率的魔法, 是居家旅行必備的工具套裝. 

PS: 最近有幾個朋友看了我的博客, 發信讓我推薦學習 Linux 的書. 我推薦 “鳥哥的Linux私房菜” 這本書. 我學 Linux 的過程中沒看過這本書, 所以折騰的比較曲折. 直到我大四我纔看到這本書, 這本書是一本非常深入淺出的好書. 

PS2: GNU 的工具鏈有把小工具 Facade 化的傾向. 連 ls 這麼簡單的命令都有幾十個參數. 在這種情況下, 還是挑選一些認爲會常用的參數學習一下就行了, 沒有必要去追求高大全. 一般說來, 這種兩個字母的小工具, 如果後面加的參數超過6個字母, 就完全不對味了. 工具這東西, 強極則無用至極. 

-EOF-

===================================

5.比代碼大全好的兩本書A

上次我說到”比代碼大全好的書“, 第一本指的是 <Software Tools>. 爲了說這本書的優點, 得先說這本書的缺點. 

這麼書基本上絕版了. 而且也沒有中文版. Amazon 連舊書攤總共就不到50本. 可見這本書目前不是一本讓廣大程序員喜聞樂見的書. 其次, 這本書用的說明問題的語言叫做 Ratfor, 基本上是 FORTRAN 和 C 雜交的產物. 估計全世界用這個的程序員和現存的這本書的數量差不多多. 但是你要是認爲這是一本古董書, 爛書或者非暢銷書, 那你就錯了.  因爲是一本編程書籍, 生命週期本來就短, 因此單以現在的銷量判斷好壞, 並不科學. 江湖失傳已久的如來神掌送給周星星的時候, 周星星也不以爲然. 但最後威力無窮. 希望這篇書評, 能夠讓讀者信服這是一本如來神掌的祕籍. 

這本書的作者是 Brian W. Kernighan 和 P. J. Plauger . 關於這兩個作者出書質量好的廢話我就不多說了(不知道沒聽說第一個的回家用C寫一個Hello, world 並面壁). 先說這本書講的什麼吧. 

這本書主要兩條線, 一條是怎樣通過一個叫做 Ratfar 的語言, 一步一步構建 UNIX 系統下的 cat, wc, tr, sort, tar 等等這些工具; 另一條是怎樣和低級繁瑣且不順手的 FORTRAN 語言做鬥爭, 克服語言的障礙, 寫出功能和可讀性俱佳的結構化程序. 第一條着重強調的是一個系統的功能分解(對UNIX哲學清楚的讀者看一下目錄就一目瞭然), 第二條實際上是敘述了一個一脈相承到”代碼大全”的哲學: 如何構建”你的”編程語言, 而不是簡單的使用”別人的”編程語言. 這一條, 道出了整個編程的真諦: 編程就是構建一個一個”自己的”小積木, 然後用自己的小積木搭建大系統. 

爲了說明小積木的道理, 我們從編程語言說起. 我以前的文章也提到過, C 並沒有一個可以傳遞一行消息出來的 Assert 機制. 因此有經驗的程序員會自己構造一個 Assert. 同樣的道理, Java 雖然很高級, 卻沒有一個很好的單元測試框架, 所以全世界 java 程序員都在用 JUnit. 這些實踐, 表明了一個現成編程語言總有一些特性不完美之處, 工具和使用者之間還有着不小的距離, 因此顯得”不順手”. 如果這個例子不夠說明問題的話, 不妨問自己: 爲什麼人不能像寫僞代碼一樣寫程序呢? 因爲我們使用了編程語言, 而編程語言有很多骯髒的細節要我們去處理, 比如下標從0開始, 浮點數不好作爲數組下標等等. 語言的細節需要處理這個問題, 從 Fortran 到 Python, 只有程度的改變, 並沒有本質的改變. 況且, 通用編程語言之所以通用並且簡單, 就是因爲支持的功能比較基本, 可擴展性強. 因此, 基本功能都有, 高級功能缺少成了通用編程語言的最大特點. 不管編程語言多麼”高級”, 總是沒有自己的思維高級. 因此, 編程的第一步就是把語言改造成自己的語言. 即使強大到直接能 import antigravity 的 Python, 也有需要改造的地方(最好的例子就是 Python 3000 的推出).

小積木有了, 就要構建大系統了. 在這一點上, Software Tools 可以說是非常好的一本源代碼導讀. 自從 Lion 分析 Unix 源代碼以來, 源碼剖析成了程序員修煉的一個捷徑. 可是現在程序的源代碼樹都很繁雜, 能真的拿出來分析的很少很少了. Bell 實驗室的兩位作者從實作 UNIX 系統下的工具出發, 挑選出經過實踐檢驗的優秀代碼來講解. 這樣來自一線的題材是極其寶貴的, 就算在最新的 Beautiful Code 中, 大多代碼也只是教科書代碼而已. 至於代碼大全, 完全就是玩具代碼. 而 Software Tools 有幾千行代碼的大程序, 也有幾行代碼的小程序; 有算法程序, 也有文件IO程序, 基本覆蓋日常所有用例, 對於內功修煉大有裨益. 

除了道出”改造你的語言”的真諦之外, 這本書其他論點也可謂字字珠璣. 比如講goto帶給程序員的自由恰好是你不想要的自由, 因爲這個自由會帶來很多錯誤. (很多語言都有這種不想要的自由, 比如 C++, 到處都是). 比如說講結構化編程不會自動帶來清晰的程序, 因爲機械的規則永遠不能代替清晰的思考. 這個道理在面向對象/設計模式領域也一樣. 比如本書還論證了爲啥要詳細設計, 因爲設計和編碼環節對於程序員講是愉快的事情, 值得更多投入. 而 debug 和 測試環節是比較痛苦的事情, 所以要少投入. 還比如人比機器時間貴, 所以程序員要越懶, 越快完成編程越好. 除非程序太慢, 否則從總成本看, 機器多用點時間沒事, 人用的時間要越少越好, 等等等等. 類似於這樣的深刻揭示編程的哲學理念的句子俯拾皆是, 比起相同內容但是篇幅冗長的代碼大全, 這本書適合隨身攜帶, 隨時閱讀, 隨時提高. 

老規矩, 結尾順手說個八卦吧. 話說爲了把 Ratfor 這個假想的語言翻譯成當時最流行的兩種語言, FORTRAN 和 PL/I, bwk 爺爺寫了一個宏替換的工具, 能夠把 Ratfor 替換成骯髒的 FORTRAN, 而他們寫乾淨的 Ratfor. Dennis Ritchie 爺爺看到鳥, 很贊, 於是推廣了一下這個宏替換工具, 起個詭異的名字叫做 m3 (macro for ap3). 然後 bwk 爺爺又看到了 dr 爺爺的工作, 回過來又和 dr 爺爺合作, 寫出了金光閃閃的 m4. 如果你常常編譯開源軟件, 肯定會注意到一個叫做 configure 的生成 makefile 的程序. 這個 configure 的讀入, 一般情況下是可配置的, 叫做 config.ac, 就是 m4 語言寫的. 雖然因爲版權問題, 現在 GNU m4 和兩位爺爺沒啥關係了, 但是基本的語法和用法都是一樣的. 各位知道 K&R 的讀者千萬不要錯過這個好用的工具(也是編程語言). 

這個工具其實我也只懂皮毛, 也不常用, 只是用來自動編號一些行, 做一些稍微複雜一點的不能用正則的文本替換. 不過我似乎在某個地方聽一個高手說, Linux 命令行下文本處理三劍客乃是 sed/awk/m4, sed 和 awk 的強大早就見識了, 相必m4與他們各有千秋. 故而略介紹一下. 

另外, 本書也是 troff 排版的. 按照我的 troff 排版無爛書定理, 這本書也屬一流好書.

==================================

6.高效能編程的七個好習慣

這七條都是我這個不怎麼高效能編程的人悟到的. 不權威, 不一定全對. 

1. 使用工具幫你找 Bug, 而不是人工找. 

工具包括用單元測試, assert語句, 代碼測試容器. 人工指用 print 和 debugger 一行一行跟蹤. 我們知道, 編程中絕大部分時間是耗費在除 bug 上. 不同的人有不同的 debug 的方法. 我個人比較喜歡”極限編程(XP)” 學派的主義, 也就是說, 代碼未動, 測試先行. 

單元測試中的紅棒綠棒(熟悉 JUnit 的讀者知道我在說什麼)一出現, 哪裏出了問題就一目瞭然. 單元測試的另外一個好處在於增加寫程序的自信. 以前沒用單元測試之前, 每天晚上改代碼改到很晚的時候腦子常常不靈活, 把代碼改錯, 然後第二天來還要重頭弄. 有了單元測試之後每天晚上保證測試全部過掉, 這樣心理踏實, 睡覺也香, 早晨也不忙, 吃飯也棒. 

一般的語言都有 assert, 但是很少有人用. 其實 assert 是一個非常好的DEBUG 工具, C 的 assert 能夠把哪一個文件哪一行出了錯都告訴你. 不過我一般會自己寫一個這樣的 assert 宏:

#define ASSERT(value, msg) if (!(value)) {fprinft(stderr, "At file %s, line %d: /n message: %s/n", __FILE__, __LINE__, msg); exit(-1);}

這樣的 ASSERT 可以帶一個信息出來, 比起原來只告訴你哪個文件哪一行更加有價值. 

第三個是用容器幫你找 Bug. 這一點以 C/C++ 程序最爲突出, 因爲編譯之後直接就是可執行代碼, 運行時的信息不像 Java 和 Python 這樣有 VM 的語言容易得到. 這時候, 我推薦 valgrind. 這個工具能夠把 C/C++ 程序放到一個容器中執行, 記下每一個內存訪問. 被這樣的容器 debug 一下, 基本上指針指飛了 (Segmentation Fault) 的情況幾乎就沒有了. 想像一下是用 GDB 追蹤非法指針和內存泄露方便, 還是用容器告訴你哪一個指針非法, 哪一個內存沒釋放方便 :)

2. 選用自動化工具構建

用 gcc 或者簡單的 IDE 來編譯和運行程序在編程初期是很快速的, 可是越到後來, 會越臃腫. 在編譯的時候, 不同的參數, 不同的目標, 在 IDE/gcc 裏面每次都要設定. 而且一般的 IDE 也不能做到自動解決依賴等高級方法. 因此, 最好的方法是用 Ant 或者 Makefile 管理項目. 這方面教程很多, 而且我估計編程的個個都知道. 不管項目大小, 注意頻繁使用就是了. 

自動化測試也有很多工具, 特別是 GUI 和命令行測試的自動化, 工具鏈都很完整. 大公司裏的程序員走這方面的流程都比較規範(我在西門子實習過), 但是小一點的公司中, 或者個人搞小項目的時候, 就不一定想得起來了(大部分我見到的程序員就手工來測試). 手工測試看上去快, 但是要是積累的次數多了就比較浪費時間了. 其實自動化測試工具的學習成本很低的, 事半功倍. 

3. 買本小書做參考, 而不是用 Google. 

這是大實話. 我大三開始學 Python 的時候, 語言特性並不熟悉, 手頭也沒有書, 因此常常連取個隨機數都要上 Google 查一下庫. 我發現, 不管網絡多快, 自己搜索技術多牛, 還是沒有手頭一本書方便. 後來打印了一個7頁的標準庫的 cheatsheet, 編程立即行雲流水. 我在實習的時候也觀察到, 大部分時候程序員不可能記住一個框架所有的API, 所以他們要不等 IDE 幾秒鐘做代碼補全, 要不一邊翻文檔一邊做. 或許MSDN 這些本地文檔系統比查書快吧, 但是用 Google 和網絡搜索絕對比書慢. 現在因爲工作原因, 常常要學一些新的語言, 我做的第一件事情, 就是把他的庫接口的網頁全部打印了下來. 

4. 用腳本語言開發原型

人月神話的作者 Brooks 說: 準備把第一版扔掉, 因爲第一版必然要被扔掉. 這是大實話和真理. 既然第一版要被扔掉, 咱們就讓第一版扔掉得越早越好. 說白了就是, 原型要快速的被開發. 

所謂的快速原型開發, 大致有兩個捷徑, 第一是隻做核心的功能, 輸入輸出都是構造好的簡單的例子. 第二是隻做最簡單的情況, 對於性能和健壯性什麼的都不太考慮. 這兩點, 恰好是腳本語言最擅長的. 腳本語言擅長於用精簡的幾行構造出複雜的功能, 並且語法很鬆散, 潛在假設程序是正確的. 

即使在代碼編寫階段, 一些功能的實現, 也是要先寫個簡單的, 再慢慢打磨成複雜的. 腳本語言此時依然有用. 比如我在用 Java 的時候, 常常不確定一個函數返回的對象究竟某個屬性是什麼樣的值. 這時候我就會用 Java 的 bsh 腳本寫一行打印, 而不會寫一個複雜的 out.println 再編譯再運行再把那行刪除掉. 當然, 這幾年很流行動態語言, 原型和產品之間的差距已經變得很小了. 

5. 必要的時候, 程序要使用清晰的, 自我解釋的文本文件作爲日誌輸出. 

不知道各位調試程序的時候是不是和我一樣, 看到不確定的和要跟蹤的變量就直接插入一行 print. 我以前一直這樣做, 但是頻繁的插入這樣的打印會使得屏幕的輸出很亂, 不知道哪行是什麼意思. 一個更加好的辦法是寫一個日誌函數, 可以分也可以不分優先級, 總之保證 Debug 的時候的輸出以一種統一的, 可管理的方式出現. 這樣, 在最後發佈穩定版本的時候, 只需要簡單的幾行命令就可以從代碼中剔除所有的日誌打印行. 

如果必然要輸出日誌, 最好要分配一個單獨的命令行參數, 用來控制程序究竟輸出不輸出日誌, 輸出哪些日誌. 一開始看上去這個是費時費力, 越到後來日誌越多的時候, 就體會到方便之處: 有時候你只想要某一類日誌, 可是其他的記錄偏偏來搗亂. 多加一個參數可以使得程序更加靈活, 根本不需要去修改代碼或者條件編譯就能得到不同級別的程序日誌.

日誌和程序的輸出結果一定要清晰且能自我解釋, 否則不如沒有日誌. 我切身經歷是這樣的: 幾個月前, 我一個程序跑了大約一天, 最後輸出了很大的日誌和結果. 但是很不幸的是, 結果裏只有數字, 沒有任何說明. 我自己都忘了每一行是什麼意思. 而且更加麻煩的是程序的輸出藏在重重判斷和循環之內, 使得根本沒有辦法分析這一行輸出對應的輸入是什麼. 於是, 最終只能再次浪費一天的時間讓程序再跑一次.  經過這次教訓, 我的程序日誌和結果中插入了不少讓人可讀的內容. 這樣, 即使程序丟失了, 結果還是能夠被人解讀的. 

更多的關於數據和程序結果要能自我解釋的精彩論述, 可參見 More Programming Pearls 第四章. 

6. 使用命令行小工具操控分析你的結果和代碼, 而不是用自己的眼睛和手.

我發現, 人有一個固有的習慣, 就是喜歡自己去”人工”, 而不喜歡用工具. 因爲人工讓人感覺工作更加刻苦, 更加快, 更加有控制感. 比如說吧, 上面我說的測試, 我就不只一次見到爲了測一個交互式的命令行, 一個程序員寧願老是每次打相同的三個命令, 而不願意用一個簡單的 expect. 再比如說, 面對長長的日誌文件, 我見到很多人都是用文本編輯器直接打開, 用鼠標滾輪一行一行的往下翻, 而不是使用 grep. 包括看網頁, 很多人從來不用查找功能, 而是一行一行的往下瞄. 包括打遊戲也是, 好的UI腳本(不是外掛)一大把, 可是玩 WoW 的人很少用, 都喜歡自己重複點鼠標.

別看上面說的這些好像程序員沒有, 其實我們常常陷入這個誤區. 舉個簡單的例子, 一個 python 程序裏面有十幾個 print 函數, 我們想把這些打印全部滅掉, 一般人會打開文件慢慢瞄, 稍微高級一點的用查找, 找到了, 用快捷鍵刪掉整行. 其實最好的方法根本都不要編輯器, 應該用 grep -v. 或者 sed, 但是這樣的方法極少會有人用的. 我也是強迫自己無窮多次之後, 才漸漸的用這套快速的方法. 

7. 程序能跑就是萬歲. 除非萬不得已, 儘量不要在性能上優化你的代碼

Knuth 名言: Premature optimization is the root of all evil. (提前優化是萬惡之源). 一般我們寫代碼的時候, 不知不覺的就會覺得, 哎呀, 這樣寫效率不高, 我要構造一個數據結構啥啥. 隨機訪問一定要哈希表, 排序一定上快排, 查找一定要二分, 強連通分量一定要用 Tarjan 算法, 動規一定比窮舉好等等, 這些競賽的時候極限情況下正確的論斷其實在實際環境中並不重要, 因爲做編程的一開始關鍵是能跑, 而不是跑得快. 往往這麼以優化, 程序很難 debug, 倒是還要去翻算法導論和TAoCP 看人家的二分怎麼寫的等等. 

在程序能跑的情況下, 優化也要特別小心. 我曾經有一個程序, 大約有 90% 的運算是查表, 只有 1% 的是乘法, 另外是一些判斷和把插到的結果插入到一個集合中. 我的查表是用的最土的 list.index. 按照正常的想法, 應該把這個優化成哈希表. 而實際上我用 profile 工具一看, 才知道, 原來是插入到一個集合的操作費時間, 因爲每次都需要 extend, 涉及到很多內存分配的操作. 我做過非常多的 profile 測試, 沒有一次不出乎我預料的. 程序運行時間總是在自己不認爲浪費的地方被浪費掉. 因此, 就算萬不得已優化, 也務必要先做一下 profiling. 我喜歡 python 的地方就在於, 他的 profiling 只需要一行語句就完成了, 而且結果具體乾淨. 其他的語言, 至今沒見到這麼簡單的 profiling 工具. 

另外: 用兩個或者大於兩個顯示器. 不要用或者少用鼠標.

============================

7.比代碼大全好的兩本書B

各位讀者老大中有不少都是大學生, 相信不少都參加過形形色色的英語寫作培訓班. 如果當年您參加培訓班的時候, 老師沒有介紹一本叫做 <The Elements of Style> (TEoS) 的書, 建議您現在立即衝過去找他們退錢. 爲啥呢, 因爲這本書是講解英語寫作繞不開的經典聖經(即使這本書已經被說爛了, 批評也不少, 但還是經典). 假如培訓機構或者老師上課沒推薦到這本書, 這個培訓機構要不是太牛逼了, 要不是水貨. 而大家都知道, 水貨和牛逼的比例總是 1:epsilon.  

作爲Amazon 上 297 個5星的書, 書評我就不狗尾續貂了. Knuth 爺爺也是很喜歡這本書滴, 因此在 Stanford 開課的時候讓學生人手一本 (我們系今年新生也強制人手一本). 這本書不光勾勒了英語的基本寫作要素, 也刻畫了一個時代: 從此, 任何需要”藝術和技藝”的領域, 都會時不時跳出一些牛人, 模仿這本書的題材和哲學, 用簡潔的文筆勾勒出這個領域的基本要素. 以我熟悉的計算機領域爲例, 就有 “The Elements of Programming Style”, “The Element of Programming Style with Perl”. “C Elements of Style”, “The Elements of Java Style”, “The Elements of UML Style” 等等書, 都是希望繼承 TEoS 的衣鉢, 勾勒出編程的一些風格要素. 今天我要說的比<代碼大全>好的書的第二本, 就是叫做 <The Elements of Programming Style>的. 我以前在計算機科學必讀經典中, 也提到了這本書. 

這本書作者和上一本 Software Tools 一樣, 屬於一個家族哲學下的兩本不同角度的書. 關於它的書評也很多, 我就不一一廢話了. 只說幾個體會較深的. 

第一是寫程序和寫作一樣, 要寫的清楚. 這本書翻開第一條就是 Write clearly - don’t be too clever. 看上去說的和沒說一樣, 其實實踐起來乃是金科玉律. 我曾自己寫過三層嵌套的 “? :” 表達式, 寫的時候自己被自己的聰明都感動了, 回來改的時候自己被自己當時的聰明給打擊了: 死活看不懂當時啥意思, 只好寫一個 printf 在後面測輸出. 假如當時多花幾分鐘寫的清楚一點明白一點, 就犯不着回頭修改的時候花半小時破譯了. 現實中的情況沒這麼極端, 但是也比比皆是. 相信任何正常的程序員, 每天都要爲了理解以前寫的不大清楚了程序浪費不少時間 (反正我是記不住一年前寫的代碼的每個小細節). 因此, 寫的時候寫的清楚比什麼都重要. 

在寫得清楚上, Knuth 爺爺是榜樣. 他提出的 Literate Programming 的思想雖然太學術, 使得實踐的人不多, 但是的確使得程序更加好讀. Knuth 爺爺把他的用C語言作爲基本語言的 Literate Programming 系統叫做 CWEB. 大名鼎鼎的 TeX 就是 CWEB 寫成. 如果對 Knuth 爺爺比較粉的粉絲們恰好要做圖算法,  Stanford Graphbase 是一本非常好的書, 裏面貼得全是程序, 但是因爲 Knuth 爺爺用 CWEB 寫成, 文檔和程序渾然一體, 讀起來絲毫不覺得思維在程序和自然語言間做切換. Java 下有名的 XDoclet 和 Javadoc, 事實上也是 Literate Programming 的一種體現. 據 Knuth 爺爺講他寫 CWEB 程序能笑出來, 這種境界不是一般人能有的. 而且 Knuth 爺爺在提出 Literate Programming 的時候, 就野心勃勃的說: 寫文章也是寫, 寫程序也是寫, 我們 Literature Programming 的口號就是: 沒有蛀牙 程序員也能拿普利策. (”I’m hoping someday that the Pulitzer Prize committee will agree.” Prizes would be handed out for “best-written program”.)

又八卦走題了. 言歸正傳, 我的第二個深刻的體會是”讓計算機幹髒活”. 什麼叫髒活呢? 讓你不爽的活叫髒活. 比如 Debug, 比如無窮多的複製粘帖, 比如替換一個大小寫, 數數幾個單詞, 做做單元測試等等. 用眼睛瞄肯定會死人. 我以前在 “高效能編程的七個好習慣”  這篇文章中也說了, 就不多廢話了. 

當然, 現實的問題是, 理論是理論, 實踐是實踐. 事實上, 我們要不然就是不用或者想不起來用工具(理由是不習慣), 要不然就是成爲工具的奴隸. 李笑來老師也觀察到了第一點, 比如這篇. 爲什麼明明別人告訴我有高效率工具和習慣存在的情況下, 我們還不去用不去改, 或者如何不成爲工具的奴隸這兩個話題都太大了, 我也寫不好, 就不廢話了. 然而, 不管最後實踐用還是不用, 讀一些被別人實踐檢驗過的經驗之談還是很有用的. 這也是我推薦這本書的原因. 不知道大家有沒有發現, 潛意識中如果有個正確的小聲音不時在原則上提醒自己, 實踐的時候潛移默化的就會越做越好. 

最後依然附送兩個八卦. 第一個是關於 TEoS 這本書的. 這本書列了很多的原則和規則, 都是具體的對某個詞某個句型的建議, 因此英語寫作的時候可以直接應用這些規則. 不過對着書查規則顯然屬於髒活的範圍, 所以呢, 我們的”讓計算機做髒活”的哲學就發揮作用了: 在 Linux 下有一個程序叫diction, 用他可以檢查英語寫作的文章符不符合 TEoS 的標準, 我以前也專門介紹過. diction 會挑出那些不符合 TEoS 的句子, 告訴你讓你修改. Knuth 爺爺也說, 雖然這個程序很笨, 但是至少可以強迫你重新審視你的文章, 挑出弱智的錯誤. 其實 GNU/Linux 下幫助英文寫作的工具很多, 雖然不完美, 也稱得上完整了. 我以前的文章可供大家參考 . 和 diction 一起的另一個工具叫做 style, 可以做像長句分析, 被動語態分析, 平均單詞和詞彙量估計等統計, 以及語言學水平上的英語水平估計(等價於美國幾年級學生水平的估計). 這些估計都是語言學家研究數年的標準指標. 大家都知道, GRE 作文是計算機批閱的, 雖然我們不知道算法, 但是可以想象, ETS 那麼笨, 肯定是請語言學家幫忙設計的程序, 所以必然或多或少的用到很多標準的語言學指標. 所以呢, 你不用計算機程序分析分析自己的文章, 光聽培訓機構的一些老師忽悠, 怎麼知道自己文章水平吶? 相比較一些培訓機構的老師, 指不定 style 這個程序更像 ETS 的評價標準. 

第二個八卦是關於寫清晰的程序的. 或許大家都聽說過史上最牛逼的註釋的故事. 雖然各人有個人認爲的最牛註釋, 我個人喜歡的叫做 /* You are not supposed to understand this. */ (我不指望你懂這是啥意思). 這句話其實本來不該這麼出名的, 恰好是因爲出現在開源的第六版UNIX中, 恰好寫的人是 Dennis M. Ritchie, 恰好澳大利亞出了一個叫 Lion 的人把 UNIX 源代碼扒出來搞了個源碼解析, 又恰好當年這本源碼解析幾乎每個黑客都人手一本. 所以, 這個極其挑戰其他黑客智力的註釋就變得流行起來鳥. DMR 同學對此有技術上的詳細解釋,  不再廢話. 就是友情含淚勸告讀者: 您要是在你的程序裏面搞這麼一句然後又被你同事和老闆看到鳥, 你就完蛋鳥. 世上只有一個牛逼的 DMR 敢這麼寫.  

PS: 想要看看The Elements of Style 書的內容的老大們, 可以猛點這個鏈接

想要看 The Elements of Programming Style 說了哪些的老大們, 可以猛點這個鏈接

-EOF-

================================

8.Smalltalk 中的珠璣 

如果我們能夠重回1980年, 回望整個計算機編程語言領域, 特別是工業界編程, 打死也不會想到日後 Java 這種無名小卒, 以及 C++ 這個又面向對象又支持過程的雙面間諜能夠紅得發紫. 當年最流行的語言, 當屬 FORTRAN, C 和 Smalltalk. 前兩個我們按住不表, 單說這個 Smalltalk. 我們現在的教科書基本都不介紹 Smalltalk, 或者就用一句: Smalltalk 是第一個純面向對象的語言 概括過去. 其實 Smalltalk 中有很多的好的思想, 一直在今天都發揮着魔力. 

施樂當年的圖形界面(來源: harding.edu)

爲提起大家興趣, 我先說血統和設計等八卦. Smalltalk 的血統是算得上高貴的, 來自當年超級牛逼的 施樂 PARC 實驗室. 施樂的 PARC 幹過很多事情, 比較著名的一個故事是說喬布斯同學去參觀, 看見那邊科學家已經做出了 GUI (圖形界面程序), 於是偷偷的回家搞 Macintosh, 搞好之後在1984年發佈, 賣得大大的好, 賺得盆滿鉢盈. 西雅圖當時有個大學沒畢業做軟件的小夥子, 看見喬老師賺了大錢, 想想覺得自己的人生挺沒意思的, 只是和 IBM 做訂購 DOS 的生意, 於是起了自立爲王的念頭; 加上看到喬老師的蘋果機一個窗口一個窗口的很好玩, 於是一激動就自己搞了一個 Windows. (這個作軟件的小夥子就是比爾蓋茨啦). 這小夥子很牛, 把喬老師的蘋果機逼到了角落裏. 喬老師是最不能嚥下惡氣的人, 於是連在 Stanford 演講了時候還不忘提一下微軟抄蘋果. 法律上就更不要說了, 兩家公司之間曠日持久的 GUI 專利權官司從1988年打到1994年. 兩家公司都一步不讓. 最後施樂火了, 跳出來大喊一聲: 靠, GUI 乃是我發明的. 於是把蘋果給告了. 所謂螳螂捕蟬, 黃雀在後, 蘋果被施樂這麼一搞, 自己抄別人的老底就被挖出來了, 告微軟就顯得特別勉強, 所以官司最後也沒贏, 以蘋果無理取鬧失敗爲結果.

施樂不光用 GUI 引領了我們現在計算機圖形界面, 還發明了以太網, 鼠標, 所見即所得的編輯器等. 要不是這幾樣東西, 現在的計算機說不定是另一個樣子呢. 言歸正傳, 前有施樂 PARC 出品了這麼多偉大產品, 後加上 Alan Kay 這種牛人主導設計, Smalltalk 的血統之好, 和出自 AT&T Bell 實驗室的 C 是有一拼的. C 還是兩個人無聊敲打出來的, Smalltalk 是正兒八經作爲一項研究弄出來的產品.  

事實上 Smalltalk 的確也是劃時代的產品. 我就說我知道的兩個部分. 

第一是現代程序員耳熟能詳的 MVC 結構以及整個 Design Pattern 的思想. MVC 出現在 Smalltalk 中並不是偶然的. 當年施樂開發 Smalltalk 主要是用來做圖形界面編程的, 而圖形界面的編程首先就是從施樂發明圖形界面開始的. 試想一個程序員成天寫命令行程序, 肯定是不會太在意 MVC 的分離. UNIX 世界中並沒有MVC的對應物, 因爲壓根不需要. 而圖形界面程序的複雜度比其他程序要高太多了, 因此自然的就產生了 MVC 這樣解開功能模塊耦合的自然的設計. MVC 的重要程度和流行程度可以從兩個小事情看出來. 第一是著名的 GoF 書, 翻開第一章第二節就開始講 MVC, 用 MVC 作爲整本書的綱領章節, 可見其重要程度. 第二是衆多的 Java 框架, 比如Struts, JSF, 裏面的對象就很直白的叫做 XXModel 或者 XXViewer. 這些傳統都是從 Smalltalk 開始的, MVC 的影響一直到今天還到處都是. Smalltalk 不光催生了 MVC, 也催生了 Design Pattern. 細心閱讀 GoF 的 DP 書我們就會發現, 裏面所有的 Pattern 大多是在設計一個所見即所得的編輯器的背景下提出來的. 而上面我們已經說了, 施樂是第一家搞這個玩意的. 如果我們追溯 Smalltalk 早期很多的論文, 很明顯可以看出, 雖然沒有用 Design Pattern 這個詞, 開發的時候要遵循一定的”對象結構”的思想是隨處可見的. 

第二是我認爲非常重要的: 運行時類型信息支持, 或者叫反射. 簡單的說, 就是一個對象在運行的時候能夠知道自己的類型(類名稱), 以及這個類有哪幾個方法, 哪幾個字段等等. 

關於反射的基本概念在腳本語言裏面是屢見不鮮的了. 大家都知道, LISP 裏面的 eval 後面可以加任何的字符串, 構造出一個運行時對象. 腳本語言實現反射也很簡單: 本來就是解釋執行的語言, 多一個 eval 等價於多調用一次解釋器而已. 而編譯型語言就麻煩了, 因爲解釋器已經在編譯期用過了, 運行的時候解釋器是不存在的. 這樣, 就造成了編譯型語言沒有運行時信息這個本質困難. Smalltalk 用了一個巧妙的方法解決了這個問題, 也就是 Java 和 Python 等現代語言用的方法: 虛擬機. 能編譯的代碼被先編譯, 需要解釋的代碼在運行時可以被虛擬機自帶的解析器再解析. 除了加入一個小的解釋器到虛擬機外, Smalltalk 更進一步, 把對象的元信息也抽象成一個對象, 這樣運行時需要的一個對象的所有元信息都能在面向對象的標準框架下表達. 我們用類 Java 的語言來舉例: 一個叫 a 的 Foo 對象, 包含一個 a.hello() 的方法, 這個方法既可以通過 a.hello() 來調用, 也可以通過 a.class 先得到 a 的類, 再通過 a.Class.findMethod(”hello”) 找到這個方法. 最後再通過 .invoke() 調用這個方法. 這樣的流程在沒有虛擬機的 C++ 裏面是沒法完成的. 

在1980年, 這個反射機制的劃時代意義是怎麼說都不爲過的. 我以我熟悉的 JUnit 的進化史爲例說明這個議題. 

現在做單元測試的框架, 一般都被稱爲 xUnit 家族. xUnit 家族最早的成員, 不是 JUnit, 而是 SUnit (Smalltalk Unit). SUnit 的歷史比 Junit 悠久得多, 大約在1994年的時候, Kent Beck, 也就是 Junit 的作者之一, 寫了 SUnit. 而後纔有了 JUnit (1998). 所以, 在 SUnit 的網站上, 極其顯擺的寫着”一切單元測試框架之母” (The mother of all unit testing frameworks). 事實上這是大實話 — 所有單元測試框架裏面的名詞術語, 都從 Sunit 來的, 如 TestCase, Fixture 等等. 

既然 SUnit 和 Junit 是同一個作者, 而早在1996年, Java 就已經成爲工業界炙手可熱的語言, 爲什麼要等到兩年之後, JUnit 才橫空出世呢. 這裏面的原因說簡單也簡單: 自動單元測試需要反射支持.  1998 年前的 Java 沒有反射, 直到1998年 Java 1.2 發佈, 反射才完整的被支持. 所以, 只有1998年之後, Java 纔有辦法做自動單元測試. 

我們回顧一下 Junit 的工作流程: 繼承一個 TestCase, 加入很多以 test 開頭的方法, 把自己的類加入 TestSuite 或者直接用 TestRunner, 讓測試跑起來. Junit 能夠自動覆蓋所有 test 開頭的方法, 輸出紅棒綠棒. 這地方的關鍵是自動覆蓋. 假如每個測試都是靠程序員自己寫 printf 比較, 那不叫自動. 假如每個 TestCase 裏面的每個 test 開頭的方法都要程序員自己寫代碼去調用, 那也不叫自動. 所謂的自動, 就是在機器和人之間形成一定的規約, 然後機器就去做繁瑣的工作, 最小化人的工作(RoR就是很好的例子). 

注意到我們的需求是 “讓 Junit 自動調用以 test 開頭的方法”, 而不需要自己很笨的一個一個自己去調用這些方法. 這意味着 Java 語言必須支持一個機制, 讓 JUnit 知道一個測試類的所有方法名稱, 然後還能挑出 test 開頭的方法, 一一去調用. 這不就是反射麼! 事實也證明了這一點: 目前互聯網上找到的最早的 Junit 的源代碼, 1.0 版的核心就只用了一個 Java 的標準庫: reflect. 相反, 不支持反射的語言, 就得告訴單元測試的框架我要運行哪些. 比如說 C++ 的單元測試框架 CppUnit, 就很不方便–必須告訴框架我要測哪幾個函數, 就算他們以 test 開頭也不行. 還有一個好玩的例子是 J2ME 的測試框架. J2ME 是 Java 小型版, 不支持 reflect, 因此, JUnit 平移不上去. 如果細看所有的這些移植 JUnit 的嘗試, 很容易發現, 移植出去的版本作用到有反射機制的語言上, 使用起來就很方便, 就比較成功, 比如NUnit; 沒反射機制的就比較麻煩, 用的人也相對少, 比如 CppUnit 和 J2MEUnit. 反正任何對於 JUnit 的移植, 都繞不開”反射” 這個機制. 有反射者昌, 無反射者弱. NUnit 這個移植版本, 還曾經被 Kent Beck 誇設計好, 其原因, 與 C# 語言比 Java 更加良好的 attribute 和 反射機制, 是息息相關的. 

此外, 現代框架中流行的 依賴注射 (Dependency injection), 反轉控制 (Inversion of control), 都是基於反射的. 這也就是爲啥用傳統的不支持反射的語言很多年的人很少聽過這些名詞的原因. 

有興趣的讀者可以繼續閱讀 wikipedia 關於反射元編程 這兩篇文章, 相信會得到更加多的啓示. 

Smalltalk 的IDE 開發環境 (來源: arstechnica.com)

Smalltalk IDE (arstechnica.com )

除了以上兩點, IDE 和庫的思想. 我們今天用的標準名詞, 如”方法”, “字段”, 都是來自於 Smalltalk 的. 這些也都是劃時代的工作, 因爲我不熟悉, 也不敢不懂裝懂的展開介紹了.  
有時候回看歷史, 特別是回看編程語言的設計和進化的歷史, 會發現很多散在的晶亮的珠璣. 

(完)

==============================

9.快工具, 新思想 

和世界上大多數國際機場一樣, 美國夏威夷國際機場非常大. 爲了方便旅客在航站之間轉運, 航站之間用巴士提供交通服務. 在夏威夷, 他們用本地人的語言把這種巴士命名爲 wiki wiki, 意思是”很快”. 因爲在本地人的語言裏面, wiki 是”快”的意思. 

1995 年的時候, “極限編程”方法論大牛, Ward Cunningham, 覺得應該建立一個公共的網站, 讓人能夠輸入一個 Pattern 的名字, 就能查閱到一個 Design Pattern 的用法, 而且這個網站還能被人編輯, 實現知識共享. 從此, 世界上第一個 wiki 網站就建立起來了, 他把它的東西叫做 WikiWikiWeb, 意思就是”快速查閱的網站”. 這時候 wiki 還只是在程序員之間流行, 直到 2001 年, 一個叫 Jimmy Wales 的, 創建了 Wikipedia, 從此, 纔算是普及了. Wiki 和 Wikipedia 徹底改變了我們的生活. 試想, 人類協作創造了一本共享智慧的, 隨時可訪問(中國大陸和朝鮮除外)的百科全書, 是多麼值得榮耀的偉大成就! 

且慢, 以前人類難道沒有百科全書麼? 有, 幾乎每個像樣的圖書館都有大英百科全書, 爲什麼這些百科全書沒有如此大的改變我們的信息獲取方式呢? 沿着同樣的邏輯鏈條, 我們可以問更多的問題: 在沒有 Google 之前, 似乎搜索引擎也有, 但這個東西我們很少用, 也很少聽說, 爲什麼就是 Google 一出現, 就徹底改變了我們的檢索方式呢? 

問題的答案很多, 我說我的答案: 很多事情, 只有在人能很快的完成的時候, 纔有了做的可能. 這句話可能比較拗口, 反過來說可能更加好懂: 如果用某種方法做一件事情太耗時間了, 那麼人就不可能用這個方法做事情. 只有一個方法能夠讓人足夠快的做好事情的時候, 這個方法纔會變得實用, 同時這個事情纔有做的可能性. 

爲避免過於抽象, 我們仍然用例子說明. 美國憲法規定要10年做一次人口普查, 但是直到 1890 年, 美國才進行了歷史上第一次完整的全國人口普查. 傳統上, 人口普查的數據提取上來, 要花多於10年的時間才能處理完, 因此, 人口普查從來就做不完. 直到1890年左右, IBM 公司發明打孔卡片, 賣給了美國人口統計局, 美國人口統計局採用了打孔卡片作爲報表, 從此才能在2年內做完一次人口普查統計. 打孔卡片比人填表統計快多少呢, 也就快5倍而已. 但是就這個5倍, 把原本不可能做到的全國人口普查變成了可能. 

天氣預報也是很好的例子. 天氣預報的原理是解一個數值偏微分方程, 這個道理科學家在1922年就知道了. 但在計算機沒出現前, 是沒有天氣預報員這個職業的. 直到1955年, 在電子計算機的幫助下, 天氣預報才變成了現實. 那麼, 1955 年的電子比1922年的機械式計算機快了多少倍呢? 也就1000倍. 另外有一個未經證實傳說說, 以前預報24小時內的天氣預報需要計算機計算25小時, 直到更快的計算機出現, 才使得2小時之內可以算出24小時的天氣預報, 使得天氣預報實用化. 這個, 也就是10倍的更新. 

快工具和慢工具的差別, 帶來了一件事情可做與不可做的差別. 其實不光是表面上速度的改變, 對應內裏是整個方法體系的本質改變纔是關鍵. 比如, 在 UNIX 下數一個文檔有幾個a開頭的詞是很簡單的事情, 只需要知道正則表達式和管道就行了. 在沒有正則表達式和管道的環境裏, 這個事情就比較難 (或者有更加好的方法我不知道?). 當然, 這事也可以做, 只是慢了10倍而已. 同理, 從紐約到華盛頓步行也能到, 就是慢了一點而已. 而汽車讓從紐約到華盛頓變成了一件很平常的事情, 其實小汽車也就比步行快了不到20倍而已. 到圖書館查百科全書也是一種獲取資料方式, 在網上 Google 也是一種方式, 後者(一分鐘)比起前者(一小時), 也就快了10倍到100倍而已. 可不同的僅僅是速度麼? 正則表達式提供了新的描述角度; 汽車是一種新的不耗費體力的快速交通工具; Google 是一種新的獲取信息的手段, 這些速度的表面差異, 對應的內裏, 是本質差異. 雖然改變的是速度, 卻不僅僅是速度. 甚至, 我們可以大膽的斷定, 如果沒有本質的內裏的變化, 速度也不可能有10倍的提升. 

我們都知道, 做事情要高效, 要 WikiWiki. WikiWiki,  是每一個想要管理好時間的人的聖盃, 是每一個想多做點事情的人的魔咒; 可是很顯然, 平凡的工具, 至多越用越熟; 即使用到爛熟, 也不能帶來本質的效率提升的; 一成不變的思想, 最多用到極致, 形成一個自我體系, 但跳出體系外, 是不能帶來嶄新的角度和本質的提升的. 特別的, 是在計算機科學以及計算機編程領域, 快工具和新思想層出不窮. 依我的觀察, 在計算機科學的發展史中, 每一個時代都有很多新思想涌現, 帶來的是革命性的思維方法和嶄新的理論實踐, 以及快好幾個數量級的效率提升; 在在編程方面, 我們大多數人也見識了 UNIX 管道哲學, 和函數式編程的哲學對效率的提升.  這些新思想, 好工具, 是我們計算機科學領域最好的珠璣, 也是在大海邊玩耍的孩子不可錯過的晶亮的貝殼. 

那麼, 心急的讀者要問了, 到底哪些是晶亮的貝殼和藏着的珠璣呢? 別急, 我會把我見到的認爲是晶亮的貝殼和珠璣的好東西記錄在這裏, 所以, 請繼續關注我這個系列的後續文章 :) 

最後附送幾個不算八卦的八卦, 算是本文花絮: 

1. 本文中間那句拗口的中文是翻譯自 Software Tools 中的一句: < Many jobs will not get done at all unless they can be done quickly.> 裏面還有 “We consider people cost a great deal more than machines, and the disparity will increase in the future” 以及 “The extra freedom permitted by got’s is just what you don’t want” 等道理深刻的話語.  有時候我真的懷疑, Software Tools 是一本講哲學的書, 而不是一本編程書. 

2. Wiki 最早的思想來自於蘋果機上的 hypertalk. 這個軟件相當於是個人多媒體 Wikipedia. 蘋果的 Applescript 自然語言編程的語法, 也是借鑑的這個軟件的語言, 叫做 hypertalk. 這個軟件稱得上是個人計算機的 killer app, 但是不幸被蘋果收購之後就中斷開發了.  Steve Jobs 這傢伙很沒文化, 收購併扼殺了很多蘋果上的經典軟件, 這些故事等以後有空細說. 

3. 關於汽車之比人快10倍, 但是本質上改變了人的生活的例子是借用的 Knuth 的, 具體可見 <Mathematics and Computer Science: Coping with Finiteness>, 文章發在 1976 年的 Science. 這是一篇好文章!

4. Ward Cunningham 的網站, http://c2.com/cgi/wiki 是歷史上最早的 wiki 百科, 只是全是關於計算機和編程的而已.

==============================

編程珠璣番外篇的番外篇 

1. 最近挺忙的, “編程珠璣番外篇”更新不快了, 閒散的短篇反而多了. (我和霍炬說過, 寫技術八卦非常的耗時間, 平均寫一篇要做50次以上的Google, 還要整理很多書的讀書筆記查資料等等, 耗時基本在三小時左右). <編程珠璣番外篇> 是一個我自己也非常中意的系列文章, 用師兄劉未鵬的話說, “這個系列傳播力很強”. 所以, 我會一直堅持寫下去, 寫完了匯成一個 PDF 大家取用.

2. 今天技術牛人 DBAnotes 推薦我的博客了, 他說:

4G Spaces
地址: http://blog.youxu.info/
作者: 徐宥
我和作者不認識。他的《編程珠璣番外篇》是今年見到的最好的技術八卦(並非貶義);”完全用鍵盤”工作的系列文章很好的體現了 Geek 精神。

感謝他的推薦和表揚. 希望這個技術八卦系列在2009年依然是質量上乘的技術八卦 :)

3. 下面的寫作計劃其實我早就有了, 苦於沒有時間碼字 (事實上從最近到1月12日, 我都沒時間碼字了). 爲了讓大家保持一個好胃口, 趁着 DBAnotes 的推薦, 說一下劇透好了 (有興趣的快訂閱本博客啊):

協程的八卦: 搶佔式多任務 協作式多任務的概念, 協程在編程語言裏的實現的歷史, yield 關鍵字的來歷. 協程在現代編程語言中的消亡和復興.

關於用戶級線程庫的八卦: 內核線程還是用戶線程, 歷史的演變, 各大編程語言的實現, 爲啥Python 不能在多核上提高效率而Java 能. 用戶級線程庫在現代編程語言中的復興.

Java平臺的動態化: JSR292 的前世今生, 一個靜態語言的平臺如何加入一個指令就能動態化? PVM 和 JVM 的區別在哪裏, 誰是下一代一統江湖的語言平臺?

我相信, 後面會寫的越來越精彩, 希望大家持續關注.

==============================

A.P2P客戶端的策略和奇妙的對策論-1 

最近日本著名演員飯島老師去世了. 在我這個年齡段的人中, 熟悉飯島老師的相信十有八九都是通過奇妙的叫做 bt 或者 電驢 的軟件認識的. 今天我們就來八卦一下程序設計人員是如何設計這些客戶端的策略使得您既能下載欣賞到飯島老師的片子, 又不會浪費您太多的上傳帶寬的. 簡單的說, 就是 P2P 軟件的客戶端的策略該如何設計, 使得整個系統能夠幫助每個用戶獲得相應的利益最大化.

要研究這個問題, 我們得從博弈論談起. 但是因爲這個是給程序員看的八卦, 不是數學專業課, 我們不在這裏說太多的數學, 而是用例子和八卦引入.

大家都知道, 1994 年的諾貝爾經濟學獎給了一個數學家, 約翰.納什 (電影”美麗心靈”爲證). 納什的理論工作是推廣了馮諾伊曼開創的極大極小定理(博弈論的基本定理). 而在通俗的對博弈論的介紹中, 提到納什, 一般都是着重在納什均衡和囚徒困境上. 我們不具體深究納什均衡的數學意義, 而是以下面一個具體的極其簡化的例子來說明囚徒困境:

假設 BT 網絡中兩個節點 阿強(A) 和 B哥(B) 要交換文件. 文件很大, 我們假設需要非常多輪交換才能完成. 每一輪, 每個節點可以選擇 平衡上傳/下載 和 幾乎不上傳/貪婪下載兩組策略. 我們按照博弈論的一般用語, 把第一種策略稱爲 C(合作), 第二種稱爲 D(叛變). 同時, 假設A, B 都是使用 ADSL 網絡, 所以上傳成本比下載成本要高很多, 我們在計算回報的時候考慮這樣的不對稱. 現在, 假設 A 和 B 各自有對方需要的文件, 那麼, 如果 A, B 同時選擇策略 C, 即平衡的上傳和下載, 他們得到的回報都是 3, 如果其中一個人偷雞選擇 D, 即幾乎不上傳, 光下載; 而另一個節點選擇 C, 則選擇 D 的能夠下載到所要的文件且幾乎不需要付出上傳的代價, 我們記回報爲 5, 而另一個人付出了上傳的費用, 卻得到了一點點的下載, 可以把回報看成是0. 如果兩個人都選擇貪婪下載, 幾乎不上傳, 那麼兩個人都得到了一點點下載, 現在這樣的下載量沒有3多, 但是因爲本身付出的上傳成本也少, 我們把這時候兩者的回報都定爲 1.

說了這麼多, 只是爲了讓問題更加的真實. 這些交代的條件的數學本質, 可用表格表示, 博弈論中稱之爲支付矩陣:

C(合作) D(叛變)
C (3,3) (0, 5)
D (5,0) (1, 1)

現在的問題是, 阿強和B哥都是理性的, 也是自私的, 因此, 他們都認爲, “假如我選 C, 對方可能選 C 或者 D, 那麼我這個策略最糟糕的情況下收益是 0, 而假如我選 D, 最糟糕的情況下收益是 1″ 那麼, 因爲 D 下最糟糕的收益比 C 最糟糕的情況下收益要大, 理智的人肯定選D. 我們看到, 兩者選擇 D 都是理性的, 但是實際上從對兩者的收益分析看, 兩者都選擇 C 纔是更加優的. 這個表面上看上去很理智但是最後沒有到達對雙方最好的結果的困境, 就是所謂的囚徒困境. (看過這篇八卦, 您也可以叫做飯島老師困境)

關於囚徒/飯島困境的簡單介紹就到這裏, 現在我們看我們的原始問題. 我們知道, BT 交換文件是分成一塊一塊的, 也就是說, 是一次一次的交換的. 我們把每次交換叫做一輪的話, 整個系統是一個多輪的博弈問題(或者叫做多階段的博弈問題). 這個博弈問題, 就顯得好玩起來了. 爲什麼呢, 因爲多階段博弈, 居然能夠讓自私的A和B兩個節點爲了自己的利益, 進化出合作來.

我們先簡單的說明一下多階段博弈不必然的能跳出囚徒困境. 比方說, 如果 A 和 B 知道一共有 N 輪博弈, 那麼最後一輪, 理智的他們肯定都陷入了囚徒困境, 在第 N 輪 的策略清楚之後, N 的問題就轉化爲 N-1 輪的問題. 所以, 必然的, A 和 B 在所有 N 輪上, 都會陷入囚徒困境 (好比奸商一輩子只和你做有限次買賣的話, 就會一直黑你, 不黑白不黑). 他們等到花兒也謝了, 也不能得到自己想要的內容. 但是, 問題的奧妙在於, 假如A 和 B 不知道一共多少輪, 或者有無限輪呢? 假如阿強在某輪選擇平衡的上傳和下載(C), 則可能正好碰上 B 哥 也選擇”友好合作”, 那麼, 兩個人都舒舒服服的交換了飯島老師的片片. 所以, 對於一個設計良好的BT客戶端, 問題的關鍵在於怎麼選擇自己的策略, 使得既能完成自己自私的下片目標, 又能注意和其他客戶端良好的合作使得自己的收益最大, 而不在於在特定的一輪中自己的得失.

這裏, 我們的目標是設計一個良好的策略. 通常, 在設計一個實踐中性能良好的算法的時候, 數學家和計算機科學家在這裏的方法就鮮明的分野了. 數學家, 會證明這樣算法的存在性, 性能上下界, 和衆多的必要條件, 以及算法之間在最理想的情況下的好壞比較. 而計算機科學家, 會像搭積木一樣, 用不同的基本模塊, 直接嘗試不同的組合, 一一做實驗, 看哪種方法最好. 在這裏, 我僅介紹一種計算機科學家的方法: 通過讓不同方法比賽, 取出贏家, 贏家的方法最好的方法. 其實準確的說, 這個就是達爾文的適者生存的方法. 而這個比賽本身又是一段非常有趣的八卦, 因此我着重花筆墨介紹一下.

在心理學和行爲學領域, 有一本非常著名的書, 叫做<合作的進化>. 其作者, 記載了在80年代, 他組織的兩次比賽, 叫做IPD (Iterative Prisoner’s Dilemma, 多輪囚徒困境). 競賽的目的是在一個多輪的囚徒困境中找出最好的策略, 參賽者自己寫好算法程序, 然後由組織者讓這些程序兩兩對弈, 看誰在多輪囚徒困境中得到最多的分. 在所有的數學家計算機科學家等提交的很多程序中, 表現最好的一個策略, 超乎尋常的只有四行簡單的 Basic 程序. 這四行 Basic 程序, 勾勒出了一個叫做 “針鋒相對” 的算法(Tit for Tat).  這個算法策略很簡單, 一開始採用合作, 假如對方上一輪合作, 則本輪合作. 如果對方上一輪對抗, 則本輪對抗. 用中國人熟悉的話說, 叫做”人不犯我, 我不犯人; 人若犯我, 我必犯人”. (四句話正好對應四行程序, 不是巧合). 其他的算法, 比如隨機算法呀, 永遠敵對的算法呀, 都比不過這個算法. 因此, 這個算法贏得了第一年的競賽.

第二次, 各位吸取教訓, 繼續開發好算法. 猜猜第二次誰贏了? 居然還是那四行程序! 在合作的進化中, 作者從”寬容, 以牙還牙”等社會學的角度去解釋爲啥這四行程序會贏. 或許對人生有深刻思考的人會感嘆, 這四行程序的確蘊含了深刻的智慧. 但是, 很不幸的是, 這個程序在現實中, 有一個非常大的漏洞, 而因爲這個漏洞, 使得BT程序如果不修改策略, 先現實中會寸步難行. 這個看上去非常理智非常聰明的策略到底是怎樣的大漏洞呢, 我先賣個關子, 下回分解.

(想看劇透的, 可以看 Wikipedia 的條目: Tit for Tat: http://en.wikipedia.org/wiki/Tit_for_Tat )

 

上篇我們說到 Tit for Tat 的策略有一個極大的漏洞, 是什麼樣的漏洞呢? 我們不妨先用通俗的例子理解一下.

假如現實生活中有兩個人 A 和 B, 都是認爲自己非常理智, 而且嚴格執行”以牙還牙”策略的人遇到了一起, 會發生什麼樣的事情呢? 我們按照他們初始的策略, 分三種情況討論.

1. 假如 A 某次不小心招惹了一下 B (執行了被 B 解讀爲 D 的策略), 按照 B 的策略, 必然會在下一輪執行 D 策略 (報復). 而 A 對 B 初始是執行 C 策略 (合作) 的. 在 B 報復之後, A 下一輪就會採用報復. 而相反的, B 在本輪看到 A 合作之後, 下一輪就會報復. 如此往返. 不難看出, A 和 B 會陷入彼此報復的怪圈當中, 用大白話說, 就是所謂的冤冤相報何時了.  更加糟糕的是, 博弈的雙方都認爲自己是完全理智而且願意合作的, 但是就是因爲正好彼此差了一步, 因此從A的角度看B, A 會認爲 B 是一個完全不懂得合作的蠢貨 (A 提出合作的時候B正好報復). B 看 A 也一樣. 現實生活中我們也能發現這種例子, 比如兩個性格很強的人遇到了, 在某件事情上不投合, 結果成了一輩子的仇人, 還互相認爲對方是傻X. 此時, 雙方都得不到期望的最大受益.

2. 如果一開始雙方都採用 D 策略, 則可以遇見, 這樣的 D 策略將持續下去, 沒有一方會主動的讓步, 因爲先讓步的一方必然吃虧. 現實中, 我們也能觀察到這樣的事情, 即博弈雙方仇怨越積越深, 最後到了不可化解的地步. 此時, 雙方都陷入了囚徒困境.

3. 如果博弈的雙方一開始都採取 C(合作)策略. 那麼, 博弈雙方則能夠永遠的友好合作下去, 獲得最大的受益. 此時, 雙方獲取的受益都最大化了.

從上面的分析我們可以看到, 在多輪囚徒困境的情況下, 如果有多個 Tit-for-Tat 策略參與, 那麼每個的受益, 極端的依賴於初始狀態的設定. 在數學和計算機科學中, 這樣的系統, 叫做”初值敏感系統”. 一般認爲, “初值敏感系統”是非常不好的系統, 原因在於缺乏”魯棒性”. 這裏我走一下題, 解釋一下初值敏感系統和魯棒性這兩個概念.

大家都知道有一個叫做”蝴蝶效應”的東西, 大體是說, 一隻蝴蝶在巴西扇動翅膀,有可能會在美國的德克薩斯引起一場龍捲風. 原因在於, 這隻蝴蝶翅膀扇動的氣流, 引起的一個小小的攪動, 可能會在系統中被各種各樣的因素放大, 最後演變成一個非常顯著的效應. 中國也有一句古話, 叫做差之毫釐, 謬以千里, 說的都是, 初始的微小變化, 都能引起最後結果的顯著不同. 我們這裏的初值敏感系統, 和蝴蝶效應也是類似的, 能從小的攝動引發出顯著的後果的. 比如大家都知道, 在”一隻饅頭引發的血案”中, A 在很不經意的情況下, 對B 採用了 D 策略(搶了饅頭), B 由此產生了報復, 搞得 A 國破家亡.

顯然, 面對這樣的系統, 人類即使有模型, 也是很難預測未來的, 因爲初值條件在測量上的一點點微小的誤差, 都能造成預測的結果的巨大不同. 爲了表徵這個特性, 我們把”不對初值敏感”的特性成爲魯棒性 (Robustness). (這個魯棒, 您可以直接理解爲山東大棒, 結實, 抗得住外界的一些攝動).

聰明的讀者要說了, 即使系統不魯棒, 我們能不能設計好初值, 使得系統沿着最好的方向演化呢? 答案是不能. 因爲任何一個客戶端擁有的上傳和下載的帶寬都是有限的, 有限的資源必然會導致資源的競爭, 從而導致必然某些請求不能滿足. 在這種情況下, D 策略是不可避免的. 況且, 網絡情況複雜多變, 即使雙方都有意採取 C 策略, 很可能因爲網絡的複雜性, 雙發獲得的受益不對等, 從而引發一方採取 D 策略. 所以, 如果 Tit for Tat 這種初值敏感策略放到 P2P 客戶端中, 結果是不可想像的, 因爲這時候每個客戶端都是碰不得的刺蝟, 一旦在某個時間點某個節點出現了差錯, 很可能整個系統都陷入”冤冤相報”的死結, 讓整個網絡沒法完成文件的傳輸, 反而忙着互相報復和自我保護.

從上面的分析我們看出, 靠精心設計初值來維護這個系統是不現實的, 我們需要設計的, 是一個好的策略, 使得不管初值怎麼變, 系統中每個個體依然能夠獲得較大的收益.  那麼, 怎樣設計這個魯棒的系統呢? 我們從極端的兩個例子開始, 一種是不管別人怎麼出牌, 永遠合作的; 另一種是或者不管別人怎麼策略, 永遠背叛的. 這兩個都很魯棒, 都很”彪悍”. 但是毫無疑問, 效用不見得最大化.

從這兩個極端的例子表現不怎麼好來看, 我們的確應該要根據對手的策略選擇自己的策略, 同時又不能非常的依賴於對手的策略(否則就初值敏感了). 那麼, 最簡單的方法就是: 我們以一定的概率去執行以牙還牙, 但是也允許以一定的概率不管上次選什麼, 這次和對手選擇合作(跳出怪圈). 這樣, 因爲隨機性的引入, 對初值的依賴就隨着時間的流逝越來越小了.

在多個人的環境中, 我們的確願意和對手選擇隨機合作, 但是因爲資源的限制, D 是不可避免的. 但是我們不會讓 D 永遠下去, 我們每輪和隨機的對手選擇一次隨機的合作, 這樣就不會被怪圈所左右. 這個就是 bt 協議跳出冤冤相報的精髓. 一旦知道了這個, 本文思想就差不多介紹完了. 下面就是程序員的編碼工作了. 下面的內容完全是基於 Bram Cohen (bt 協議創始人) 的經典論文 “Incentives Build Robustness in BitTorrent” ( http://www.bittorrent.org/bittorrentecon.pdf ) 裏面的內容展開的. 我只介紹和博弈論有關的部分. 讀英文更加習慣的讀者直接看原論文比讀下面的文章更加好.

首先說點背景知識, bt 把文件看成一塊一塊的, 並且用一定的排序算法決定現在能夠下載哪一塊. 其次, bt 協議同時和多個機器之間建立 TCP 連接, 但是採用堵塞的方法控制傳輸. 因爲建立連接代價比較大, 所以 bt 協議維持連接不變, 在其上採用 choking (堵塞) 的方法來執行 D 策略, 採用 un-choking 的方法 來執行 C 策略, 而不是每次都重新建立和取消連接. IP 協議在這方面有天然的優勢.

每次, BT 協議選擇 k (通常爲7, 限速的情況下爲2, 3, 或 4) 個其他的客戶端來執行 C 策略(即給上傳). 在上一輪中給出最多下載的那些節點, 在本輪將被執行 C 策略(注意到有的節點上一輪並沒有給上傳, 即從C 到 D). 同時, 爲了避免其他的更加好的節點被忽視, 每 m 輪, BT 客戶端選擇一個隨機的 choke 了的節點執行 C 策略 (即從 D 到 C. 同時, 因爲資源限制, 必然有一個被 choke 了, 即從 C 到 D).

那麼, 什麼時候執行 D 呢? 在 BT 協議中, 假如連續 n 輪, 都沒有從一個節點收到任何下載, 在 bt 術語中, 這個叫做 snubbed. 這時候, 則該節點認爲自己被那個結點執行D策略了. 作爲報復, 自己也停止對該節點的上傳(即以牙還牙, 從 C 到 D. ). 除非等到下次隨機的選到了那個節點(再次到 C ).

這就是 bt 的協議關於博弈論的全部. 其中, 一輪持續時間在現在的實現中是 10 秒. m 爲 3, n 爲 6. 目前暫不清楚 Bram Cohen 是否通過實驗得到這些參數, 有興趣的讀者可以自己查閱 bt 源代碼, 改一下, 看看哪個更加好. 同時, 因爲其他客戶端採用的是 Tit for Tat, 想把自己的客戶端改成 吸血bt 是不可行的, 也佔不到別人便宜.

PS: 有興趣的讀者可閱讀 bt 源代碼中的 Choker.py. BT 源代碼用 Python 寫成, 比較好懂.

==================================

C.正則表達式精義-1 

很多天前和 zuola 聊天, 偶然提到正則表達式, zuola 說, 會正則表達式的都是牛人. 我說, 其實不難, 買本書看看就會了. 這幾天, zuola 又在我博客上留言說會正則表達式纔是真的程序員, 因此我想, 還是寫篇比較淺顯的教程, 讓 zuola 同學快速成爲牛人吧.

對於普通人來說, 正則表達式是比較難的. 從我個人的體驗來看也是一樣. 這個難, 主要在於兩方面:

1. 接受正則表達式的思維方式;

2. 熟悉表達式裏面各種各樣的符號的用法.

第一點的難度在於這是個新東西, 和以前的知識結構不一樣; 第二點的難度在於各種各樣的環境下都對最基本的正則表達式做了很多擴展, 引入了各種各樣的新的符號, 這樣, 就使得學的時候一下子面對太多的複雜度不知所措. 舉例來說, 大多數教程把 ^$*+-[](){}|.?/ 這些符號全部放到一起講, 全然不分他們的層次關係, 導致學習者雲裏霧裏. 同時, 不同的工具又定義了自己的特殊規則, 使得學習曲線更加陡峭. 因此, 我打算把正則表達式的知識點, 分幾個不同的層次, 一一剖析. 在這一部分中, 我把正則表達式瑣碎的細節一一剔除, 希望看到這篇文章的, 願意學習正則表達式的讀者, 能夠迅速從這些繁瑣的細節中解脫出來, 掌握其本質.

首先說正則表達式是什麼. 正則表達式是一種描述性的語言, 用來概括一類字符串 (或者說一個字符串集合).

我們當然可以用自然語言來描述一類字符串, 比如我們說, 以 “010 開頭的電話號碼”, “夾在HTML 的 <b> 和 </b> 中間的內容”, “含有 hello 的字符串”, “負數”, “IP地址” “郵箱地址”, 等等. 其實在實際應用中, 我們也常常有這個需求, 比如說提取一篇郵件中所有的 email 地址 (查找), 或者把提取某類電話號碼, 升個位, 加個區號什麼的 (替換). 人當然可以做這個事情, 但是這個事情重複且單調, 又並不需要太多的智力, 因此, 計算機是最好的工具. 但是問題是, 我們怎麼能夠告訴計算機, 我們對哪類字符串感興趣呢? 計算機科學家就幫我們設計了一種讓人能夠簡單的寫出來, 表達我們人類想表達的含義, 而計算機又恰好能夠很容易的理解和處理的一種表達式, 這就是正則表達式了. 從人和計算機的角度說, 正則表達式是一種人和計算機都能輕鬆處理的約定, 用來描述一類具有某個性質的字符串.

正則表達式它既有傾向於人的思考方式的一面, 也有傾向於計算機工作原理 (有限自動機) 的一面. 因此, 傳統意義上, 如果想真正理解正則表達式, 就要從理解計算機原理入手. 所幸的是, 我們普通用戶, 在日常使用中, 並不需要了解計算機的原理, 因爲這麼多年技術的發展給了正則表達式很多新特性, 讓正則表達式越來越脫離計算機的侷限, 變得更加適合複雜的任務, 但這樣的代價是正則表達式的細節越來越繁雜了, 對於初學者來說更加難學了. 因此我們在這裏, 先講本質, 後談細節.

最基本的正則表達式, 只有三句話:

一個字符串是一個正則表達式

比如 aaa, 就是一個正則表達式, 它描述了一個字符串集合, 這個字符串集合裏面只有 aaa 這一個元素

兩個正則表達式可以直接串起來, 比如 aaabbb 其實, 是由六個正則表達式 a a a b b b 接起來組成的. 我們先籠統的說, 接起來就等於把描述的內容接起來, 等一下再詳細解釋接起來的含義.

兩個字符串, 比如 aaa 和 bbb, 用 | 連起來, 變成了 aaa|bbb, 也構成一個正則表達式

它描述的字符串集合是原來分別的並集, 比如 aaa|bbb 描述了一個集合, 這個集合裏面有 {aaa, bbb} 兩個字符串.

好了, 就這兩三話, 就可以解釋正則表達式最基本的思維方式了: 用一個表達式, 去描述一類字符串(或者說, 一個集合).

光有這兩個, 還不夠強大, 因爲上面的正則表達式, 我寫幾個, 就描述了幾個字符串, 也就是說, 描述來, 描述去, 都是有限的集合, 不能描述無限的集合. 而我們想要描述的整數啊, 域名啊, 郵箱地址啊, 都是一切就有可能的, 因此, 我們有必要引入一個新的記號, 能夠描述無限的集合,

一個正則式 X 可以加上一個 *, 用來描述任意多個原來 X 描述的字符串拼起來的字符串.

這句話比較費解, 我們用例子來說明一下, 比如 a* 這個正則表達式, 我們知道 a 描述了一類字符, 這類字符裏面只有一個 a, 所以, a* 描述了一個或者多個 a.

我們再看 a | b* , 按照定義, 這個正則表達式描述了 a 和 b, bb, bbb 等. 如果我們引入一個括號, 寫成 (a|b)* , 那麼 a|b 就變成一個整體, 描述了 a 或者 b, 這時候, (a|b)* 就是一切只由 a, b 組成的字符串. 這裏的括號, 是爲了避免歧義, 表示 * 是作用在 a|b 整體上的. 這時候, (a|b) 描述了 a 和 b, 整體加了一個 *, 意味者我們可以任意選 a 或者 b 一個接一個拼起來, 所以, aba, aab 都是在 (a|b)* 的那一類裏面的. 注意, * 可以匹配 0 個, 就是說, 這裏麪包含了什麼都沒有. 比如說 ab*c 也描述了 ac, 因爲中間可以有 0 個 b. 如果您想至少要一個b, 可以寫成 abb*c.

爲了幫助您理解接起來, 我們再看一個複雜的例子, o(n|ff). 我們知道, n|ff 描述了 n 或者 ff. 當我們直接把 o 接在前面的時候, 描述的是 on 或者 off. 就是說, 接起來的時候, 要把 o 和後面每種情況都組合一次. 我們再看 (a|o)(n|ff). 前面描述的是 a 或者 o, 後面描述的是 n 或者 ff, 接起來, 描述了 an, aff, on, off.

我們都知道, 正則表達式描述的是一類字符串, 所以, X 和 Y 在接起來變成 XY 以後, 自然的變成了描述 每一種 X 裏面的字符串和 Y裏面字符串接起來的情況. 同樣, * 好像把 X 和自己接起來多次一樣 (可以是任意次), 每次只要接起來的是X裏面的字符串, 就一定被 X* 所表述.

(熟悉集合的朋友立即知道 正則表達式是用一個表達式代表了一個集合, X|Y 等價於兩個集合的並集, 而 XY 拼起來等價於他們所有的元素 x, y 拼起來的集合).

好了, 恭喜您, 您已經學會正則表達式了. 真的, 你已經全部學會了正則表達式的知識. 不過不着急, 我們先回顧一下正則表達式的要點:

1. 正則表達式由普通的字符, 以及幾個特殊的字符, 即 括號 (), 或者 | 和 星號 * 組成. 用來描述一類字符.
2. | 表示或者. 如果有兩個正則表達式 X 和 Y, 那麼 X|Y 就描述了原來 X 描述的和 Y 描述的.
3. 正則表達式可以接起來, 變成一個更長的, 描述了一個各個部分被那些被接起來的正則表達式描述的字符串.
4. () 是爲了避免歧義.

我們上面說的這四個, 就是 100% 如假包換的正則表達式了. 以後的, 都是爲了更加方便的使用正則表達式, 而又引入的一些擴展. 恰恰是這些擴展, 讓初學者陷入了細節的泥潭. 我們在下一節, 一個一個的來對付諸如 +, [, -, ], ^, $, {m}, 等這些非基本的高級的功能. 需要強調的是, 這些高級的功能, 其實都只是爲了人書寫方便, 而且是完全可以用我們這裏說的最基本的幾個規則代替的. 這些高級功能, 我們下節再講.

練習:
寫出匹配以下性質字符串的正則表達式:

1. 字符串 2009

2. 周曙光同學有兩個名字, 分別叫做 zola 和 zuola, 人們常常混淆. 請幫周曙光同學設計一個正則表達式, 可以幫他匹配自己的名字.

3. 二進制數字 (最少有一位, 但只含有 0 或者 1的)

4. 非零的十進制數字 (有至少一位數字, 但是不能以0開頭)

練習軟件:

有一些比較好的軟件幫你學習正則表達式, 我推薦初學者用 egrep. 可以在 windows 下用, 具體用法是在命令行 打入 egrep “正則表達式” 文件名
egrep 會把文件裏面和正則表達式匹配的行 (該行含有一個字符串, 被正則表達式描述了) 打出來. egrep -o “正則表達式” 文件名 的話就會只打出那個完全匹配的字符串, 而不是行. 另外, 在 Linux 下可以用 grep –color “表達式” 文件名, 這樣, 匹配上的那個字符串, 會被高亮顯示出來.

練習文件:

0108200920088964
zuola -d
zooooola
world hello -012012 2009
0909 zola zhou
0101001
zuola

(把這個文件存成文本文件, 用 windows 的朋友可以放在您的 “我的文檔” 裏面, 因爲 cmd 就是從那裏開始運行. 然後您下載一下 egrep 做實驗)

答案:

1. 2009
2. z(|u)ola [或者您可以寫成 zuola|zola]
3. (0|1)(0|1)*
4. (1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)*

你會看到第四題的答案很笨拙, 居然寫了這麼長. 後面的大部分細節, 就是爲了諸如此類的寫得更加簡潔一點.

Update:

1. 按照 AW 的留言和他的博客上的讀者留言, 這個在線網站可以在線測試正則表達式:

http://gskinner.com/RegExr/

2. 如果要論正則表達式方面的參考書的話, 我推薦 < 精通正則表達式>, 中文版餘晟同學翻譯的, 質量上乘. 這本書可能是正則表達式方面唯一的一本聖經了, 上次我也是直接推薦給 zuola. 本來我是想打算寫完了所有的初級教程再推薦的, 所以在本文初稿中沒有提到這本參考書.

3. 才和 zuola 聊天, 他說要講點具體的 blogger 用到的例子. 其實我之所以沒在這篇文章裏面講, 就是因爲這樣的例子, 都是和應用程序結合的, 需要 sed, htaccess, awk 或者 linux 管道的具體知識, 我就是想解開這些知識的耦合. 一下子看着天書一樣的 sed 替換表達式, 是很難一下子學會的. 他的建議是非常有價值的, 可能在本系列最後, 我會補充一篇 blogger 常用的正則表達式用例. 

================================

D. 高級語言怎麼來的-1 

終於放暑假了, 有心情來八卦了. 我主要想八卦一下高級語言的設計思想和各種範式的來龍去脈, 也就是回答這個問題: 編程語言爲什麼會發生成現在這個樣子哩? 這裏面的奧妙又在哪裏哩? 我嘗試着把這個系列的八卦寫下去, 包括虛擬機的設計, 線程的設計, 棧和寄存器兩大流派的來龍去脈等等, 也算是完成年初給大家許下的諾言.

高級編程語言的創始紀上寫道:”初, 世間無語言, 僅電路與連線. 及大牛出, 天地開, 始有FORTRAN, LISP. ALGOL 隨之, 乃有萬種語.” 我們都知道, LISP 是基於遞歸函數的, FORTRAN 是做科學計算的. 現在的C 等等, 都比較像 FORTRAN 不像 LISP. 可是很少有人知道, 最初, FORTRAN 是不支持函數遞歸調用的, 而LISP是一生下來就支持的, 所有高級語言裏面的遞歸調用, 都是逐漸從 LISP 那裏學來的. 這段塵封的歷史非常有趣, 值得八卦一番.

一般人學編程, 除了寫 Hello World 之外, 人生寫的第二個程序, 不是階乘就是菲波拉契數列, 要不就是漢洛塔. 而這幾個程序, 基本上都是因爲函數的遞歸調用才顯得簡單漂亮. 沒有遞歸的日子裏, 人民非常想念您. 可是, 第一版的 FORTRAN 就居然居然不支持遞歸. 細心的讀者要問了, 不支持遞歸的語言能圖靈完全麼? 當然可以, 圖靈機就是沒遞歸的典型的例子. 但是沒遞歸調用的程序會很難寫, 尤其像漢諾塔這種. 那麼, FORTRAN 他怎麼就悍然不支持遞歸呢, 讓我們回到 1960 年.

話說當年, IBM 是計算機行業的領軍者. 那時候的計算機, 都是比櫃子還大的大傢伙, 至於計算能力嘛, 卻比你的手機還弱. 那時候計算機所做的最多的事情, 不是發郵件打遊戲, 而是作計算. 作計算嘛, 自然需要一種和數學語言比較接近的編程語言. 於是, 1960年, IBM 就搗鼓出了 FORTRAN, 用行話說, 就是公式翻譯系統. 這個公式翻譯系統, 就成了世界上第一個編程語言. 這個編程語言能做數學計算, 能作條件判斷, 能 GOTO. 用現在的眼光看, 這個語言能構模擬圖靈機上的一切操作, 所以是圖靈完全的. 學過數值計算的同學都知道, 科學計算無非就是一大堆數學計算按照步驟進行而已. 所以, 一些控制判斷語句, 數學公式加上一個數組, 基本上就能完成所有的科學計算了. IBM 覺得這個語言夠用了, 就發佈了 FORTRAN 語言規範, 並且在自家的大型機上實現了這個語言. 

在實現這個語言的時候, IBM 的工程師要寫一個 FORTRAN 編譯器 (請注意那時候的大型機沒有操作系統). 那時候的編譯器都是用機器語言或者很低級的彙編語言寫成的, 所以編譯器要越簡單越好. 這些工程師覺得, 弄一個讓用戶運行時動態開闢內存的機制太麻煩了, 所以乾脆, 強迫用戶在寫程序的時候, 就要定好數組的大小, 變量的類型和數目. 這個要求並不過分, 因爲在科學計算中, 數組的維度, 用到的變量等, 在計算之前, 就是可以知道大小的. 用現在的話說, 就是不能動態開闢內存空間, 也就相當於沒有 malloc 的 C, 或者沒有 new 的 C++. 這樣的好處是, 一個程序要多少內存, 編譯的時候就知道的一清二楚了. 這個主意看上去很聰明, 不過 IBM 的工程師比你想得更加聰明, 他們想, 既然一個程序或者子程序要多少內存在編譯的時候都知道了, 我們乾脆就靜態的把每個子程序在內存中的位置, 子程序中參數, 返回值和局部變量放的位置, 大小都定好, 不久更加整齊高效麼. 是的, 我們都知道, 在沒有操作系統管理的情況下, 程序的內存策略越簡單越好, 如果內存放的整整齊齊的, 計算機的管理員就能夠很好的管理機器的內存, 這樣也是一件非常好的事情. (再次強調, 當年還沒有操作系統呢, 操作系統要等到 1964年發佈的 IBM 360 纔有, 具體開發一個操作系統之難度可參考< 人月神話>).

可是, 聰明的讀者一下子就看出來了, 這樣靜態的搞內存分配, 就遞不成歸不了了. 爲啥呢. 試想, 我有個 Fib 函數, 用來計算第 N 個菲波拉契數. 這個函數輸入一個整數, 返回一個整數, FORTRAN 編譯器幫我把這個函數給靜態分配了. 好, 我運行 Fib(5) 起來, FORTRAN 幫我把 5 存在某個專門給輸入參數的位置. 我在 Fib(5) 裏面遞歸的調用了Fib(4), FORTRAN 一看, 哈, 不還是 Fib 麼, 參數是 4, 我存. 這一存, 新的參數4, 就把原來的 5 給覆蓋掉了, 新的返回值, 也把原來的返回值給覆蓋掉了. 大事不好了, 這麼一搞, 新的調用的狀態居然覆蓋了老的調用, 這下, 就沒法返回原來的 Fib(5) 了, 這樣一搞, 怎麼遞歸啊?

IBM 這些寫編譯器的老前輩們, 不是不知道這個問題, 而是壓根就鄙視提出這個問題的人: 你丫科學計算遞歸什麼呀, 通通給我展開成循環, 展不開是你數學沒學好, 想用遞歸, 你就不要用 FORTRAN 了. 那時候 IBM 乃是老大, 只有他們家才生產大型機, 老大發話, 下面的消費者只能聽他的.

既然軟件不支持, 硬件也就可以偷工減料嘛, 所以, 硬件上, 就壓根沒有任何棧支持. 我們都知道, 計算機發展史上, 軟件和硬件是相互作用的. 我們現在也很難猜測, 是IBM 的軟件工程師因爲 IBM 的硬件工程師沒有在硬件上設計出堆棧所以沒有能在 FORTRAN 裏面設計出遞歸調用呢, 還是 IBM 的硬件工程師覺得既然軟件沒要求, 我就不設計了呢? 不管怎麼樣, 我們看到的是, 1960 年前, 所有的機器的硬件都沒有直接支持棧的機制. 熟悉CPU的都知道, 現代 CPU 裏面, 都有兩個至關重要的地址寄存器, 一個叫做 PC, 用來標記下一條要執行的指令的位置, 還有一個就是棧頂指針 SP. 如果沒有後者, 程序之間的調用就會非常麻煩, 因爲需要程序員手工維護一個棧, 才能保證程序之間調用最後還能正確的返回. 而當年, 因爲 FORTRAN 壓根就不支持遞歸, 所以支持 FORTRAN 的硬件, 就省去了棧指針了. 如果一個程序員想要遞歸調用, 唯一的實現方法, 就是讓程序員借用一個通用寄存器作爲棧指針, 自己硬寫一個棧, 而且不能用 FORTRAN.

因爲 FORTRAN 不支持遞歸調用, 按照自然規律, 自然會有支持遞歸的語言在同時代出現. 於是, 很快的, LISP 和 ALGOL 這兩個新語言就出道了. 我們只說 LISP. 它的創始人 John McCarchy 是 MIT 教授, 也是人工智能之父, 是學院派人物. 他喜歡丘齊的那一套 Lambda 演算, 而非圖靈的機械構造. 所以, LISP 從一開始, 就支持遞歸的調用, 因爲遞歸就是 lambda 演算的靈魂. 但是有兩大問題擺在 McCarchy 面前. 一是他的 LISP 理論模型找不到一個可以跑的機器, 二是他的 LISP 模型中有一個叫做 eval 的指令, 可以把一個字符串當成指令在運行時求值, 而這個, 當時還沒有人解決過. 按照 Paul Graham 大叔在他的 Hackers and Painters 裏面的說法, McCarchy 甚至壓根就不想實現這個 eval 指令, 因爲當 IBM 的一個叫 Steve Russell的工程師宣稱要實現 eval 的時候, McCarthy 還連連搖手說理論是理論, 實際是實際, 我不指望這個能被實現. 可是, Russell 居然就把這兩個問題一併給解決了(這哥們也是電子遊戲創始人, 史上第一個電子遊戲就是他寫的, 叫 Space War). 他的方法, 說來也簡單, 就是寫了一個解釋器, 讓 LISP 在這個解釋器裏面跑. 這個創舉, 讓傳統上編譯-> 運行 的高級語言流程, 變成了 編寫-> 解釋執行的流程, 也就是著名的 REPL 流程. 他做的事情, 相當於在IBM 的機器上用機器碼寫了一個通用圖靈機, 用來解釋所有的 LISP 指令. 這個創舉, 就讓 LISP 從理論走到了實踐.

因爲有了運行時的概念, LISP 想怎麼遞歸, 就可以怎麼遞歸, 只要運行時支持一個軟件實現的棧就可以了. 上面我也說了, 也就是寫解釋器的人麻煩一點而已, 寫LISP程序的人完全就可以不管下層怎麼管理棧的了. 同時, 有了解釋器, 也解放了原來動態分配空間的麻煩, 因爲現在所有的空間分配都可以由解釋器管理了, 所以, 運行時環境允許你動態的分配空間. 對空間分配的動態支持, 隨之就帶來了一項新技術: 垃圾收集器. 這個技術出現在 LISP 裏面不是偶然的, 是解釋器的自然要求和歸宿. 在 FORTRAN 上本來被繞過的問題, 就在 LISP 裏面用全新的方法被解決了. LISP 的劃時代意義和解釋器技術, 使得伴隨的很多技術, 比如抽象語法樹, 動態數據結構, 垃圾收集, 字節碼等等, 都很早的出現在了 LISP 中, 加上 LISP 本身規則很少, 使用起來非常靈活, 所以, 每當有一項新技術出現, 特別是和解釋器和運行時相關的一項新技術出現, 我們就會聽到有人說, “這玩意兒 LISP 裏早就有了”, 這話, 是有一定道理的.

除了上面的軟件模擬之外, MIT 還有一派在作硬件模擬, 這一派, 以後發展成了燦爛一時的 LISP machine, 爲日後所有虛擬機理論鋪開了一條新路. 這一派在70, 80年代迅速崛起, 然後隨着 PC 的興起又迅速的隕落, 讓人唏噓不已.

最後附送一個八卦: 1960 年的時候, 高級語言編程領域也發生了一件大事, 即 ALGOL 60 的提出. ALGOL 是劃時代的標準, 我們今天用的 C/Java 全是 ALGOL 家族的. ALGOL 注意到了 FORTRAN 的不支持遞歸的問題, 於是從一開始, 就訂立標準支持遞歸. 但是, 處理遞歸需要很小心的安排每個函數每次調用的地址和所謂的活動窗口(Active Frame), 而並不是每個編譯器都是牛人寫的, 所以在處理遞歸這樣一個新事物上, 難免會出點小問題和小 BUG. 這時候, 搞笑的高爺爺(Knuth) 出場了, 他提出了一個測試, 叫做 “是男人就得負67″. (The man or boy test). 恕我功底不深, 不能給各位讀者把這個男人測試的關竅講清楚, 但是, 我知道, 這個測試, 乃是看 ALGOL 60 編譯器有沒有正確的實現遞歸和外部引用的. 照高爺爺的說法, 真的男人要能得到正確答案, 不是男人的就得不到正確答案. 當然, 高爺爺當時自己也沒有男人編譯器, 所以自己猜了一個 -121, 後來, 真的男人編譯器出來了, 正確答案是 -67. 可見, 高爺爺的人腦編譯器, 也不是男人編譯器…

各位欲知詳情的, 猛點這個.

===============================

E. 高級語言怎麼來的-2 

虛擬機的前世今生

上節我們提到了 LISP 中, 因爲 eval 的原因, 發展出了運行時環境這樣一個概念。基於這個概念,日後發展出了虛擬機技術。但這段歷史並不是平鋪直敘的,實際上,這裏面還經歷了一個非常漫長而曲折的過程, 說起來也是非常有意思的。 這一節我們就着重解釋虛擬機的歷史。

我們 21 世紀的程序員,凡要是懂一點編程技術的,基本上都知道虛擬機字節碼這樣兩個重要的概念。 所謂的字節碼 (bytecode),是一種非常類似於機器碼的指令格式。這種指令格式是以二進制字節爲單位定義的(不會有一個指令只用到一個字節的前四位),所以叫做字節碼。所謂的虛擬機,就是說不是一臺真的計算機,而是一個環境,其他程序能在這個環境中運行,而不是在真的機器上運行。現在主流高級語言如 Java, Python, PHP, C#,編譯後的代碼都是以字節碼的形式存在的, 這些字節碼程序, 最後都是在虛擬機上運行的。

1. 虛擬機的安全性和跨平臺性

虛擬機的好處大家都知道,最容易想到的是安全性和跨平臺性。安全性是因爲現在可執行程序被放在虛擬機環境中運行,虛擬機可以隨時對程序的危險行爲,比如緩衝區溢出,數組訪問過界等等進行控制。跨平臺性是因爲只要不同平臺上都裝上了支持同一個字節碼標準的虛擬機,程序就可以在不同的平臺上不加修改而運行,因爲虛擬機架構在各種不同的平臺之上,用虛擬機把下層平臺間的差異性給抹平了。我們最熟悉的例子就是 Java 了。Java 語言號稱 一次編寫,到處運行(Write Once, Run Anywhere),就是因爲各個平臺上的 Java 虛擬機都統一支持 Java 字節碼,所以用戶感覺不到虛擬機下層平臺的差異。

虛擬機是個好東西,但是它的出現,不是完全由安全性和跨平臺性驅使的。

2. 跨平臺需求的出現

我們知道,在計算機還是鎖在機房裏面的昂貴的龐然大物的時候,系統軟件都是硬件廠商附送的東西(是比爾蓋茨這一代人的出現,纔有了和硬件產業分庭抗禮的軟件產業),一個系統程序員可能一輩子只和一個產品線的計算機打交道,壓根沒有跨平臺的需求。應用程序員更加不要說了,因爲計算機很稀有,寫程序都是爲某一臺計算機專門寫的,所以一段時間可能只和一臺龐然大物打交道,更加不要說什麼跨平臺了。 真的有跨平臺需求,是從微型計算機開始真的普及開始的。因爲只有計算機普及了,各種平臺都被廣泛採用了,相互又不互相兼容軟件,纔會有軟件跨平臺的需求。微機普及的歷史,比 PC 普及的歷史要早10年,而這段歷史,正好和 UNIX 發展史是並行重疊的。

熟悉 UNIX 發展史的讀者都知道, UNIX 真正普及開來,是因爲其全部都用 C,一個當時絕對能夠稱爲跨平臺的語言重寫了一次。又因爲美國大學和科研機構之間的開源共享文化,C 版本的 UNIX 出生沒多久,就迅速從原始的 PDP-11 實現,移植到了 DEC,Intel 等平臺上,產生了無數衍生版本。隨着跨平臺的 UNIX 的普及, 微型計算機也更多的普及開來,因爲只需要掌握基本的 UNIX 知識,就可以順利操作微型計算機了。所以,微機和 UNIX 這兩樣東西都在 1970年 到 1980 年在美國政府,大學,科研機構,公司,金融機構等各種信息化前沿部門間真正的普及開來了。這些歷史都是人所共知耳熟能詳的。

既然 UNIX 是跨平臺的,那麼,UNIX 上的語言也應當是跨平臺的注: 本節所有的故事都和 Windows 無關,因爲 Windows 本身就不是一個跨平臺的操作系統)。UNIX 上的主打語言 C 的跨平臺性,一般是以各平臺廠商提供編譯器的方式實現的,而最終編譯生成的可執行程序,其實不是跨平臺的。所以,跨平臺是源代碼級別的跨平臺,而不是可執行程序層面的。 而除了標準了 C 語言外,UNIX 上有一派生機勃勃的跨平臺語言,就是腳本語言。(注:腳本語言和普通的編程語言相比,在能完成的任務上並沒有什麼的巨大差異。腳本語言往往是針對特定類型的問題提出的,語法更加簡單,功能更加高層,常常幾百行C語言要做的事情,幾行簡單的腳本就能完成

3. 解釋和執行

腳本語言美妙的地方在於,它們的源代碼本身就是可執行程序,所以在兩個層面上都是跨平臺的。不難看出,腳本語言既要能被直接執行,又要跨平臺的話,就必然要有一個“東西”,橫亙在語言源代碼和平臺之間,往上,在源代碼層面,分析源代碼的語法,結構和邏輯,也就是所謂的“解釋”;往下,要隱藏平臺差異,使得源代碼中的邏輯,能在具體的平臺上以正確的方式執行,也就是所謂的“執行”。

雖說我們知道一定要這麼一個東西,能夠對上“解釋”,對下“執行”,但是 “解釋” 和 “執行” 兩個模塊畢竟是相互獨立的,因此就很自然的會出現兩個流派:把解釋和執行設計到一起把解釋和執行單獨分開來 這樣兩個設計思路,需要讀者注意的是,現在這兩個都是跨平臺的,安全的設計,而在後者中字節碼作爲了解釋和執行之間的溝通橋樑,前者並沒有字節碼作爲橋樑。

4. 解釋和執行在一起的方案

我們先說前者,前者的優點是設計簡單,不需要搞什麼字節碼規範,所以 UNIX 上早期的腳本語言,都是採用前者的設計方法。 我們以 UNIX 上大名鼎鼎的 AWK 和 Perl 兩個腳本語言的解釋器爲例說明。 AWK 和 Perl 都是 UNIX 上極爲常用的,圖靈完全的語言,其中 AWK, 在任何 UNIX 系統中都是作爲標準配置的,甚至入選 IEEE POSIX 標準,是入選 IEEE POSIX 盧浮宮的唯一同類語言品牌,其地位絕對不是 UNIX 下其他腳本語言能夠比的。這兩個語言是怎麼實現解釋和運行的呢? 我從 AWK 的標準實現中摘一段代碼您一看就清楚了:

int main(int argc, char *argv[]) {
  ...
  syminit();
  compile_time = 1;
  yyparse();
  ...
    if (errorflag == 0) {
      compile_time = 0;
      run(winner);
    }
  ...
}

其中, run 的原型是
run(Node *a)   /* execution of parse tree starts here */

winner 的定義是:
Node    *winner ;    /* root of parse tree */

熟悉 Yacc 的讀者應該能夠立即看出, AWK 調用了 Yacc 解析源代碼,生成了一棵語法樹。按照 winner 的定義, winner 是這棵語法樹的根節點。 在“解釋”沒有任何錯誤之後,AWK 就轉入了“執行” (compile_time 變成了 0),將 run 作用到這棵語法樹的根節點上。 不難想像,這個 run 函數的邏輯是遞歸的(事實上也是),在語法樹上,從根依次往下,執行每個節點的子節點,然後收集結果。是的,這就是整個 AWK 的基本邏輯: 對於一段源代碼, 先用解釋器(這裏awk 用了 Yacc 解釋器),生成一棵語法樹,然後,從樹的根節點開始,往下用 run 這個函數,遇山開山,遇水搭橋,一路遞歸下去,最後把整個語法樹遍歷完,程序就執行完畢了。(這裏附送一個小八卦,抽象語法樹這個概念是 LISP 先提出的,因爲 LISP 是最早像 AWK 這樣做的,LISP 實在是屬於開天闢地的作品!)Perl 的源代碼也是類似的邏輯解釋執行的,我就不一一舉例了。

5. 三大缺點

現在我們看看這個方法的優缺點。 優點是顯而易見的,因爲通過抽象語法樹在兩個模塊之間通信,避免了設計複雜的字節碼規範,設計簡單。但是缺點也非常明顯。最核心的缺點就是性能差,需要資源多,具體來說,就是如下三個缺點。

缺點1因爲解釋和運行放在了一起,每次運行都需要經過解釋這個過程。假如我們有一個腳本,寫好了就不修改了,只需要重複的運行,那麼在一般應用下尚可以忍受每次零點幾秒的重複冗餘的解釋過程,在高性能的場合就不能適用了。

缺點2因爲運行是採用遞歸的方式的,效率會比較低。 我們都知道,因爲遞歸涉及到棧操作和狀態保存和恢復等,代價通常比較高,所以能不用遞歸就不用遞歸。在高性能的場合使用遞歸去執行語法樹,不值得。

缺點3,因爲一切程序的起點都是源代碼,而抽象語法樹不能作爲通用的結構在機器之間互傳,所以不得不在所有的機器上都佈置一個解釋+運行的模塊。 在資源充裕的系統上佈置一個這樣的系統沒什麼,可在資源受限的系統上就要慎重了,比如嵌入式系統上。 鑑於有些語言本身語法結構複雜,佈置一個解釋模塊的代價是非常高昂的。本來一個遞歸執行模塊就很吃資源了,再加一個解釋器,嵌入式系統就沒法做了。所以,這種設計在嵌入式系統上是行不通的。

當然,還有一些其他的小缺點,比如有程序員不喜歡開放源代碼,但這種設計中,一切都從源代碼開始,要發佈可執行程序,就等於發佈源代碼,所以不願意公佈源代碼的商業公司很不喜歡這些語言等等。但是上面的三個缺點,是最致命的,這三個缺點,決定了有些場合,就是不能用這種設計。

6. 分開解釋和執行

前面的三個主要缺點,恰好全部被第二個設計所克服了。在第二種設計中, 我們可以只解釋一次語法結構,生成一個結構更加簡單緊湊的字節碼文件。這樣,以後每次要運行腳本的時候, 只需要把字節碼文件送給一個簡單的解釋字節碼的模塊就行了。因爲字節碼比源程序要簡單多了,所以解釋字節碼的模塊比原來解釋源程序的模塊要小很多;同時,脫離了語法樹,我們完全可以用更加高性能的方式設計運行時,避免遞歸遍歷語法樹這種低效的執行方式;同時,在嵌入式系統上,我們可以只部署運行時,不部署編譯器。 這三個解決方案,預示了在運行次數遠大於編譯次數的場合,或在性能要求高的場合,或在嵌入式系統裏,想要跨平臺和安全性,就非得用第二種設計,也就是字節碼+虛擬機的設計

講到了這裏,相信對 Java, 對 PHP 或者對 Tcl 歷史稍微瞭解的讀者都會一拍腦袋頓悟了: 原來這些牛逼的虛擬機都不是天才拍腦袋想出來的,而是被需求和現實給召喚出來的啊!

我們先以 Java 爲例,說說在嵌入式場合的應用。Java 語言原本叫 Oak 語言,最初不是爲桌面和服務器應用開發的,而是爲機頂盒開發的。SUN 最初開發 Java 的唯一目的,就是爲了參加機頂盒項目的競標。嵌入式系統的資源受限程度不必細說了,自然不會允許上面放一個解釋器和一個運行時。所以,不管Java 語言如何,Java 虛擬機設計得直白無比,簡單無比,手機上,智能卡上都能放上一個 Java 運行時(當然是精簡版本的)。 這就是字節碼和虛擬機的威力了。

SUN 無心插柳,等到互聯網興起的時候, Java 正好對繪圖支持非常好,在 Flash 一統江湖之前,憑藉跨平臺性能,以 Applet 的名義一舉走紅。然後,又因爲這種設計先天性的能克服性能問題,在性能上大作文章,憑藉 JIT 技術,充分發揮上面說到的優點2,再加上安全性,一舉拿下了企業服務器市場的半壁江山,這都是後話了。

再說 PHP。PHP 的歷史就包含了從第一種設計轉化到第二種設計以用來優化運行時性能的歷史。 PHP 是一般用來生成服務器網頁的腳本語言。一個大站點上的PHP腳本, 一旦寫好了,每天能訪問千百萬次,所以,如果全靠每次都解釋,每次都遞歸執行,性能上是必然要打折扣的。 所以,從 1999年的 PHP4 開始, Zend 引擎就橫空出世,專門管加速解釋後的 PHP 腳本, 而對應的 PHP 解釋引擎,就開始將 PHP 解釋成字節碼,以支持這種一次解釋,多次運行的框架。 在此之前, PHP 和 Perl, 還有 cgi, 還算平分秋色的樣子,基本上服務器上三類網頁的數量都差不多,三者語法也很類似,但是到了 PHP4 出現之後,其他兩個基於第一種設計方案的頁面就慢慢消逝了, 全部讓位給 PHP。 你讀的我的這個 Wordpress 博客,也是基於 PHP 技術的,底層也是 Zend 引擎的。 著名的 LAMP 裏面的那個 P, 原始上也是 PHP,而這個詞真的火起來,也是 99年 PHP4 出現之後的事情。

第二種設計的優點正好滿足了實際需求的事情,其實不勝枚舉。比如說 在 Lua 和 Tcl 等宿主語言上也都表現的淋漓盡致。像這樣的小型語言,本來就是讓運行時爲了嵌入其他語言的,所以運行時越小越好,自然的,就走了和嵌入式系統一樣的設計道路。

7. 結語

其實第二種設計也不是鐵板一塊,裏面也有很多流派,各派有很多優缺點,也有很多細緻的考量,下一節,如果不出意外,我將介紹我最喜歡的一個內容: 下一代虛擬機:寄存器還是棧。

說了這麼多,最後就是一句話,有時候我們看上去覺得一種設計好像是天外飛仙,橫空出世,其實其後都有現實,需求等等的諸多考量虛擬機技術就是這樣,在各種需求的引導下,逐漸的演化成了現在的樣子。

==============================

F. 高級語言怎麼來的-3 

FORTRAN 語言是怎麼來的

在高級語言是怎麼來的子系列的第一篇中, 我們結合當時硬件的特點,分析了 FORTRAN 爲什麼一開始不支持遞歸。但是 FORTRAN 本身是怎麼來的這個問題其實還是沒有得到正面回答,本節我們就談談 FORTRAN 語言本身是怎麼來的。

其實,FORTRAN 語言也是現實驅動的。 所以我們還是回到當時,看看當時程序員的需求和軟硬件條件,看看 FORTRAN 是怎麼來的。 瞭解歷史的另一個好處是, 因爲 FORTRAN 的發展歷史正好和高級語言的發展歷史高度重合,所以瞭解 FORTRAN 的背景,對於理解其他高級語言的產生都是大有幫助的。

1. 困難的浮點計算
我們先從硬件的角度說起。 大致從 1946 年第一臺計算機誕生,到 1953 年,計算機一直都缺少兩件非常重要的功能,一個叫浮點計算,一個叫數組下標尋址,這兩個功能的缺失直接導致了高級語言的興起。 我們依次單個分析。讀者對浮點計算應該都不陌生,用通俗的話說就是如 0.98×12.6 這樣的實數乘法,或者  0.98 + 12.6 這樣的實數加法的運算。用行話說,就是用計算機進行大範圍高精度數的算術運算。

學過二進制的同學都知道,二進制整數之間的乘法和加法等運算都是相對簡單的,和正常的十進制運算是一樣的,只是把加法和乘法這些基本操作用更加簡單的邏輯或(OR) 和 邏輯與 (AND) 實現而已,在電子電路上也很好實現。 因此,就是世界上最早的電子計算機,ENIAC,也是支持整數的乘法加法等算術操作的。

可是浮點運算就不一樣了。 因爲一個額外的小數點的引入,在任何時候都要注意小數點的對齊。 如果用定點計數,則計數的範圍受到限制,不能表示非常大或者非常小的數。所以,浮點數一般都是用科學記數法表示的,比如 IEEE 754 標準。(不熟悉 IEEE 754 的讀者也可以想像一下如何設計一套高效的存儲和操作浮點數的規範和標準,以及浮點算法), 科學記數法表示的浮點數的加減法每次都要對齊小數點,乘除法爲了保持精度,在設計算法上也有很多技巧,所以說,相比較於整數的運算和邏輯運算,浮點運算是一件複雜的事情。落實到硬件上就是說,在硬件上設計一個浮點運算,需要複雜的電路和大量的電子元器件。在早期電子管計算機中,是很少能做到這麼大的集成度的。因此,不支持浮點也是自然的設計取捨。在計算機上放一個浮點模塊這個想法,需要等電子工業繼續發展,使得電子管體積小一點,功耗低一點後,才能進入實踐。

2. 關於浮點計算的一些八卦

關於浮點,這裏順帶八卦一點浮點計算的事情。在計算機芯片設計中,浮點計算一直是一個讓硬件工程師頭疼的事情,即使到了386時代,386 處理器 (CPU)的浮點乘法也是用軟件模擬的,如果想用硬件做浮點乘法,需要額外購買一塊 80387 浮點協處理器 FPU,否則就在 386 上做軟件的模擬。這樣做的原因在一塊硅片上刻蝕一個 CPU 和一個FPU 需要的集成度還是太高,當時的工藝根本沒法實現。真的把 FPU 和 CPU 融在一起刻蝕到一塊硅片上,已經是 1989 年的事情了。當時,Intel 把融合了 80386 和 80387 的芯片改了改,起了個名字叫 80486,推向了市場。帶着浮點的處理器的普及,使得個人計算機能做的事情變多了。極度依賴於浮點計算的多媒體計算機(視頻和聲音等多媒體的壓縮,轉換和回放都是要依賴於浮點運算的),也正好隨着 80486 的流行,逐漸普及開來。

在處理器上融合浮點運算依然是困難的。即使到今天,很多低端的處理器,都不帶有浮點處理器。 所以,號稱能夠上天入地的,被移植到很多低端設備比如手機上的 Linux 內核,必然是不能支持浮點運算的,因爲這樣會破壞內核的可移植性。我們都知道, 在內核模式下,爲了保證內核操作的原子性,一般在內核從事關鍵任務的時候所有中斷是要被屏蔽的,用通俗的話說就是內核在做事情的時候,其他任何人不得打 擾。 如果內核支持浮點運算,不管是硬件實現也好,軟件模擬也罷, 如果允許在內核中進行像浮點計算這樣複雜而耗時的操作,整個系統的性能和實時響應能力會急劇下降。  即使是在硬件上實現的浮點運算,也不是件容易的事情,會耗費CPU較多的時鐘週期,比如 Pentium 上的浮點數除法,需要耗費 39 個時鐘週期才行,在流水線設計的CPU中,這種佔用多個時鐘週期的浮點運算會讓整個流水線暫停,讓CPU的吞吐量下降。在現代 CPU 設計中,工程師們發明了超標量,亂序執行,SIMD 等多種方式來克服流水線被浮點運算這種長週期指令堵塞的問題,這都是後話了。

正因爲對於計算機來說,浮點運算是一個挑戰性的操作,但又是做科學計算所需要的基本操作,所以浮點計算能力就成了計算機能力的一個測試標準。我們常常聽說有一個世界上前 500 臺最快的超級計算機列表,這裏所謂的“快”的衡量標準,就是以每秒鐘進行多少次浮點計算(FLOPS) 爲準。按照 Top500.org, 即評選世界上前 500 臺超級計算機的機構 2009年6月的數據,世界上最快的計算機,部署在美國能源部位於新墨西哥的洛斯阿拉莫斯國家實驗室 (Los Alamos National Laboratory),當年造出第一顆原子彈的實驗室。這臺超級計算機,浮點計算速度的峯值高達 1456 TFlops,主要用來模擬核試驗。因爲美國的所有核彈頭,海軍核動力航母中的反應堆以及核試驗,都由能源部國家核安全署(NNSA) 管理,所以能源部一直在投資用超級計算機進行核試驗。 在 1996 年美國宣佈不再進行大規模的物理核試驗後的這麼多年,美國能源部一直用超級計算機來做核試驗,所以在 Top500 列表中,美國能源部擁有最多數量的超級計算機。

3. 數組下標尋址之障

言歸正傳,我們剛纔說了在早期計算機發展史上,浮點計算的困難。除了浮點計算,還有一件事情特別困難,叫做數組下標尋址。用現代通俗的話說,就是當年的計算機,不直接支持 A[3] 這樣的數組索引操作,即使這個操作從邏輯上說很簡單:把數組 A 的地址加上 3,就得到了 A[3] 的地址,然後去訪問這個地址。

這個困難在今天的程序員看來是不可思議的。 爲什麼這麼簡單的數組下標尋址機制最一開始的計算機沒有支持呢? 原來,當年的計算機內存很小,只有一千到兩千的存儲空間,所以,描述地址只需要幾位二/十進制數(BCD)。從而,在每條指令後面直接加一個物理地址是可行且高效的尋址方式。這種尋址方式,叫做直接尋址,當時所有的機器,都只支持直接尋址,因爲在機器碼中直接指出操作數的準確地址是最簡單直接的方法,計算機不需要任何複雜的地址解碼電路。但壞處是,這個設計太不靈活了,比如說 A[3] 這個例子,就沒法用直接尋址來表示。

一般情況下,如果知道數組A, 對於 A[3] 這樣的例子,用直接尋址問題去模擬間接尋址的困難還不是很大,只要程序員事先記住數組 A 的地址然後手工加上 3 就行了 (A也是程序員分配的,因爲當時沒有操作系統,所以程序員手工管理內存的一切)。可是,也有一些情況這樣直接尋址是不行的。比如說,當時計算機已經能支持跳轉和判斷指令了,也就是說,可以寫循環語句了。我們可以很容易看到, 以 i 爲循環變量的循環體內,對 A[i] 的訪問是不能寫成一個靜態的直接尋址的,因爲 i 一直在變化,所以不可能事先一勞永逸的定好 A[i] 的所在位置,然後靜態寫在程序中。

這樣,即使寫一個簡單的 10×10 矩陣的乘法,程序員就不得不死寫 10的三次方即1000 行地址訪問,而沒辦法用幾行循環代替。當時的一些聰明人,也想了一些方法去克服這個問題,比如說,他們先取出 A 的地址,然後做一次加法,把結果,也就是當時 A[i] 的地址,注射到一個讀內存的 LOAD 指令後面。然後執行那條 LOAD 指令。比如我要讀 A[i],我先看,A的地址是 600,再看看 i 是3, 就加上 i,變成603,然後,把後面的指令改成 LOAD 603, 這樣,就可以讀到 A[i]。這個小技巧之所以可行,要感謝馮諾依曼爺爺的體系設計。在馮諾依曼計算機中,數據和程序是混在一起不加區分的,所以程序員可以隨時像修改數據一樣修改將要運行的下一條程序指令。就這樣,靠着這個小技巧, 好歹程序員再也不要用1000行代碼表示一個矩陣乘法了。

4. SpeedCoding 的出現

計算機本來就是用來做數學計算的,可是科學計算裏面最最基本的兩個要素–浮點計算和數組下標訪問,在當時的計算機上都缺少支持。這種需求和實際的巨大落差,必然會召喚出一箇中間層來消弭這種落差。 其實計算機科學的一般規律就是這樣:當 A 和 C 相差巨大的時候,我們就引入一箇中間層 B,用 B 來彌合 A 和 C 之間的不兼容。 當年的這個中間層,就叫做 SpeedCoding,由 IBM 的工程師 John Backus 開發。

SpeedCoding,顧名思義,就是讓程序員編程更快。它其實是一個簡單,運行在 IBM 701 計算機上的解釋器。它允許程序員直接寫浮點計算和下標尋址的指令,並且在底層把這些 “僞指令” 翻譯成對應的機器碼,用軟件模擬浮點計算,自動修改地址等等。這樣,程序員就可以從沒完沒了的手工實現浮點運算和下標尋址實現中解放出來,快速的編程。這個 SpeedCoding,這可以算得上是 FORTRAN 的種子了。

雖然這個解釋器超級慢,程序員用這個解釋器也用得很爽,也不感到它非常慢。 這是因爲當年計算機浮點計算都繞不過軟件模擬,即使最好的程序員用機器碼而不用這個解釋器,寫出來的程序,也不比這個解釋器下運行快多少。另一個更加重要的原因是,這個解釋器極大的減少了程序員 debug 和 code 的時間。隨着計算機速度的提高,當年一個程序耗費的計算成本和程序員編程耗費的人力成本基本上已經持平了,所以,相比較於寫更加底層的機器碼,用了 SpeedCoding 的程序員的程序雖然慢點,但人力成本瞬間降成 0,總體下來,用 SpeedCoding 比起不用來,總體成本還要低不少。

好景不長,因爲客戶一直的要求和電子工業的發展,IBM 在 1954 年,終於發佈了劃時代的 704 計算機,很多經典的語言和程序,都首次在 704 上完成了。比如之前我們在本系列的D篇中提到的 Steve Russell 的 LISP 解釋器,就是在 704 上完成的。 704 計算機一下子支持了浮點計算和間接下標尋址。 這下用 SpeedCoding 的人沒優勢了,因爲機器碼支持浮點和下標尋址之後,寫機器碼比寫 SpeedCoding 複雜不了多少,但是速度快了很多倍,因爲 SpeedCoding 解釋器太慢了,以前因爲浮點和解釋器一樣慢,所以大家不在意它慢,現在浮點和尋址快了,就剩下解釋器慢,寫機器碼的反而佔了上風,程序員也就不用 SpeedCoding 了。

5. FORTRAN 創世紀

在 704 出來之前,做 SpeedCoding 的 John Backus 就認識到,要想讓大家用他的 SpeedCoding, 或者說,想要從軟件工具上入手,減少程序的開發成本,只有兩個方法:

1. 程序員可以方便的寫數學公式 

2. 這個系統最後能夠解析/生成足夠的快的程序。

他認爲,只有達到了這兩點,程序員纔會樂意使用高級的像 SpeedCoding 這樣的工具,而不是隨着硬件的發展在機器碼和 SpeedCoding 這樣的工具之間跳來跳去。他本人通過實現 SpeedCoding, 也認識到如果有一個比機器碼高級的語言, 生產效率會高很多倍。那麼,現在唯一的問題就是實現它,當然,這就不是一個小項目了,就需要 IBM 來支持他的開發了。 所以,在 1953年,他把他的想法寫成了一個文檔,送給了 IBM 的經理。項目在 1954 年, 704 發佈的當年,終於啓動。John Backus 領導的設計一個能達到上面兩點的編程系統的項目的成果,就是日後的 FORTRAN。

和現在大多數編程語言不一樣,FORTRAN 語言的設計的主要問題不是語法和功能,而是編譯器怎麼寫才能高性能。John Backus 日後回憶說,當時誰也沒把精力放在語言細節上,語言設計很潦草的就完成了(所以其後正式發佈後又經過了N多修訂),他們所有的功夫都是花在怎麼寫一個高性能的編譯器上。這個高性能的編譯器很難寫,到 1957 年才寫好,總共花了 IBM 216 個人月。等到 FORTRAN 一推出,不到一年的時間,在 IBM 總共售出的 60 臺 704上,就部署了超過一半。現在沒啥編程語言能夠這麼牛的攻城掠地了 :)

6. 結語

放到歷史的上下文中看,FORTRAN 的出現是很自然的。一方面,複雜的數學運算使得一個能夠表述數學計算的高級語言成爲必須,計算機的發展也爲這個需求提供的硬件條件;另一方面,隨着計算機的發展,程序員的時間成本一直不變,但是計算的成本一直在降低,用高級語言和用機器碼在性能上的些許差異變得可以忽略。這樣的歷史現實,必然會召喚出以少量的增加計算機工作量爲代價,但能大幅度降低程序員時間成本的新的工具和設計。

這種新的工具,新的設計,又對程序設計產生革命性的影響。在整個編程發展的歷史上,FORTRAN 和其他高級語言的出現可以說是第一波的革命;而後, UNIX和C語言的興盛,使得系統編程的效率得到革命性提升,可以算是第二波革命;而面向對象方法,使得複雜的 GUI 等系統的編程效率得到提升,應該算得上是第三波革命。到如今,現在各種各樣的方法論就更加多了,且看以後回看,哪種方法和工具能夠大浪淘沙留下來。

=============================

G. 程序員心底的小聲音 

程序員心底的小聲音

編程大約有三個境界,新手,高手,和高不成低不就的中手。這三個境界,大致和王國維先生劃定的做學問的三個境界一一對應。 一般來說,如果不經過幾十萬行的代碼的錘鍊(衣帶漸寬終不悔,爲伊消得人憔悴),或者長期在一個高手團隊裏面打磨切磋,那麼無論怎麼樣的理論熟悉,打字熟練,考試全A,編程起來,都應該算是中手。一箇中手如果機緣很好,得到高人親自指點,則能很快成長爲高手,如果沒有這樣的機緣,那就要在“衆裏尋她千百度”這個層次苦苦的求索錘鍊很久,才能“驀然回首”。

讀書是一種很好彌補沒有高手在場的方法,都說書是最好的老師嘛。 可是現實是,高手寫給中手的書很少。 在任何行業,適合新手的入門的書很多,適合中手的書就很少。 原因有兩個,一來高手極少願意耐心的的指點成長祕訣,就算寫了,也是蜻蜓點水,因爲這些經驗啊結論啊,都被他們本身提煉成了珠璣,他們覺得最重要的也就是這麼寥寥幾句,也沒有太多的廢話好寫。 而讀者如果沒有類似的經歷,則看到這些珠璣,只是覺得把玩頗爲有趣而已,極少能有同感。 鮮有高手,能把技術書寫成散文集,如 Brooks 一樣,在《人月神話》中把經驗教訓和經歷背景等一一道來,並且從這些經歷中抽出一般性的知識。 所以,高手的風格一般是浮光掠影概括一下大致自己領會到的幾個原則和教訓。 這些寥寥數語的珠璣,對於其他高手來說一看就懂,但是對於中手來說就很難以理解。 所以很多高手寫出來的給中手看的書就曲高和寡。 二來,中手其實水平差異巨大,偏好也各不一樣,有的或許根本認識不到自己應該走的成長軌跡,有的認爲這些書籍是片面知識,所以把不喜歡的書都給扔垃圾堆了,光撿自己喜歡的書看;有的未必看得上高手的經驗,認爲高手說的那些自己也早就領悟到了。所以,也不喜歡購買這些書籍。這兩個原因,就造成了高手提攜中手的書在市場上很少見到。

不過這樣的書倒不是沒有,比方說在編程領域, 我至少可以推薦四本這類的書,這四本分別是
《Pragmatic Programmer》, 《The Art of UNIX Programming》, 《Elements of Programming Style》 和 《The Productive Programmer》. 這四本書,都是高手所寫,都屬於高手指導中手的典範。第二第三本我原先都介紹過。 而第四本餘晟同學的書評比我寫得好幾百倍,所以我就以 《Pragmatic Programmer》 爲例說明這個問題。

我們前面說了,對於中手,特別是在“尋她千百度”這個層次的中手來說,或許本身已經撿到了一些珠璣,或許對於像 《Pragmatic Programmer》 裏面說的那些 Tip,有的是深有同感的。 比如 DRY (Don’t Repeat Yourself 不要重複你自己), 基本上大家都知道,可是在實際中(至少我自己)還是不停的一次一次的犯錯誤,做事情不符合 DRY 原則(一次一次犯這個錯誤本身也是一個 DRY 錯誤, 因爲 DRY 原則要求你對於每種錯誤你只能犯一次)。 讀到的時候深有同感, 寫代碼的時候卻忘到 Java 國去了,這還真不是個案,是非常普遍的現象。

能不能讓正確的原則指揮正確的行動本身,其實就是區分是否是高手的一個顯著標志。 試想,兩個都瞭解 KISS 原則的程序員在一起寫代碼,高手的代碼必然是自然流露出 KISS 的優雅,而中手或許需要旁人提醒和多次重構,才能達到理想的狀態。 出現這個問題的原因很明顯–中手沒有完全內化 KISS 原則,所以尚且不能“運用自如”。 內化是一個非常複雜的認知過程,本身涉及到大腦中 mind set 和 paradigm 的切換, 所以必然不是一個簡單的隔夜就能完成的過程,這也就是爲啥能夠“消得人憔悴”,但是切換一旦完成,實踐中就會自然流露出這種新的認識,也就是到了一個新的境界,發現燈火闌珊處。

那麼原則和知識的內化這個過程怎麼能夠加速呢?也就是說,怎麼較快的到達高手境界呢? 可以肯定的說,光靠對自己說我“下次一定按照這個原則這樣做”是不行的。認知科學認爲,頻繁的高強度的外部刺激和自主的有意識的反覆提醒是加速內化的兩個重要方法。 第一個方法需要外部環境的支撐。 試想,如果一個程序員不是天天和複雜文本處理打交道,他必然沒有足夠外部刺激來熟悉和內化正則表達式; 如果一個程序員不是天天和極度複雜的大項目打交道,用全自動編譯環境和自動單元測試也顯得無甚必要,所以,除非你正好掉進了一個天天有高強度訓練的環境,否則全靠第一點是不可能的。 尤其是自學一門語言和一門技術的程序員,往往在沒有高強度訓練之前就拿着這些技能投入工作了,因此想成爲某方面的高手,只能採取第二條路,就是有意識的強化實踐和反覆提醒。

《聖經》裏有個故事,說一個人在沙漠裏,信心喪失的時候,突然聽到 “A Still Small Voice” (平靜的小聲音), 即上帝的啓示。這個平靜的小聲音把他從絕望中拉了回來。 其實對於這個人來說,他本身的實踐能力在 “平靜的小聲音” 出現前後並沒有多大的改變,唯一的不同就是他知道該怎麼做了。

內化一個知識或者認識的時候所循的路徑也是一樣的。 我們常常會“忘了”應該怎麼正確的做一件事情(這個地方的“忘了”,指我們之前從書中或者其他渠道讀到看到了正確的原則或方法,但是在那一刻腦子裏壓根沒考慮這個原則或方法,因爲這個原則或方法壓根沒有親自實踐過,所以根本不是自己的一部分,不屬於自己)。 在這個時候, 如果突然有一個平靜的小聲音跳出來,說,“嘿,你是不是該遵循這個原則,用這個方法?” 無需說,我們對問題的思考就能頓時全面起來, 也會更加深刻的理解原先讀到看到的不屬於自己的原則和方法。當然,我們更加感興趣的是,如何能夠在身邊沒有高手和上帝發出這樣的平靜的小聲音的時候,自己發出這樣的小聲音?

怎麼靠自己呢,記得魯迅小朋友破壞公物在課桌上刻的“早”麼?是的,我們需要抽象出一些簡單的詞句和規則,靠記憶和不斷的提醒,小規模的內化這些小聲音,讓這些簡單的小聲音能夠時刻從大腦裏跳到耳邊,提醒自己。 具體來說,在閱讀上面的幾本書,尤其是閱讀 《Pragmatic Programmer》 的時候,如果僅僅是以普通的瀏覽的方式閱讀,就會很簡單的陷入 “啊,這個我知道了,啊,那個我瞭解了,恩,這個以後要注意” 的套路中。 這樣的閱讀方式,只會強化原有的自己已經知道的部分,而不大可能把“以後要注意” 這部分全部內化。所以,自負的讀者讀完了之後必然覺得“哈哈,高手不過如此,大部分我也知道嘛”,而不是“是的,我還有不少要注意”。 這兩個態度,就把高手和易於滿足的中手永恆的隔開了。 我覺得,想要內化這些小聲音,還是要靠實踐,如果不實踐,即使你把這些小聲音寫在 100 塊錢的高檔筆記本上也沒有用。我個人覺得,理想的閱讀狀態應該是先大致理解和記住裏面的 Tip, 然後每週爭取實踐 2-3 個 Tip。其實如此做完一圈也就是半年,在這一圈之後就會記住所有的 Tip 的內容,這時候,小聲音就成了自己的一部分了。然後在剩下的幾年裏,只要時時有這些小聲音挑出來,告訴你,“要自動頻繁的測試”,或者“別手動做繁瑣的工作”,你會很快的被強迫轉換到高效而優雅的工作狀態。 到了那個時候,這些小聲音就再也不會跳出來了,因爲你早就自然的遵守這些小聲音的要求了。

《Pragmatic Programmer》 和 《The Elements of Programming Style》 這些書裏面的 Tip 都不是來自上帝的話語,卻都是值得隨聲帶着的小聲音。 其實只要是處理過實際問題,編過幾萬行程序,大多程序員都差不多都會有或深刻或淺顯的對各個 Tip 都感悟,而且我相信或許對有些 Tip 的認識能比原書的作者還要深刻,這是很正常的。 事實上每一個 Tip 只是一句話而已,對這一句話的理解層次, 則完全不這一句話能夠覆蓋的。 比如說,一天寫了兩個 Hello Word 的程序員也會領悟到 DRY, 一個剛剛重構扔掉掉幾千行重複代碼的程序員也領悟到 DRY, 而這兩個 DRY 所在的認識層面, 必然是不一樣的。 再好比說我在“編程珠璣番外篇”這個系列裏面寫的有些文字,看上去很有道理,但我本人對這些文字的認識可能比我的讀者要淺, 但是這倒不妨礙引發讀者思考。 即使有些牛人覺得上面這幾本書的作者在某些原則上的認識不夠深刻,或者覺得作者只是在羅列一些小碎片,讀這些書,特別是 《Pragmatic Programmer》 這本書的那些小 Tip,依然是有益的, 因爲他或許能觸發你高於作者的思考,然後在你腦中形成更加圓潤的珠璣。而對於像我這樣屬於中手下游平時又沒有大項目訓練的人,《Pragmatic Programmer》 這本書,和其他的幾本書一起, 實在是很好的“小聲音彙編”。

============================

G. 高級語言怎麼來的-4 

LISP 語言是怎麼來的–LISP 和 AI 的青梅竹馬 A

LISP 語言的歷史和一些番外的八卦和有趣的逸事,其實值得花一本書講。 我打算用三篇文章扼要的介紹一下 LISP 的早期歷史。 講 LISP, 躲不過要講 AI (人工智能)的,所以乾脆我就先八卦八卦他們的青梅竹馬好了。

翻開任何一本介紹各種編程語言的書,都會毫無驚奇的發現,每每說到 LISP, 通常的話就是”LISP 是適合人工智能(AI)的語言”。 我不知道讀者讀到這句話的時候是怎麼理解的,但是我剛上大學的時候,自以爲懂了一點 LISP 和一點人工智能的時候, 猛然看到這句話, 打死我也不覺得”適合”。 即使後來我看了 SICP 很多遍, 也難以想象爲什麼它就 “適合” 了, 難道 LISP 真的能做 C 不能做的事情麼? 難道僅僅是因爲 John McCarthy 這樣的神人既是 AI 之父, 又是 LISP 之父, 所以 AI 和 LISP 兄妹兩個就一定是很般配? 計算機科學家又不是上帝,創造個亞當夏娃讓他們沒事很般配幹啥? 既然本是同根生這樣的說法是不能讓人信服的, 那麼到底這句話的依據在哪裏呢? 我也是後來看 AI 文獻, 看當年的人工智能的研究情況,再結合當年人工智能研究的指導思想, 當年的研究者可用的語言等歷史背景,才完全理解“適合” 這兩個自的。 所以,這篇既是八卦,也是我的心得筆記。我們一起穿越到 LISP 和 AI 的童年時代。 雖然他們現在看上去沒什麼緊密聯繫, 他們小時候真的是青梅竹馬的親密玩伴呢!

讓機器擁有智能, 是人長久的夢想, 因爲這樣機器就可以聰明的替代人類完成一些任務。 二戰中高速電子計算機的出現使得這個夢想更加近了一步。二戰後,計算機也不被完全軍用了,精英科學家也不要繼續製造原子彈了,所以, 一下子既有資源也有大腦來研究 “智能機器”這種神奇的東西了。 我們可以隨便舉出當年研究繁盛的例子: 維納在 1948 年發表了<控制論>, 副標題叫做 <動物和機器的控制和通信>,  其中講了生物和機器的反饋,講了腦的行爲。 創立信息論的大師香農在 1949 年提出了可以下棋的機器,也就是面向特定領域的智能機器。同時,1949年, 加拿大著名的神經科學家 Donald Hebb 發表了“行爲的組織”,開創了神經網絡的研究;  圖靈在 1950 年發表了著名的題爲“計算的機器和智能”的文章,提出了著名的圖靈測試。如此多的學科被創立,如此多的學科創始人在關心智能機器, 可見當時的確是這方面研究的黃金時期。

二戰結束十年後, 也就是 1956 年, 研究智能機器的這些研究者, 都隱隱覺得自己研究的東西是一個新玩意,雖然和數學,生物,電子都有關係, 但和傳統的數學,生物,電子或者腦科學都不一樣, 因此,另立一個新招牌成了一個必然的趨勢。John McCarthy 同學就趁着 1956 年的這個暑假, 在 Dortmouth 大學(當年也是美國計算機科學發展的聖地之一, 比如說, 它是 BASIC 語言發源地), 和香農,Minsky 等其他人(這幫人當年還都是年輕人),一起開了個會, 提出了一個很酷的詞, 叫做 Artificial Intelligence, 算是人工智能這個學科正式成立。  因爲 AI 是研究智能的機器, 學科一成立, 就必然有兩個重要的問題要回答, 一是你怎麼表示這個世界,二是計算機怎麼能基於這個世界的知識得出智能。 第一點用行話說就是”知識表示”的模型, 第二點用行話說就是“智能的計算模型”。 別看這兩個問題的不起眼, 就因爲當時的研究者對兩個看上去很細微的問題的回答, 直接造就了 LISP 和 AI 的一段情緣。

我們各表一支。 先說怎麼表示知識的問題。 AI 研究和普通的編程不一樣的地方在於, AI 的輸入數據通常非常多樣化,而且沒有固定格式。 比如一道要求解的數學題,一段要翻譯成中文的英文,一個待解的 sodoku 謎題,或者一個待識別的人臉圖片。 所有的這些, 都需要先通過一個叫做“知識表示”的學科,表達成計算機能夠處理的數據格式。自然,計算機科學家想用一種統一的數據格式表示需要處理多種多樣的現實對象, 這樣, 就自然的要求設計一個強大的,靈活的數據格式。 這個數據格式,就是鏈表。

這裏我就不自量力的憑我有限的學識, 追尋一下爲啥鏈表正好成爲理想的數據結構的邏輯線。我想,讀過 SICP 的讀者應該對鏈表的靈活性深有感觸。爲了分析鏈表的長處,我們不妨把他和同時代的其他數據結構來做一比較。 如我在前面的一個系列所說,當時的數據結構很有限,所以我們不妨比較一下鏈表和同時代的另一個最廣泛使用的數據結構-數組-的優劣。 我們都知道,數組和鏈表都是線性數據結構,兩者各有千秋,而 FORTRAN 基本上是圍繞數組建立的,LISP 則是圍繞鏈表實現的。通過研究下棋,幾何題等 AI 問題的表示,我們的讀者不難發現, AI 研究關心於符號和邏輯計算遠大於數值計算,比如下棋,就很難抽象成一個基於純數字的計算問題。 這樣,只能存數字的數組就顯得不適合。 當然我們可以把數組擴展一下,讓這些數組元素也可以存符號。不過即使這樣,數組也不能做到存儲不同結構的數據。 比方說棋類中,車馬炮各有各自的規則,存儲這些規則需要的結構和單元大小都不一樣,所以我們需要一個存儲異構數據單元的模塊,而不是讓每個單元格的結構一樣。 加上在AI 中,一些數據需要隨時增加和修改的。 比如國際象棋裏,兵第一步能走兩步,到底部又能變成皇后等等,這就需要兵的規則能夠隨時修改,增加,刪除和改變。 其他問題也有類似的要求,所有的這些,都需要放開數組維度大小一樣的約束,允許動態增加和減少某一維度的大小,或者動態高效的增加刪除數組元素。 而一旦放開了單元格要同構和能隨時增加和刪除這樣兩個約束,數組其實就不再是數組了,因爲隨機訪問的特性基本上就丟失了,數組就自然的變成了鏈表,要用鏈表的實現。

所以,用鏈表而不是數組來作爲人工智能的統一的數據結構,固然有天才的靈機一動,也有現實需求的影響。當然,值得一提的是,在 Common LISP 這樣一個更加面向實踐而不是科學研究是 LISP 版本中,數組又作爲鏈表的補充,成了基本的數據結構,而 Common LISP,也就能做圖像處理等和矩陣打交道的事情。這個事實更加說明,用什麼樣的數據結構作爲基本單元,都是由現實需求推動的。

當然,科學家光證明了列表能表示這些現實世界的問題還不夠, 還要能證明或者驗證額外的兩點才行, 第一點是列表表示能夠充分的表示所有的人工智能問題,即列表結構的充分性。 只有證明了這一點,我們纔敢放心大膽的用鏈表,而不會擔心突然跳出一個問題鏈表表達不了;第二是人工智能的確能夠通過對列表的某種處理方法獲得,而不會擔心突然跳出一個人工智能問題,用現有的對鏈表的處理方法根本沒法實現。只有這兩個問題的回答是肯定的時候,列表處理纔會成爲人工智能的一部分。

對於這兩個問題,其實都並沒有一個確定的答案,而只是科學家的猜想,或者說是一種大家普遍接受的研究範式(paradigm)。 在 1976 年, 當年構想 IPL, 也就是 LISP 前身的兩位神人 Alan Newell 和 Herbert Simon ,終於以回憶歷史的方式寫了一篇文章。 在這篇文章中,他們哲學般的把當時的這個範式概括爲: 一個物理符號系統對於一般智能行爲是既充分又必要的( A physical symbol system has the necessary and sufficient means for general intelligence action)。 用大白話說就是,“智能必須依賴於某種符號演算系統,且基於符號演算系統也能夠衍生出智能”。 在實踐中,如果你承認這個猜想,或者說這個範式,那你就承認了可以用符號演算來實現 AI。 於是,這個猜想就讓當時幾乎所有的研究者,把寶押在了實現一個通用的符號演算系統上,因爲假如我們製造出一個通用的基於符號演算的系統,我們就能用這個系統實現智能。

上面我們說過, 鏈表的強大的表達能力對於這個符號演算系統來講是綽綽有餘的了,所以我們只要關心如何實現符號演算,因爲假如上面的猜想是對的,且鏈表已經能夠表示所有的符號, 那麼我們的全部問題就變成了如何去構建這樣的符號演算系統。後面我們可以看到, LISP 通過函數式編程來完成了這些演算規則的構建。

這裏,需要提請讀者注意的是, LISP 的全稱是 LISt Processing, 即列表處理,但實際上 LISP 是由兩種互相正交的哲學組合形成的, 一個是列表處理,另一個是函數式編程。 雖然在下面以後,我們會介紹 S-Expression 這樣美妙的把兩者無縫結合在一起的形式,但是爲了清晰我們的概念,我要強調一下列表處理和函數式編程是兩個正交的部分。實際上,我們完全可以用其他的不是函數的方式構建一個列表處理語言。在歷史上,早在 FORTRAN 出現之前,Alan Newell 和 Herbert Simon 就用匯編實現了一個叫 IPL 的語言,而這個 IPL 語言就是面向過程的對列表處理的,而後,McCarthy 一開始也是用一系列的 FORTRAN 子程序來做列表處理的。比如 LISP 裏面的 CAR 操作,其全成實際上是 Content of the Address portion of the Register, 顧名思義,寄存器的地址單元內容,也即列表的第一個元素(和C表達數組的方式類似,這裏寄存器中存着指向列表第一個元素的指針)。 函數式的卻不以列表爲基本數據單元的語言也很多,比如 Scala ,就是以對象爲基本數據單元。 因此,函數式和列表處理是不一定要互相耦合的。 那麼,到底是什麼原因使得 LISP 選擇函數式,這樣的選擇又爲啥更加適合當時 AI 的研究呢, 我們下節將繼續介紹當時 AI 的研究範式,強弱 AI 之間的辯論和函數式編程在當年 AI 研究上的優點。

(待續)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章