哪些語言特性,有助於開發大型系統?

在大型項目上,Python 是個爛語言嗎? 因為這個問題,我想到了另外一個可能有些建設性的問題,希望大家一起來探討一下。

正好在看「深入淺出 NodeJS」,談到 Node 的模塊機制與包管理,於是聯想到大型系統開發時需要的語言特性。


關於「大型系統設計」,每個人都應該看看這篇,Clojure 設計者 Rich Hickey 的 Keynote "The Language of the System" http://www.youtube.com/watch?v=ROor6_NGIWU 討論了如何設計大型系統中的各種問題、組件特性和要求;其中捎帶提到了 Clojure 一些特點,但並不針對 Clojure,非常值得一聽。
視頻封面The Language of the System - Rich Hickey視頻

------

任何降低 book keeping、logistics 和 communication cost 的特性。

降低 book keeping cost 的特性包括:GC、自動類型推導、內置模塊化支持,甚至更極端的並行支持、分散式支持(Erlang 是最極端的例子)等。為繁瑣的 book keeping 工作提供可靠而普適的內置支持是任何一門有節操的語言減少程序員出錯機會的最基本表現形式。至少要有一個足夠通用好用的內置字元串類型而不是拿半吊子類庫來忽悠人啊 C++!

降低 logistics cost:提高(或完全去除)編譯時間。對,除了 C++,你很少能找到 Build All 需要 1h+ 的項目。Linux Kernel 都基本不需要這個數。(@vczh 我應該是聽 AutoDesk 的兄弟們說的,具體項目忘記了。雖然比不上貴司,他們至少應該不是缺錢的主……)

降低 communication cost:功能集正交:用同一種方法解決一個問題,一個技術專門用來解決一個問題。當你允許,甚至鼓勵(通過內置類庫)程序員用任何可能的方法解決問題的時候,這些代碼都屬於 write only 的類型。來 C++ 我們討論一下為什麼會有 TMP 這種變態東西,為什麼會有 new vs malloc 。你給用戶不必要的自由越多,他們就越不受控制——不受控制的結果就是它們再也沒法相互溝通了。而失去溝通能力是項目一大必死原因的第一名。

吐槽了一篇 C++,補充一些其它的吧。

儘早發現錯誤的能力:靜態類型、編譯語言在這個方面有相當的好處;內置的前後條件、函數式語言的不變數,都是。Java / C++ 可以在編譯時直接發現參數類型傳錯,Python 腳本說不定跑很久了才碰到。

工具支持。Go 直接提供了 gofmt 之類的工具,Java 有 Eclipse / IntelliJ 這些對重構有巨大幫助的輔助工具,更不說 JVM 的 JMX 。Python 的 pip 也算勉強在「工具」方面作了一點什麼。沒有標準、優秀的工具(包括環境、庫)支持,很多時候是逼迫程序員自己拿石頭鑿輪子。


首先我認為,大型系統指的是,團隊裡面的人你認識和溝通不超過5%、沒有文檔就無法理解大多數人當初寫那幾行代碼的意圖、代碼具有或者接近8位數行、而且需要長期維護、具有完善的自動化測試,所以制度本身和(面試、考核或certification等的)高門檻才是保證項目本身可以健康的生存下去的最重要的因素的系統。也不要以為這種代碼不多,windows、office、maya、cad軟體、戰鬥機控制程序、還有出了bug就可以吧楊利偉殺死的這些要命的程序——這些通常都複雜。完成這種系統,需要這個團隊在根需求無關的、跟開發有關的幾乎所有方面都實行獨裁統治。

所以這種系統需要語言提供什麼樣的功能呢?我們知道,因為這幾千萬行都是同一個軟體的代碼,所以裡面必然具有無數的互相依賴的模塊,一萬多人開發十幾年下來,什麼是什麼都基本上理不清楚了。所以這個時候你做一個修改,最重要的是要保證不要把別人的代碼搞爛。就算你跑完了所有自動化測試,如果存在一些東西是自動化測試所無法覆蓋的,那你覺得你可以靠人肉排除來保證所有已知的、修過的bug不再出現嗎?

