1. 程式人生 > >如何設計一門語言(十一)——刪減語言的功能

如何設計一門語言(十一)——刪減語言的功能

大家看到這個標題肯定會歡呼雀躍了,以為功能少的語言就容易學。其實完全不是這樣的。功能少的語言如果還適用範圍廣,那所有的概念必定是正交的,最後就會變得跟數學一樣。數學的概念很正交吧,正交的東西都特別抽象,一點都不直觀的。不信?出門轉左看Haskell,還有抽象代數。因此刪減語言的功能是需要高超的技巧的,這跟大家想的,還有跟go那幫人想的,可以斷定完全不一樣。

首先,我們要知道到底為什麼需要刪減功能。在這裡我們首先要達成一個共識——人都是很賤的。一方面在發表言論的時候光面堂皇的表示,要以需求變更和可維護性位中心;另一方面自己寫程式碼的時候又總是不惜“後來的維護者所支付的代價代價”進行偷懶。有些時候,人就是被語言慣壞的,所以需要對功能進行刪減的同時,又不降低語言的表達能力

,從而讓做不好的事情變得更難(完全不讓別人做不好的事情是不可能的),這樣大家才會傾向於寫出結構好的程式。

於是,語法糖到底是不是需要被刪減的物件呢?顯然不是。一個好的語言,採用的概念是正交的。儘管正交的概念可以在拼接處我們需要的概念的時候保持可維護性和解耦,但是往往這麼做起來卻不是那麼舒服的,所以需要語法糖。那如果不是語法糖,到底需要刪減什麼呢?

這一集我們就來討論面向物件的語言的事情,看看有什麼是可以去掉的。

在面向物件剛剛流行起來的時候,大家就在討論什麼搭積木程式設計啊、is-a、has-a這些概念啊、面向介面程式設計啊、為什麼硬體的互相插就這麼容易軟體就不行呢,然後就開始搞什麼COM啊、SOA啊這些的確讓插變得更容易,但是部署起來又很麻煩的東西。到底是什麼原因造成OO沒有想象中那麼好用呢?

之所以會想起這個問題,其實是因為最近在我們研究院的工位上出現了一個相機的三腳架,這個太三腳架用來固定一個手機乾點邪惡的事情,於是大家就圍繞這個事情展開了討論——譬如說為什麼手機和三腳架是正交的,中間只要一個前凸後凹的用來插的小鐵塊就可以搞定,而軟體就不行呢?

於是我就在想,這不就是跟所謂的面向介面程式設計一樣,只要你全部東西都用介面,那軟體組合起來就很簡單了嗎。這樣就算剛好對不上,只要寫個adaptor,就可以搞定了。其實這種做法我們現在還是很常見的。舉個例子,有些時候我們需要Visual C++ 2013這款全球最碉堡的C++ IDE來開發世界上最好的複雜的軟體,不過自帶的那個cl.exe實在是算不上最好的。那怎麼辦,為了用一款更好的編譯器,放棄這個IDE嗎?顯然不是。正確的解決方法是,買intel的icc,然後換掉cl.exe,然後一切照舊。

其實那個面向介面程式設計就有點這個意思。有些時候一個系統大部分是你所需要的,別人又不能滿足,但是剛好這個系統的一個重要部分你手上又有更好的零件可以代替。那你是選擇更好的零件,還是選擇大部分你需要的周邊工具呢?為什麼就非得二選一呢?如果大家都是面向介面程式設計,那你只需要跟cl.exe換成icc一樣,寫個adaptor就可以上了。

好了,那介面是什麼?其實這並沒有什麼深奧的理解,介面指的就是java和C#裡面的那個interface,是很直白的。不知道為什麼後來傳著傳著這條建議就跟一些封裝偶合在一起,然後各種非面嚮物件語言就把自己的某些部分曲解為interface,成功地把“面向介面程式設計”變成了一句廢話。

不過在說interface之前,有一個更簡單但是可以類比的例子,就是函式和lambda expression了。如果一個語言同時存在函式和lambda expression,那麼其實有一個是多餘的——也就是函數了。一個函式總是可以被定義為初始化的時候給了一個lambda expression的只讀變數。這裡並不存在什麼效能問題,因為這種典型的寫法,編譯器往往可以識別出來,最終把它優化成一個函式。當我們把一個函式名字當成表示式用,獲得一個函式指標的時候,其實這個型別跟lambda expression並沒有任何區別。一個函式就只有這兩種用法,因此實際上把函式去掉,留下lambda expression,整個語言根本沒有發生變化。於是函式在這種情況下就屬於可以刪減的功能

