使用和研究過這么多程序語言之后,我覺得幾乎不包含多余功能的語言纵搁,只有一個(gè):Scheme岳悟。所以我覺得它是學(xué)習(xí)程序設(shè)計(jì)最好的入手點(diǎn)和進(jìn)階工具队询。當(dāng)然 Scheme 也有少數(shù)的問題派桩,而且缺少一些我想要的功能,但這些都瑕不掩瑜蚌斩。在用了很多其它的語言之后铆惑,我覺得 Scheme 真的是非常優(yōu)美的語言。
要想指出 Scheme 所有的優(yōu)點(diǎn),并且跟其它語言比較员魏,恐怕要寫一本書才講的清楚丑蛤。所以在這篇文章里,我只提其中一個(gè)最簡單撕阎,卻又幾乎被所有人忽視的方面:語法受裹。
其它的 Lisp “方言”也有跟 Scheme 類似的語法(都是基于“S表達(dá)式”),所以在這篇(僅限這篇)文章里我所指出的“Scheme 的優(yōu)點(diǎn)”虏束,其實(shí)也可以作用于其它的 Lisp 方言棉饶。從現(xiàn)在開始,“Scheme”和“Lisp”這兩個(gè)詞基本上含義相同镇匀。
我覺得 Scheme (Lisp) 的基于“S表達(dá)式”(S-expression)的語法照藻,是世界上最完美的設(shè)計(jì)。其實(shí)我希望它能更簡單一點(diǎn)汗侵,但是在現(xiàn)存的語言中岩梳,我沒有找到第二種能與它比美。也許在讀過這篇文章之后晃择,你會(huì)發(fā)現(xiàn)這種語法設(shè)計(jì)的合理性,已經(jīng)接近理論允許的最大值也物。
為什么我喜歡這樣一個(gè)“全是括號宫屠,前綴表達(dá)式”的語言呢?這是出于對語言結(jié)構(gòu)本質(zhì)的考慮滑蚯。其實(shí)浪蹂,我覺得語法是完全不應(yīng)該存在的東西。即使存在告材,也應(yīng)該非常的簡單坤次。因?yàn)檎Z法其實(shí)只是對語言的本質(zhì)結(jié)構(gòu),“抽象語法樹”(abstract syntax tree斥赋,AST)缰猴,的一種編碼。一個(gè)良好的編碼疤剑,應(yīng)該極度簡單滑绒,不引起歧義,而且應(yīng)該容易解碼隘膘。在程序語言里疑故,這個(gè)“解碼”的過程叫做“語法分析”(parse)。
為什么我們卻又需要語法呢弯菊?因?yàn)槭艿浆F(xiàn)有工具(操作系統(tǒng)纵势,文本編輯器)的限制,到目前為止,幾乎所有語言的程序都是用字符串的形式存放在文件里的钦铁。為了讓字符串能夠表示“樹”這種結(jié)構(gòu)软舌,人們才給程序語言設(shè)計(jì)了“語法”這種東西。但是人們喜歡耍小聰明育瓜,在有了基本的語法之后葫隙,他們開始在這上面大做文章,使得簡單的問題變得無比復(fù)雜躏仇。
Lisp (Scheme 的前身)是世界上第二老的程序語言恋脚。最老的是 Fortran。Fortran 的程序焰手,最早的時(shí)候都是用打孔機(jī)打在卡片上的糟描,所以它其實(shí)是幾乎沒有語法可言的。
顯然书妻,這樣寫程序很痛苦船响。但是它卻比現(xiàn)代的很多語言有一個(gè)優(yōu)點(diǎn):它沒有歧義,沒有復(fù)雜的 parse 過程躲履。
在 Lisp 誕生的時(shí)候见间,它的設(shè)計(jì)者們一下子沒能想出一種好的語法,所以他們決定干脆先用括號把這語法樹的結(jié)構(gòu)全都括起來工猜,一個(gè)不漏米诉。等想到更好的語法再換。
自己想一下篷帅,如果要表達(dá)一顆“樹”史侣,最簡單的編碼方式是什么?就是用括號把每個(gè)節(jié)點(diǎn)的“數(shù)據(jù)”和“子節(jié)點(diǎn)”都括起來放在一起魏身。Lisp 的設(shè)計(jì)者們就是這樣想的惊橱。他們把這種完全用括號括起來的表達(dá)式,叫做“S表達(dá)式”(S 代表 "symbolic")箭昵。這貌似很“粗糙”的設(shè)計(jì)税朴,甚至根本談不上“設(shè)計(jì)”。奇怪的是宙枷,在用過一段時(shí)間之后掉房,他們發(fā)現(xiàn)自己已經(jīng)愛上了這個(gè)東西,再也不想設(shè)計(jì)更加復(fù)雜的語法慰丛。于是S表達(dá)式就沿用至今卓囚。
在使用過 Scheme,Haskell诅病,ML哪亿,和常見的 Java粥烁,C,C++蝇棉,Python讨阻,Perl,…… 之后篡殷,我也驚訝的發(fā)現(xiàn)钝吮, Scheme 的語法,不但是最簡單板辽,而且是最好看的一個(gè)奇瘦。這不是我情人眼里出西施,而是有一定理論依據(jù)的劲弦。
首先耳标,把所有的結(jié)構(gòu)都用括號括起來,輕松地避免了別的語言里面可能發(fā)生的“歧義”邑跪。程序員不再需要記憶任何“運(yùn)算符優(yōu)先級”次坡。
其次,把“操作符”全都放在表達(dá)式的最前面画畅,使得基本算術(shù)操作和函數(shù)調(diào)用砸琅,在語法上發(fā)生完美的統(tǒng)一,而且使得程序員可以使用幾乎任何符號作為函數(shù)名轴踱。
在其他的語言里明棍,函數(shù)調(diào)用看起來像這個(gè)樣子:f(1),而算術(shù)操作看起來是這樣:1+2寇僧。在 Lisp 里面,函數(shù)調(diào)用看起來是這樣(f 1)沸版,而算術(shù)操作看起來也是這樣(+ 1 2)嘁傀。你發(fā)現(xiàn)有什么共同點(diǎn)嗎?那就是 f 和 + 在位置上的對應(yīng)视粮。實(shí)際上细办,加法在本質(zhì)也是一個(gè)函數(shù)。這樣做的好處蕾殴,不但是突出了加法的這一本質(zhì)笑撞,而且它讓人可以用跟定義函數(shù)一模一樣的方式,來定義“運(yùn)算符”钓觉!這比起 C++ 的“運(yùn)算符重載”強(qiáng)大很多茴肥,卻又極其簡單。
關(guān)于“前綴表達(dá)式”與“中綴表達(dá)式”荡灾,我有一個(gè)很獨(dú)到的見解:我覺得“中綴表達(dá)式”其實(shí)是一種過時(shí)的瓤狐,來源于傳統(tǒng)數(shù)學(xué)的歷史遺留產(chǎn)物瞬铸。幾百年以來,人們都在用 x+y 這樣的符號來表示加法础锐。之所以這樣寫嗓节,而不是 (+ x y),是因?yàn)樵跊]有計(jì)算機(jī)以前皆警,數(shù)學(xué)公式都得寫在紙上拦宣,寫 x+y 顯然比 (+ x y) 方便簡潔。但是信姓,中綴表達(dá)式卻是容易出現(xiàn)歧義的鸵隧。如果你有多個(gè)操作符,比如 1+23财破。那么它表示的是 (+ 1 ( 2 3)) 呢掰派,還是 (* (+ 1 2) 3)?所以才出現(xiàn)了“運(yùn)算符優(yōu)先級”這種東西左痢∶蚁郏看見沒有,S表達(dá)式已經(jīng)在這里顯示出它沒有歧義的優(yōu)點(diǎn)俊性。你不需要知道 + 和 * 的優(yōu)先級略步,就能明白 (+ 1 (* 2 3)) 和 (* (+ 1 2) 3) 的區(qū)別。第一個(gè)先乘后加定页,而第二個(gè)先加后乘趟薄。
對于四則運(yùn)算,這些優(yōu)先級還算簡單典徊『技澹可是一旦有了更多的操作,就容易出現(xiàn)混淆卒落。這就是為什么數(shù)學(xué)(以及邏輯學(xué))的書籍難以看懂羡铲。 實(shí)際上,那些看似復(fù)雜的公式儡毕,符號也切,不過是在表示一些程序里的“數(shù)據(jù)結(jié)構(gòu)”,“對象”以及“函數(shù)”腰湾。大部分讀數(shù)學(xué)書的時(shí)間雷恃,其實(shí)是浪費(fèi)在琢磨這些公式:它們到底要表達(dá)的什么樣一個(gè)“數(shù)據(jù)結(jié)構(gòu)”或者“操作”!這個(gè)“琢磨”的過程费坊,其實(shí)就是程序語言里所謂的“語法分析”(parse)倒槐。
這種問題在微積分里面就更加明顯。微積分難學(xué)附井,很大部分原因导犹,就是因?yàn)槲⒎e分的那些傳統(tǒng)的運(yùn)算符唱凯,其實(shí)不是很好的設(shè)計(jì)。如果你想了解更好的設(shè)計(jì)谎痢,可以參考一下 Mathematica 的公式設(shè)計(jì)磕昼。試試在 Mathematica 里面輸入“單行”的微積分運(yùn)算(而不使用它傳統(tǒng)的“2D語法”)。
其實(shí) Lisp 已經(jīng)可以輕松地表示這種公式节猿,比如對 x^2 進(jìn)行微分票从,可以表示成
(D ‘(^ x 2) ‘x)
看到了嗎?微分不過是一個(gè)用于處理符號的函數(shù) D滨嘱,輸入一個(gè)表達(dá)式和另一個(gè)符號峰鄙,輸出一個(gè)新的表達(dá)式。
同樣的公式太雨,傳統(tǒng)的數(shù)學(xué)符號是這個(gè)樣子:
這是什么玩意耙髁瘛?d 除以 dx囊扳,然后乘以 x 的平方吩翻?
在 Lisp 里,你其實(shí)可以比較輕松地實(shí)現(xiàn)符號微分的計(jì)算锥咸。SICP里貌似有一節(jié)就是教你寫個(gè)符號微分程序狭瞎。做微積分這種無聊的事情,就是應(yīng)該交給電腦去做搏予⌒芏В總之,這從一方面顯示了雪侥,Lisp 的語法其實(shí)超越了傳統(tǒng)的數(shù)學(xué)碗殷。
其實(shí)我一直都在想,如果把數(shù)學(xué)看成是一種程序語言速缨,它也許就是世界上語法最糟糕的語言亿扁。數(shù)學(xué)里的“變量”,幾乎總是沒有明確定義的作用域(scope)鸟廓。也就是說他們只有“全局變量”。上一段話的 x襟己,跟下一段話的 x引谜,經(jīng)常指的不是同一個(gè)東西。所以訓(xùn)練有素的數(shù)學(xué)家擎浴,總是避免使用同一個(gè)符號來表示兩種不同的東西员咽。很快他們就發(fā)現(xiàn)所有的拉丁字母都用光了,于是乎開始用希臘字母贮预。大寫的贝室,小寫的契讲,粗體的,斜體的滑频,花體的捡偏,…… 而其實(shí),他們只不過是想實(shí)現(xiàn) C++ 里的 “namespace”峡迷。
可惜的是银伟,很多程序語言的設(shè)計(jì)者沒能擺脫數(shù)學(xué)的思想束縛,對數(shù)學(xué)和邏輯有盲目崇拜的傾向绘搞。所以他們繼續(xù)在新的語言里使用中綴表達(dá)法彤避。Haskell,ML夯辖,Coq琉预,Agda,這些“超高級”的語言設(shè)計(jì)蒿褂,其實(shí)都中了這個(gè)圈套圆米。在 Coq 和 Agda 里面,你不但可以使用中綴表達(dá)式贮缅,還可以定義所謂的 "mixfix" 表達(dá)式榨咐。這樣其實(shí)是把簡單的問題復(fù)雜化。想讓自己看起來像“數(shù)學(xué)”谴供,很神秘的樣子块茁,其實(shí)是學(xué)會(huì)了數(shù)學(xué)的糟粕,自討苦吃桂肌。
最后数焊,從美學(xué)的角度上講,S表達(dá)式是很美觀的設(shè)計(jì)崎场。所有的符號都用括號括起來佩耳,這形成一種“流線型”的輪廓。而且由于可以自由的換行排版谭跨,你可以輕松地對齊相關(guān)的部分干厚。在 Haskell 里,你經(jīng)常會(huì)發(fā)現(xiàn)一些很蹩腳螃宙,很難看的地方蛮瞄。這是因?yàn)橹芯Y表達(dá)式的“操作符”,經(jīng)常不能對在一起谆扎。比如挂捅,如果你有像這樣一個(gè) case 表達(dá)式:
case x
Short _ -> 1
VeryLooooooooooooooooooooooooog _ -> 2
為了美觀,很多 Haskell 程序員喜歡把那兩個(gè)箭頭對齊堂湖。結(jié)果就成了這樣:
case x
Short _ -> 1
VeryLooooooooooooooooooooooooog _ -> 2
作為一個(gè)菜鳥級攝影師闲先,你不覺得第一行中間太“空”了一點(diǎn)嗎状土?
再來看看S表達(dá)式如何表達(dá)這東西:
(case x
(-> (Short _) 1)
(-> (VeryLooooooooooooooooooooooooog _) 2))
發(fā)現(xiàn)“操作符總在最前”的好處了嗎?不但容易看清楚伺糠,而且容易對齊蒙谓,而且沒有多余的間隙。
其實(shí)我們還可以更進(jìn)一步退盯。因?yàn)榧^的兩邊全都用括號括起來了彼乌,所以其實(shí)我們并不需要那兩個(gè)箭頭就能區(qū)分“左”和“右”。所以我們可以把它簡化為:
(case x
((Short _) 1)
((VeryLooooooooooooooooooooooooog _) 2))
最后我們發(fā)現(xiàn)渊迁,這個(gè)表達(dá)式“進(jìn)化”成了 Lisp 的 case 表達(dá)式慰照。
Lisp 的很多其它的設(shè)計(jì),比如“垃圾回收”琉朽,后來被很多現(xiàn)代語言(比如 Java)所借鑒毒租。可是人們遺漏了一個(gè)很重要的東西:Lisp 的語法箱叁,其實(shí)才是世界上最好的語法墅垮。