這當然是不可能的,儘管不能100%解決問題,但是一個強大的靜態分析軟體是可以幫我們解決95%的問題(就是那些不能被自動化測試所覆蓋的問題裡面的95%)。於是這就要求這個語言必須要有一個強大的靜態分析軟體,或者potentially你可以寫一個強大的靜態分析軟體。

寫一個靜態分析軟體面臨的最大的困難有兩個,一個是name resolving,另一個是reference resolving。name resolving的意思就是,你得到了這個名字,你知不知道他究竟代表的是你源代碼裡面的哪一個函數或者變數。reference resolving的意思就是,你得到了兩個指針,你知不知道運行到某一行確定的代碼的時候,這兩個指針是否指向同一個對象?

於是為了讓靜態分析軟體更容易的做出來,我們需要語言本身可以讓我們很輕鬆的分析出這兩個resolving的內容。很難分析name resolving的一般是動態類型語言,譬如python、javascript、ruby這些。很難分析reference resolving的一般是帶指針和引用的靜態類型語言語言,譬如C/C++、java、C#這些。

我們知道,兩個都容易的一般指的是那些在語法上要求你明確副作用的語言,譬如Haskell。如果世界上大部分人都是從lisp/scheme/ocaml/haskell開始學習的話,其實學習Haskell並沒有那麼難(一旦你習慣了C語言那一套你就晚了)。只要你招得到足夠多的人,並且讓那些人滿足你製作出來的規章制度的話,Haskell是可以用來做大型系統的。

只不過這個事情在現實裡面一般比較難滿足,那name resolving和reference resolving取捨哪個好呢?因為解決reference resolving是在name resolving解決了的基礎上才可以解決的,所以你只能保留name resolving。如果一個語言沒有name resolving的困難,那你的靜態分析軟體的難度瞬間就下降了許多。根據上面的描述,這方面的代表自然是靜態類型語言。

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

參考了@庄表偉 的評論,我覺得只要一個具有programming language背景的人,大概都知道什麼語言的特性是在分析的時候跟name resolving或者reference resolving相關的。我可以狙擊個簡單的例子。

prototype:難以做name resolving
指針:難以做reference resolving,但是還好,因為那種超大的系統一般你不敢濫用指針,所以靜態分析軟體就算不能完全解決這個問題(本來也是無法完全解決的),也只會讓他們成為corner case,落進5%裡面。
dynamic dispatch:跟reference resolving有關
static dispatch:跟兩者無關
pure:跟兩者無關

等等。feature的列表實在是太多了,不想一個一個列出來。

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

下面還有一個匿名用戶提到了communication cost的問題。的確溝通成本是一個問題,但是語言根本不會對溝通成本有什麼影響,因為你一個組有一萬個人,就算你什麼都不幹,你能溝通多少東西。

還有編譯時間。sqlserver這麼大的程序也只需要10分鐘,因此這個故事告訴我們,提高編譯時間,不能靠語言,而要靠砸錢呀。你買得起一個大集群,我們微軟的MSBuild就可以幫你把剩下的事情做完。

至於說C++的string和TMP這種變態,STL是不是半吊子類庫我就不評論了,至於TMP——如果你不喜歡,只要你強制大家不要用,review的時候肯定會讓所有用了TMP的人無法checkin,如果他們堅持使用,他們就會沒有delivery,年底就會沒有成績,就會被末位淘汰,然後你就沒有這種煩惱了。你想通過降低語言的自由度來起到什麼幫助基本沒什麼用的,只要你把制度寫清楚了就行了。