那class和interface呢?跟上面的討論類似,我主張class也是屬於可以刪減的功能之一,而且刪減了的話,程式設計師會因為人類的本性而寫出更好的程式碼。把class刪掉其實並沒有什麼區別,我能想到的唯一的區別也就是class本身從此再也不是一個型別,而是一個函數了。這有關係嗎?完全沒有,你用interface就行了。

class和interface的典型區別就是,interface所有的函式都是virtual的,而且沒有區域性變數。class並不是所有的函式都是virtual的——java的函式預設virtual但是可以改,C++和C#則預設不virtual但是可以改。就算你把所有的class的函式都改成virtual,那你也會因此留下一些狀態變數。這有什麼問題呢?假設C++編譯器是一個介面,而Visual C++和周邊的工具則是依賴於這個class所創造出來的東西。如果你想把cl.exe替換成icc,實際上只要new一個新的icc就可以了。而如果C++編譯器是一個class的話,你就不能替換了——就算class所有的函式都是virtual的,你也不可能給出一個規格相同而實現不同的icc——因為你已經被class所宣告的建構函式、解構函式以及寫好的一些狀態變數(成員變數)所綁架了

那我們可以想到的一個迫使大家都寫出傾向於比以前更可以組合的程式,要怎麼改造語言才可以呢?其實很簡單,只需要不把class的名字看成一個型別,而把他看成一個函式就可以了。class本身有多個建構函式,其實也就是這個意思。這樣的話,所有原本要用到class的東西,我們就會去定義一個介面了。而且這個介面往往會是最小化的,因為完全沒有必要去宣告一些用不到的函式。

於是跟去掉函式而留下匿名函式(也就是lambda expression)類似,我們也可以去掉class而留下匿名class的。Java有匿名class,所以我們完全不會感到這個概念有多麼的陌生。於是我們可以來檢查一下,這樣會不會讓我們喪失什麼表達方法。

首先,是關於類的繼承。我們有四種方法來使用類的繼承。

1、類似於C#的Control繼承出Button。這完全是介面規格的繼承。我們繼承出一個Button,不是為了讓他去實現一個Control,而是因為Button比Control多出了一些新東西,而且直接體現在成員函式上面。因此在這個框架下,我們需要做的是IControl繼承出IButton

2、類似於C#的TextReader繼承出StreamReader。StreamReader並不是為了給TextReader新增新功能,而是為了給TextReader指定一個來源——Stream。因此這更類似於介面和實現的區別。因此在這個框架下,我們需要的是用CreateStreamReader函式來建立一個ITextReader

3、類似於C#的XmlNode繼承出XmlElement。這純粹是資料的繼承關係。我們之所以這麼做,不是因為class的用法是設計來這麼用的,而是因為C++、Java或者C#並沒有別的辦法可以讓我們來表達這些東西。在C裡面我們可以用一個union加上一個enum來做,而且大家基本上都會這麼做,所以我們可以看到這實際上是為了拿到一個tag來讓我們知道如何解釋那篇記憶體。但是C語言的這種做法只有大腦永遠保持清醒的人可以使用,而且我們可以看到在函式式語言裡面,Haskell、F#和Scala都有自己的一種獨有的強型別的union。因此在這個框架下,我們需要做的是讓struct可以繼承,並且提供一個Nullable<T>(C#也可以寫成T?)的型別——等價於指向struct的引用——來讓我們表達“這裡是一個關於資料的union:XmlNode,他只可能是XmlElement、XmlText、XmlCData等有限幾種可能”。這完全不關class的事情。

4、在Base裡面留幾個純虛擬函式,讓Derived繼承自Base並且填補他們充當回撥使用——臥槽都知道是回調了為什麼還要用class?設計模式幫我們準備好了Template Method Pattern,我們完全可以把這幾個回撥寫在一個interface裡面,讓Base的建構函式接受這個interface,效果完全沒有區別。

