標籤:

個人感想:《On Lisp》和 《DSL》:lisp為什麼不流行?

最近看了兩本書《on lisp》和《領域特定語言》(DSL)

前者介紹lisp的一些特點,很多和SICP重合。有的很有用,比如如何寫延後計算的一個無限長列表(流),如何寫一個prolog的解釋器,如何寫簡單和複雜的宏等等。

DSL這本書就講了如何寫內部和外部DSL。

有好幾個人都寫到,當你需要一個內部腳本語言的時候,與其自己實現,不如直接就用LISP。原因很簡單。當界面和場景需要頻繁改變更新的時候,內部腳本對於隔離數據和程序,數據模型和界面、流程式控制制(MCV)非常有用。你不必為了實現一個小功能把整個程序重新編譯一遍,用腳本實現新功能不僅簡單容易調試,還使得功能實現、可能的錯誤更直觀。

腳本需求往往是以下幾個層次逐步遞進

1 簡單的複製順序按鍵。vi和loutus123的腳本其實就是這種,缺點非常明顯,不夠智能,連臨時變數都沒有,可做的事情非常少

2 批量處理。這樣就需要循環的控制了。理論上可以通過一些方法繞過,甚至用按鍵序列也不是不能實現,只是沒有幾個寫內部腳本的會滿意於這點

3 有智能判斷,有*可靠*的臨時變數。這個時候基本就不得不引進程序塊(也就是if語句和for語句被包起來的那塊)和變數scope了。如果允許程序塊嵌套,也就是if套循環或者循環套循環,基本就不得不實現一個正規的腳本語言了,這樣工作量就大了。如果自己實現整套腳本語言,很容易出錯誤,不如嵌入一個現成的語言。

一個全功能的腳本語言,如果想要解析簡單而正確,1代碼量小2功能全,3易擴展,在過去基本只能有一個選擇,lisp。lisp不管垃圾回收的話,是當時所有語言裡面代碼最簡單功能最全的。其他所有語言都是代碼比lisp複雜功能還沒有lisp的零頭。要實現新的內部特殊語法,lisp也是最簡單的,用宏展開就完了,不像其他語言還要一個個寫邏輯綁上去。像各種shell,sendmail,各種配置腳本等等,到後來擴展起來都是頭疼的要死。

但是除了emacs和autocad用lisp外,微軟裡面用vb,一大堆人寫lua,python,tk,ruby,perl等等,origin甚至直接用偽c,就沒有幾個願意用lisp的。為什麼?

DSL裡面沒把直接說,但是解釋得很清楚:lisp是給機器讀的,層次結構清晰,幾乎不會有模糊解釋,這才有lisp宏這種用程序寫程序的東西,別的語言要想實現用程序寫程序首先就要實現解析語法樹的功能,lisp根本不用天然就是解析好的語法樹,操作起來特別方便。當然,到後面要實現寫實現宏的宏這種元語言的元語言的奇葩,還是要廢不少腦子的。

但是你反過來想,機器認得好操作的東西,還有一樣是什麼?是彙編語言。現在還有幾個人用彙編?lisp沒人用的原因很簡單,違反人類語言的習慣,不是人讀的。一個是前綴表達式的問題(forth這種比basic設計的好的多的語言也是因為類似的原因被拋棄),另外一個原因就是括弧過多的本質;層次太深。定義一個函數,定義就有起碼兩層,局部變數全局加一層,內部有並行的兩層,循環全局二層內部三層。雖然有ide和縮進規則可以保證括弧配對,但是說句實在話,我們是人,幹嗎要干中綴轉前綴和括弧配對這種本應該是電腦的活?ocamel和haskell不搞這些,命令式語法寫起來爽多了,也沒有給fp本質帶來什麼麻煩啊?沒有了宏有點不方便,但是90%需要用宏的地方用內置語法糖就解決了呀?而且語法糖也並不多。

不說寫的時候,光是看的時候就有問題。我們大家習慣了說有上下文有歧義性的語句,我們習慣了靠關鍵字和順序來定義單詞的詞性和角色,我們不習慣層次太深的套套邏輯。我到現在看lisp,判斷語言的層次關係仍然使用關鍵詞和縮進,很少去數括弧在哪裡配對。我想其實很多lisper應該也是一樣的。只要語法結構設計合理,特別是語法塊設計合理並且不搞太複雜的愈發特徵,類C語法的複雜度還是可以接受的。