說實話,多大算大這個問題很讓我困擾。你說豆瓣那規模大不大?其實沒用什麼高大上的語言特性吧,Python這麼憨厚的玩意兒,不也跑的挺好?
當然我知道同行們有些是看不上這種規模的項目,我也理解,也認同。但是戰鬥機,衛星,航空母艦,外星探險車,或者一個維護幾十年,規模超過千萬行的操作系統,這些項目對這個世界上多大比例的開發人員有參考和學習意義?但是如果我們無視這些極端場景下積累的知識和經驗,又會錯過什麼?
我個人的經驗來講,「大型系統」,並不能對等於「優秀的產品」或者能讓我心動的知識。做一個工程,需要各種各樣的妥協,最理想的工具未必就是最終能幫助我們做成這件事的。但是我還是可以盡量的給出我自己心目中一些對項目開發有正面意義的語言特性。

  • 一個安全方便的資源管理方案。主要是內存和IO資源。行內有個笑話,一群人說內存管理太重要了,不能交給程序員;一群人說內存管理太重要了,必須交給程序員。該給誰,具體的項目說了算,一個既有的質量達標的運行時,對於普遍的應用項目是有意義的。大到可以用來自誇的「大型項目」,小到手機app。GC、ARC、智能指針、內存池,都可以看作對資源管理自動化的方案。Java 雖然語法笨拙,但是JVM的質量確保它得到足夠的擁護。C說不上有什麼內置的自動化資源管理方案,所有優質的C項目都會自己實現一套,CPP理論上有非常好的方案,但是很多優質的CPP項目也是自己實現一套。
  • 一個功能豐富好用的文本類型。這個東西在理論上根本不應該是個事兒,字元串,那玩意兒不就是個特化的線性容器么,好多專家都能說的頭頭是道。但是在應用領域,文本類型的支持程度,完全可以決定一個語言是否能流行開。基本上比CPP文本功能更弱的編程語言,現在是很難被廣泛接受的。
  • 模塊和命名空間隔離能力。這個東西有些語言是放在編譯和編碼階段,有的是跟運行時儘可能相關的。但是運行時的模塊管理策略其實影響資源載入機制,也就一定程度上影響了效率。效率這個問題么,又是一個吵不完的羅圈架。很多人都覺得自己有過人的深刻見解。這個我無意參與爭執,總之語言級別提供助記方案,通常總是有幫助的。
  • 開發工具支持。我個人近幾年除了iOS/Mac OS的GUI相關的開發,基本上是純編輯器工作,這並不表示我完全否認開發工具的作用,相反好的IDE肯定對工作是有幫助的。我對IDE的負面觀點主要集中在幾點,一個是「好」語言不應該依賴於某一種特定的IDE,一個是語言應該對開發工具是透明的,工具能做到的,應該允許人手工也能做到,這樣才能方便的發展團隊自己的自動化過程。
  • 類型約束。老實說我沒覺得這個東西必不可少。它確實對開發有幫助,對錯誤容忍越少的項目,越需要類型約束幫助我們提高代碼質量。但是也確實有很多項目就是用動態類型的語言嘩啦嘩啦寫出來就最好了。程序沒有面對那麼極端的場景,我就用1/10的開發成本寫個幾萬行的項目出來,再用1/10的成本寫個萬把行的測試,再用2/10的成本去測試行不行?很多項目是可以這麼做的,而且我看這麼做出來的項目很多並不小。
  • 內置類型,特別是基礎容器和演算法庫。質量高不高,好不好用,跟語言的結合程度,至少影響了使用上的舒適感。個人觀點上CPP對C的優點,相當大的基於STL和BOOST,它們可以讓我幾乎忘了我在用一個「C」 Plus Plus語言。Python 受到歡迎,很大程度也是因為[]和{}內置類型非常好用。類似的,現在對並發和非同步的語言級支持,也是越來越受到重視,但是還沒有內置類型這麼重。很多時候我們還把它看做是需要程序員明確構造的東西。但是大概再過幾年,非同步和並發的細節也都移交給C和CPP這個層面了。