因此我們可以看到,幹掉class留下匿名class,根本不會對語言的表達能力產生影響。而且這讓我們可以把所有需要的依賴都從class轉成interface。interface是很好adapt的。還是用Visual C++來舉例子。我們知道cl.exe和icc都可以裝,那gcc呢?cl.exe和icc是相容的,而gcc完全是另一套。我們只需要簡單地adapt一下(儘管有可能不那麼簡單,但總比完全不能做強多了),就可以讓VC++使用gcc了。class和interface的關係也是類似的。如果class A依賴於class B,那這個依賴是綁死的。儘管class A我們很欣賞,但是由於class B實現得太傻比從而導致我們必須放棄class A這種事情簡直是不能接受的。如果class A依賴於interface IB,就算他的預設實現CreateB()函式我們不喜歡,我們可以自己實現一個CreateMyB(),從而吧我們自己的IB實現給class A,這樣我們又可以提供更好的B的同時不需要放棄我們很需要的A了。

不過其實每次CreateA(CreateMyB())這種事情來得到一個IA的實現也是很蠢得,優點化神奇為腐朽的意思。不過這裡就是IoC——Inverse of Control出場的地方了。這完全是另一個話題,而且Java和C#的一些類庫(包括我的GacUI)已經深入的研究了IoC、正確使用了它並且發揮得淋漓盡致。這就是另一個話題了。如何用好interface,跟class是否必須是型別,沒什麼關係。

但是這樣做還有一個小問題。假設我們在寫一個UI庫,定義了IControl並且有一個函式返回了一個IControl的實現,那我們在開發IButton和他的實現的時候,要如何利用IControl的實現呢?本質上來說,其實我們只需要創造一個IControl的實現x,然後把IButton裡面所有原本屬於IControl的函式都重定向到這個x上面去,就等價於繼承了。不過這個寫起來就很痛苦了,因此我們需要一個語法糖來解決它,傳說中的Mixin就可以上場了。不知道Mixin?這種東西跟prototype很接近但是實際上他不是prototype,所以類似的想法經常在javascript和ruby等動態語言裡面出現。相信大家也不會陌生。

上面基本上論證了把class換成匿名class的可能性(完全可能),和他對語言表達能力的影響(毫無影響),以及他對系統設計的好處(更容易通過人類的人性的弱點來引導我們寫出比現在更加容易解耦的系統)。儘管這不是銀彈,但顯然比現在的做法要強多了。最重要的是,因為class不是一個型別,所以你沒辦法從IX強轉成XImpl了,於是我們只能夠設計出不需要知道到底誰實現了IX的演算法,可靠性迅速提高。如果IY繼承自IX的話,那IX可以強轉成IY就類似於COM的QueryInterface一樣,從“檢視到底是誰實現的”昇華到了“檢視這個IX是否具有IY所描述的功能”,不僅B格提高了,而且會讓你整個軟體的質量都得到提高。

因此把class換成匿名class,讓原本正確使用OO的人更容易避免無意識的偷懶,讓原本不能正確使用OO的人迅速掌握如何正確使用OO,封死了一大堆因為偷懶而破壞質量的後門,具有相當的社會意義(哈哈哈哈哈哈哈哈)。

我之所以寫這篇文章是為了告訴大家,通過刪減語言的功能來讓語言變得更好完全是可能的。但這並不意味著你能通過你自己的口味、偷懶的習慣、B格、因為智商低而學不會等各種奇怪的理由來衡量一個語言的功能是否應該被刪除。只有冗餘的東西在他帶來危害的時候,我們應該果斷刪除它(譬如在有interface前提下的class)。而且通常我們為了避免正交的概念所本質上所不可避免的增加理解難度所帶來的問題,我們還需要相應的往語言裡面加入語法糖或者新的結構(匿名class、強型別union等)。讓語言變得更簡單從來不是我們的目標,讓語言變得更好用才是。而且一個語言不容易學會的話,我們有各種方法可以解決——譬如說增加常見情況下可以解決問題的語法糖、免費分享知識、通過努力提高自己的智商(雖然有一部分人會因此感到絕望不過反正社會上有那麼多職業何必非得跟死程死磕)等等有效做法。

於是在我自己設計的腳本里面,我打算全面實踐這個想法。