實際上,lisp的優點很多,但是lisp宏這東西,雖然比c語言的簡單替換宏要有力的多,卻容易進一步帶來理解的困難。你寫的程序是要給別人看得,你寫的方便要別人也看的方便才是,用了太多自定義宏,除非這種替換是簡化原有層次關係變得更直觀的,否則只會像方言一樣帶來不便。我同意宏是個好東西,但是寫一個生成宏的宏,除非沒有更好的方案,我覺得實在是有點過了。而現代的 腳本語言也不是早年的那些了,從設計的一開始就考慮好了解析的需求,從虛擬機的層次實現新語法(類型反射+語句塊,我覺得是比lisp宏更強大更本質的解決方案),所以語法樹分析也沒有那麼必要了,雖然應用範圍窄了不少。內部語言最討厭的就是語法解析部分了,只要表現力足夠,人家又寫好了現成的解釋器不必自己實現,那麼用容易理解的就好了,不必非lisp不可。

ruby自稱是新時代的perl,實際上的lisp。我覺得像lisp的部分就在於可以實現類似宏的內部新語法,和程序塊(一般叫迭代器,其實就是一個匿名的函數或者lambda函數作為常規函數的參數的用法)這兩個。ruby用的是反射的方法,我覺得比lisp宏的抽象層次更高,同時又保留了一般語法的易讀性。迭代器和lambda函數的區別也非常簡單,就是少用了一到兩層括弧。別看就「少了那麼幾個關鍵字」「少了一層括弧」的語法糖,易讀性和易用性增加了上十倍。C#裡面引進非同步關鍵字,多了一個關鍵字,看起來更複雜了,但是寫程序的時候再也不用函數套函數了,讓編譯程序去算嵌套關係,把計算機擅長的東西還給計算機,極大簡化了寫程序的難度,增加了直觀性。語法糖減少了語法層次,就是好的;只是減少敲鍵盤次數卻打亂了層次或者引入歧義,則沒什麼必要。王垠說的python的縮進容易在打字錯誤的時候搞亂層次就是個例子。C就有不少這種錯誤設計,但是lisp這樣沒有半點變通又太過了。

可能有人會說,這些語言擴展都很有限,沒有lisp豐富和強大,不少情況擴展起來也沒有lisp簡單,不會出現像lisp那樣光靠宏就實現面向對象那樣強大。問題是,用戶用得爽,容易理解,20%的改進解決了80%的問題,這就夠了。如果有必要用到非常大的複雜情況,這種情況就不該用腳本了。設計DSL首先就要警惕像過去的一大堆腳本語言那樣大而全讓用戶不知道怎麼寫,從而失去了腳本的本意。

==========

談到內部dsl,onlisp一書裡面有兩個例子,一個是把樹狀判斷邏輯鏈編譯成一個單獨的函數變數。一個是實現一個完全由lisp實現的資料庫外加語言。非常讓人印象深刻。但是,實際上都是玩弄lisp,根本不是現實的解決方案,只是證明能做,並不能證明做好。

資料庫問題的關鍵在於性能,性能瓶頸又來自於小內存和大數據,還有數據可靠性和持久性。lisp萬般能耐,不和底層打交道,永遠解決不了語言外的問題。

邏輯判斷鏈是個數據結構的問題。lisp的表能模擬任何一種數據結構,閉包還能用函數對象把數據結構的細節隱藏起來——但是這和解決問題有關係嗎?你要寫複雜的邏輯判斷,應該把數據結構突出出來才是,這樣才能防止錯誤,把數據的生成和分配用閉包隱藏起來除了減少內存分配錯誤,幫助很小。

這個世界上存在複雜性,有些複雜性是數據內在的,我們應該直面;有的是語言的表現能力問題,我們要改進語言或者使用dsl或者寫類庫,隱藏複雜性;這兩者應該分開
推薦閱讀:

什麼?Python3.X不能輸出中文?原來是編輯器geany的鍋?!
對於UiPath的初步了解
Python中期學者必備之閉包
C語言基礎:函數參數與返回值
英偉達技巧教程是什麼?

TAG:編程 |