這些東西也只是有助於我們工作,其實沒有絕對的致命因素,最終還是要講究妥協。例如C/CPP沒有那麼完整的runtime,但是我們有資金,有開發周期,我們請來專家寫一個專用的runtime給我們自己用行不行?其實質量可能會更高對吧。那些「只能用」C/CPP才做得到的「大型項目」,很多都要做這樣的工作,另一個角度看,我們用的很多編程語言,也是在享受別人預先完成的runtime。別的我不好說,Python/Ruby就是典型。但是我不認為這就表示這樣的語言格調不高,沒有價值。例如CPP對C語言的很多優勢,如果換個角度,把LUA看做一個C的runtime和DSL工具集,能不能用來比較和代替?這個不討論了,意思就表達到這裡了。
一時能想到的,比較具體的點就這些了。至於那些形而上的理論,其實我很難說服別人,也很難被人說服。就不提了。
===========
忘了說,是否方便調試其實應該算在內,雖然抬杠的說可以說這個東西不是語言應該操心的問題,但現實是某種語言是不是方便調試除錯還是可以比較的。


我覺得 @徐辰 說得不錯

沒有什麼像樣的「大型系統」是用單一語言開發出來的

以SAP為例,底層是基於C/C++開發的Kernel,資料庫層面用了SQL,應用層使用SAP自己的ABAP語言,GUI有兩個版本,Windows版是用VC,跨平台版是用JAVA,還有Web前端用了JS,移動端用了Obj-C
從這個意義上說,根本不可能有一種語言(或者說一組互不矛盾的語言特性)可以解決真正大型系統的開發問題
這也是為什麼世界上會有那麼多語言,卻沒有一個能夠一統江湖。
大型項目的根本問題還在於管理,或者說是人的問題,而不是語言或工具的問題。
不過我在 在大型項目上,Python 是個爛語言嗎? 這個問題的回答里也說過了,仍然有一些語言是對人的管理是有幫助的。
JAVA就是其一。
單就語言的技術特性來說,JAVA的優點並不算特別突出,缺點倒是不少。
但是在對開發人員的管理方面,它有一個極大的優點——優秀的JAVA程序員與差的JAVA程序員之間的差距相對較小,而且JAVA程序員好找……
這也是為什麼很多「大型」項目都喜歡用它——不一定是最好的,但基本可以保證不會差到哪裡去
之所以有這樣的優點,在於它的語言特性在很多方面作出了限制,盡量從技術上避免人犯錯。比如GC,靜態類型,引用,包,介面……
對了,還有很多現成的庫等資源,加上強大的IDE。
而其它大部分語言雖然都可能做出更好的東西,但存在兩個根本的問題:
第一:有更大的可能做出完全失敗的東西,比如C++
第二:沒有足夠的人手,比如Haskell
最後,就個人而言,我非常不喜歡JAVA,因為它對於程序員來說太束手束腳了,而且寫起來太啰嗦。


我使用C/C++,Java多年,最近一年也學習了Scala, Clojure和Go語言。程序語言設計方面,我向@連城 請教的最多,他應該比我更有實力說一說,他寫過一個筆記,理想的編程語言。

我理想的語言特性如下,條目很雜,有大有小:

  1. 強類型+靜態類型。一個好的類型系統,能夠在編譯階段檢查出很多錯誤,也方便IDE開發豐富的功能。絕對不允許有隱式類型轉換,例如從int32 到 int64,從int64到int32更不允許。C是一個弱類型語言,C++比C略強,但也不能算是強靜態類型。Java也不算,Scala比Java略強。最強的是Hashkell。
  2. 除了靜態類型,還需要有很強的自動類型推導,寫代碼跟動態語言一樣,很簡潔。例如C++11裡面的auto, Scala里的val, var,目前,它們的自動類型推導還沒有強到我心目中完美的樣子,有些時候編譯器推導不出類型,需要手動加上類型。
  3. 要有閉包,函數作為一等公民。函數式編程在並發方面有很強的表達能力,例如Spark和Storm。也可以讓代碼寫的很短,且在概念上很統一。C++11有了lambda, Java 8也即將有,Scala, Clojure更不用說了,Go語言也有。
  4. 要有面向對象。面向對象是一個很好的抽象工具,現在的很多程序員都接受了這個工具,所以要做成語言特性。但是不能做成像Java, C++這種,要做的像Smalltalk那樣,基於消息傳遞的。Go語言的那種隱式介面,跟C++, Java也很不同,不過我還沒有理解透徹,目前只對消息傳遞有好感,從Scala的Actor里感受到的。
  5. 定長的基本數據類型。例如int32,uint32(Java,go語言就是這麼做的,C99也有了stdint.h),實在不喜歡C/C++的標準,沒有定死數據類型的長度,很多時候容易造成bug。其實很多C/C++項目都會重新 typedef int int32, typedef long long int64 定義一套定長的基本類型
  6. 字元串使用UTF8編碼,源碼文件也使用UTF8編碼。國際化是必須的,UTF8已經是事實上的標準,何不採納呢。Go語言是這麼做的!
  7. 要有GC. 對於一個靜態類型語言來說,每個對象的變數和生命周期是非常清楚的,應該讓機器自動管理內存。這個時候有兩種方案,智能指針(例如c++11里shared_ptr或unique_ptr)或GC(例如Java, Scala, Clojure, Go)。GC可以做得很薄,就成了智能指針了,所以GC不僅僅可以做到智能指針的事,還能做更多。
  8. 既然有了gc,就沒有「指針」,只有引用了。指針這個東西,只是以前編譯器技術不發達的時候出現的,現代編程語言應該一律用引用這個概念。
  9. 不區分棧變數和堆變數。Java就是這麼做的,現代編譯器技術,可以做到不損耗性能,又降低程序員負擔。
  10. 沒有分號。分號散布在代碼里,充滿了小麻點,太干擾視線了。現代編譯技術完全可以做到不不需要分號,例如Scala, Go就是這麼做的。
  11. 千萬不要使用縮進語義(如Python, Hashkell那樣很不好)。容易出BUG,也不容易自動格式化。一個大型工程中,一個程序員無意中添加了一個TAB,程序依然編譯通過,但是意義不一樣,從而添加了一個BUG,但是靜態分析很難檢查出這類錯誤。我很同意王垠這篇文章的觀點,語法最漂亮的編程語言是哪種?
  12. 要有支持單機並行的標準庫或語言特性。現在大部分機器都是多核了。不要用老舊的線程和進程模型,這種模型編程複雜,很容易死鎖。Java 7里的Fork/Join 也不太好用。C++11加進了一個thread庫,太落後了,現在誰還用線程這種抽象工具呢?像Scala里就有Actor,Erlang有輕量級process,Go語言有gorutine和channel,這幾個接近我心目中的樣子。
  13. 要有支持分散式的標準庫。現在的服務端編程,多機集群是家常便飯,標準庫應該有一個方便易用的工具,方便程序員編寫分散式程序。例如Erlang OTP, Scala有AKKA(還是比不上OTP完整)。
  14. 類型放在變數名後。我個人也認為這樣可讀性要好一些。Go語言是這麼做的,為什麼 Go 語言把類型放在後面?,Scala也是,ActionScript也是。

先想到這麼多,大家多交流交流!


首先我特別贊同 @vczh 關於大型系統的定義,而在這個定義下,他的答案似乎已經沒有太多可以補充的了。但遺憾的是發現還是有很多人把這個問題看成了有哪些語言特性有助於讓程序員更爽。我想說的是,其實讓程序員更爽的特性,有助於開發大型系統的特性,幾乎是兩碼事兒

@vczh 的答案很抽象,抽象的東西總是很NX的,也就是幾句話能涵蓋一篇十萬字文章所說的東西,我沒打算把這篇文章展開來。在他討論的範疇之外,我來補充一點偏門些的東西。


1、C語系或者類C語言風格。
vczh也談到了這一點:

如果世界上大部分人都是從lisp/scheme/ocaml/haskell開始學習的話,其實學習Haskell並沒有那麼難(一旦你習慣了C語言那一套你就晚了)

而事實上遺憾的是世界上大部分人都是從C語言或者很像C的語言(Java啥的)開始學習的。所以選擇一個非C風格的語言,那麼首先,你可能根本就招不到做這個項目所需要的人手,,,,,,
甚至於即使你選擇了一個類C風格的語言,仍然可能招不到足夠的人員,事實上你的選擇可能只有:C、C++、C#、Java,,,,,

2、儘可能多的編譯器檢查。
強類型檢查是最基本的,可見性(也就是封裝),只讀性(const、readonly、final)以及等等等等,在人和編譯器之間,你應當無條件的相信後者,因為後者出錯的可能性和前者完全不是一個量級的。
一個合格的程序員和菜鳥的區別就在於合格的程序員永遠不會懷疑編譯器錯了,而是懷疑自己錯了。這是無數次對編譯器的誤解之後得到的寶貴經驗。

3、足夠大官方文檔和類庫。
足夠大的官方文檔可以避免兩個程序員對某個函數所實現的功能和限制理解的完全不一致,足夠大的類庫可以避免當你制定了一個規範的時候需要去給程序員解釋為什麼使用這個開源項目而不是另一個。
這些,都可以大幅降低隱性的communication cost。

4、單元測試和IDE支持。
據我的個人研究統計,人犯錯的概率比機器至少高出一百倍,事實上即使寫程序超過十幾年,在各種語言之間轉悠一會兒之後,每年總有一次犯諸如把相等運算符寫成賦值運算符的錯誤。即使戴了合適的眼鏡,偶爾也會分不清分號和冒號,以及逗號和句點。
而機器即使運行一百年,也絕對不會把賦值運算符當作相等運算符。


問題當然是相對於小系統而言,

那麼大家都知道,腳本,無類型的語言對於小系統(極端的情況是命令行),來說很適合,因為代碼短小,代碼量是否節省並非是開發大型系統所必須——反正大型系統嘛,都要很多人的,

大型系統,比較重要的,可靠性,這個最好得有靜態類型檢查(不至於死在低級筆誤),要麼有很好的單元測試工具,還有模塊化(可以分而治之),

當然系統越大,性能靈活性(比如說可以(熱)更新)越成為一個問題,

所以我覺得就目前選擇來說,java/c#這一類語言作為大系統的框架主體挺合適的.


和語言無關,能夠有效駕馭複雜度的方式是把系統分解成

  1. 用來驅動系統的數據
  2. 處理數據的規則系統

有可能一層分解還不夠,那就需要多層分解

原因在於,規則系統可以使得測試空間大大下降(正交了)。一旦規則系統被「proven」,剩下的就是檢驗驅動數據,而那是個很多人(比如業務經理)都能做的事,可以發動人海戰術還不用特別擔心溝通和協作瓶頸,最關鍵的是沒有coding style、personal style問題,也容易發現問題(可以把數據可視化、使用分析工具交叉檢驗數據)


能想到的唯一有助於開發大型系統的「語言特性」就是——慢,慢到令人髮指,慢到讓所有人對這個語言開發出來的系統性能不報任何期望,慢到PRD中沒法加入任何性能指標,然後你就會發現大型系統無非就是放大鏡下的小型系統了(任何對這一段有疑問的同學請參見所有的ERP/CRM等「大型系統」)。
除此之外,其它的語言特性和系統規模關係不大,因為沒有什麼像樣的「大型系統」是用單一語言開發出來的(表舉反例,所有的反例都不夠大)。


被老莊邀請,不過沒時間細細回答這個問題。但是我很建議看看go語言的特性選擇,看看rob pike的演講:《少就是指數級的多》、《Go語言,面向軟體工程設計的語言》。醍醐灌頂呀。


盡量不做第一個吃螃蟹的,有大型系統做先例的我覺得都可以
erlang夠小眾 資料也不多 ide更是渣的很
但就是可以用在生產環境,因為愛立信都用了你還怕個鳥啊
當然國內遊戲伺服器也有用的
BTW:京東一直嚷嚷著要從ASP 換到j2ee 說是這不行那不行,難道asp就這麼不堪嗎?
微軟中國、噹噹網沒法活了?


推薦閱讀:

微軟當年的 J++ 究竟是什麼?為什麼 Sun 要告它?
優秀的程序員應該掌握多少門編程語言?
以英語為母語的人寫代碼時是什麼感覺?
學習彙編語言有什麼好處?
為什麼蘋果新語言 Swift 的 RC4 運算效能是 Python 的 220 倍?

TAG:軟體開發 | 編程語言 |