概述:國內的計算機教育和工程培訓,彷佛一直在宣傳「語言不重要,重要的是思想」,「語言一通百通」等觀點,甚至在許多人眼中「語言的討論」徹底是不入流的,但其實「編程語言」與「工具」、「框架」或是「開發方法」等事物同樣,都對生產力有着重要的影響。事實上,語言的發展歷史比其餘方面更爲悠久,而且在過去十幾年,甚至最近幾年中都依然在不斷的碰撞,演變。
程序設計離不開編程語言,可是編程語言在國內的大環境中彷佛一直是個二等公民。國內的計算機教育和工程培訓,彷佛一直在宣傳「語言不重要,重要的是思想」,「語言一通百通」等觀點,甚至在許多人眼中「語言的討論」徹底是不入流的,但其實「編程語言」與「工具」、「框架」或是「開發方法」等事物同樣,都對生產力有着重要的影響。事實上,語言的發展歷史比其餘方面更爲悠久,而且在過去十幾年,甚至最近幾年中都依然在不斷的碰撞,演變。期間一些新的語言誕生了,而另外一些在當時看來陽春白雪的語言和編程範式也從新得到了人們的重視。程序員
Anders Hejlsberg是微軟的Technical Fellow,擔任C#編程語言的首席架構師,也參與了.NET Framework,以及VB.NET和F#等語言的設計與開發。幾個月前,Anders在比利時的TechDays 2010及荷蘭DevDays 2010分別進行了一場演講,闡述了他眼中對於編程語言的發展趨勢及將來方向,本文便對他的觀點進行了總結。正則表達式
大約25到30年前,Anders開發了著名的Turbo Pascal,這是一套集語言、編譯器及開發工具於一體的產品,這也是Anders進入編程語言這一領域的起點。Anders談到,現在的計算機和當年他開發的Turbo Pascal所用的Z-80已經不可同日而語。從那時算起,現在的機器已經有大約10萬倍的外部存儲容量,1萬倍的內存大小,CPU速度也有大約1000倍的提升。可是,若是咱們比較現在的Java代碼及當年Pascal代碼,會發現它們的差異其實並不大。Anders認爲編程語言的發展很是緩慢,期間固然出現了一些東西,例如面向對象等等,可是遠沒有好上1000倍。事實上,近幾十年來的努力主要體如今框架及工具等方面(以下圖)。例如.NET Framework裏有超過一萬個類及十萬個方法,與Turbo Pascal相比的確有了超過1000倍的增加。一樣相似,如今的IDE包含了無數強大的功能,例如語法提示,重構,調試器等等。與此相比,編程語言的改進的確很不明顯。數據庫
在過去五、60年的編程歷史中,編程語言的抽象級別不斷提升,人們都在努力讓編程語言更有表現力,這樣咱們能夠用更少的代碼完成更多的工做。咱們一開始使用匯編,而後使用面向過程的語言(如Pascal和C),而後是面嚮對象語言(如C++),隨後便進入了託管時代,語言運行於受託管的執行環境上(如C#,Java),它們的主要特性有自動的垃圾收集,類型安全等等。Anders認爲這樣的趨勢還會繼續保持下去,咱們還會看到抽象級別愈來愈高的語言,而語言的設計者則必須理解並預測下一個抽象級別是什麼樣子的。另外一方面,如.NET,Java等框架的重要性提升了許多,編程語言每每都傾向於構建於現有的工具上,而不會從頭寫起。如今出現的編程語言,例如F#,以及Java領域的Scala,Clojure等等,它們都是基於現有框架構建的,每次從頭開始的代價實在過高。編程
在Anders眼中,現在影響力較大的趨勢主要有三種(以下圖),它們分別是「聲明式的編程風格」(包括「領域特定語言」及「函數式編程」)、過去的五年很是火熱的「動態語言」(其最重要的方面即是「元編程」能力)以及多核環境下的「併發編程。此外隨着語言的發展,本來經常使用的「面向對象」語言,「動態語言」或是「函數式」等邊界也變得愈來愈模糊,例如各類主要的編程語言都受到函數式語言的影響。所以,「多範式」程序設計語言也是一個愈發明顯的趨勢。瀏覽器
聲明式編程與DSL安全
目前常見的編程語言大都是命令式(Imperative)的,例如C#,Java或是C++等等。這些語言的特徵在於,代碼裏不只表現了「作什麼(What)」,而更多表現出「如何(How)完成工做」這樣的實現細節,例如for循環,i += 1等等,甚至這部分細節會掩蓋了咱們的「最終目標」。在Anders看來,命令式編程一般會讓代碼變得十分冗餘,更重要的是因爲它提供了過於具體的指令,這樣執行代碼的基礎設施(如CLR或JVM)沒有太多發揮空間,只能老老實實地根據指令一步步的向目標前進。例如,並行執行程序會變得十分困難,由於像「執行目的」這樣更高層次的信息已經丟失了。所以,編程語言的趨勢之一,即是能讓代碼包含更多的「What」,而不是「How」,這樣執行環境即可以更加聰明地去適應當前的執行要求。性能優化
關於聲明式的編程風格,Anders主要提出了兩個方面,第一個方面是DSL(Domain Specific Language,領域特定語言)。DSL不是什麼新鮮的玩意兒,咱們平時常常接觸的SQL,CSS,正則表達式等等都屬於DSL。有的DSL可能更加專一於一個方面,例如Mathematica,LOGO等等。這些語言的目標都是特定的領域,與之相對的則是GPPL(General Purpose Programming Language,通用目的編程語言)。Martin Fowler將DSL分爲外部DSL及內部DSL兩種。外部DSL有本身的特定語法、解析器和詞法分析器等等,它們每每是一種小型的編程語言,甚至不會像GPPL那樣須要源文件。與之相對的則是內部DSL。內部DSL其實更像是種別稱,它表明一類特別API及使用模式。數據結構
XSLT,SQL等等均可以算做是外部DSL。外部DSL通常會直接針對特定的領域設計,而不考慮其餘方面。James Gosling曾經說過:每一個配置文件最終都會變成一門編程語言。一開始您可能只會用它表示一點點東西,慢慢地您便會想要一些規則,而這些規則則變成了表達式,後來您可能還會定義變量,進行條件判斷等等,而最終它就變成了一種奇怪的編程語言,這樣的狀況家常便飯。如今有一些公司也在關注DSL的開發。例如之前在微軟工做的Charles Simonyi提出了Intentional Programming的概念,還有JetBrains公司提供的一個叫作MPS(Meta Programming System)的產品。最近微軟也提出了本身的Oslo項目,而在Eclipse世界裏也有Xtext,因此其實現在在這方面也有很多人在嘗試。因爲外部DSL的獨立性,在某些狀況下也會出現特定的工具,輔助領域專家或是開發人員自己編寫DSL代碼。還有一些DSL會以XML方言的形式提出,利用XML方言的好處在於有很多現成的工具可用,這樣能夠更快地定義本身的語法。架構
而內部DSL,正像以前提到的那樣,它每每只是表明了一系列特別的API及使用模式,例如LINQ查詢語句及Ruby on Rails中的Active Record聲明代碼等等。內部DSL可使用一系列API來「假裝」成一種DSL,它每每會利用一些「流暢化」的技巧,例如像jQuery那樣把一些方法經過「點」鏈接起來,而另外一些也會利用元編程的方式。內部DSL還有一些優點,例如能夠訪問語言中的代碼或變量,以及利用代碼補全,重構等母語言的全部特性。 併發
DSL的可讀性每每很高。例如,要篩選出單價大於20的產品,並對所屬種類進行分組,並降序地列出每組的分類名稱及產品數量。若是是用命令式的編程方式,則多是這樣的:
顯然這些代碼編寫起來須要一點時間,且很難直接看出它的真實目的,換言之「What」幾乎徹底被「How」所代替了。這樣,一個新的程序員必須花費必定時間才能理解這段代碼的目的。但若是使用LINQ,代碼即可以改寫成:
這段代碼更加關注的是「What」而不是「How」,它不會明確地給出過濾的「操做方式」,也沒有涉及到建立字典這樣的細節。這段代碼還能夠利用C# 3.0中內置的DSL,即LINQ查詢語句來改寫:
編譯器會簡單地將LINQ差距語句轉化爲前一種形式。這段代碼只是表現出最終的目的,而不是明確指定作事的方式,這樣即可以很容易地並行執行這段代碼,如使用PINQ則幾乎不須要作出任何修改。
函數式編程
Anders提出的另外一個重要的聲明式編程方式即是函數式編程。函數式編程歷史悠久,它幾乎和編程語言自己同時誕生,如當年的LISP即是個函數式編程語言。除了LISP之外還有其餘許多函數式編程語言,如APL、Haskell、ML等等。關於函數式編程在學術界已經有過許多研究了,大約在5到10年前許多人開始吸取和整理這些研究內容,想要把它們融入更爲通用的編程語言。如今的編程語言,如C#、Python、Ruby、Scala等等,它們都受到了函數式編程語言的影響。
使用命令式編程語言寫程序時,咱們常常會編寫如x = x + 1這樣的語句,此時咱們大量依賴的是可變狀態,或者說是「變量」,它們的值能夠隨程序運行而改變。可變狀態很是強大,但隨之而來的即是被稱爲「反作用」的問題,例如一個無需參數的void方法,它會根據調用次數或是在哪一個線程上進行調用對程序產生影響,它會改變程序內部的狀態,從而影響以後的運行效果。而在函數式編程中則不會出現這個狀況,由於全部的狀態都是不可變的。事實上對函數式編程的討論更像是數學、公式,而不是程序語句,如x = x + 1對於數學家來講,彷佛只是個永不爲真的表達式而已。
函數式編程十分容易並行,由於它在運行時不會修改任何狀態,所以不管多少線程在運行時均可以觀察到正確的結果。假如兩個函數徹底無關,那麼它們是並行仍是順序地執行便沒有什麼區別了。固然,現實中的程序必定是有反作用的,例如向屏幕輸出內容,向Socket傳輸數據等等,所以真實世界中的函數式編程每每都會考慮如何將有反作用的代碼分離出來。函數式編程默認是不可變的,開發人員必須作些額外的事情才能使用可變狀態或是危險的反作用,與之相反,如C#或Java必須使用readonly或是final來作到這一點。此時,使用函數式編程語言時的思惟觀念便會有所不一樣了。
F#是微軟隨VS 2010推出的一門函數式編程語言,它基於OCaml的核心部分,所以是一門強類型編程語言,並支持一些如模式匹配,類型推斷等現代函數式編程語言的特性。在此之上,F#又增長了異步工做流,度量單位等較爲前沿的語言功能。在F#中若是要計算一個列表全部元素之和,也可使用命令式的風格來編寫代碼:
只不過,F#中的一切默認都是不可變的,開發人員須要使用mutable關鍵字來聲明一個可變的狀態。事實上,在F#中更典型作法是:
在數學裏咱們常用遞歸,把一個公式分解成幾個變化的形式,以此進行遞歸的定義。純函數式的代碼其「數學性」較強,若是您分析上面這段代碼,會發現它幾乎就是標準的數學定義。在編程時咱們也使用遞歸的作法,編譯器會設法幫咱們轉化成尾調用或是循環語句。
動態語言與元編程
動態語言不會嚴格區分「編譯時」和「運行時」。對於一些靜態編程語言(如C#),每每是先進行編譯,此時可能會獲得一些編譯期錯誤,而對於動態語言來講這兩個階段便混合在一塊兒了。常見的動態語言有JavaScript,Python,Ruby,LISP等等。動態語言和靜態語言各有一些優點,這也是兩個陣營爭論多年的內容。不過Anders認爲它們各自都有十分重要的優勢,而將來不屬於其中任何一方。他表示,從編程語言發展過程當中能夠觀察到兩種特色正在合併的趨勢,將來應該屬於二者的雜交產物。
許多人認定動態語言執行起來很慢,也沒有類型安全等等。例若有這樣一段代碼:
這段代碼在C#和JavaScript中都是合法的,可是它們的處理方式截然不同。在C#中,編譯器能夠推斷出a和n都是32位整數,則for循環和相加操做都只是簡單的CPU指令,天然效率很高。可是對於JavaScript等動態類型語言來講,var只表明了「一個值」,它能夠是任意類型,所以這裏其實還會包含一個「類型標記」,代表它在運行時是什麼類型的對象。因此二者的區別之一即是,表示一樣的值在動態語言中會有一些額外的開銷,在現在的CPU中,「空間」也意味着「速度」,因此較大的值便須要較長時間進行處理,這裏便損失了一部分效率。此外JavaScript在計算a加i時,那麼必須先查看兩個變量中的類型標記,根據類型選擇出合適的相加操做,而後加載兩個值,最後再進行加法操做,一旦越界了還要利用double。很明顯在這裏也會帶來許多開銷。通常來講,動態語言是使用解釋器來執行的,所以還有一些解釋器須要的二進制碼,把這些性能損失所有加起來之後,便會發現執行代碼時須要10倍到100倍的性能開銷。
不過近幾年出現的一些動態虛擬機或引擎將此類狀況改善了許多。現在大部分的JavaScript引擎使用了JIT編譯器,因而便省下了解釋器的開銷,這樣性能損失便會減少至3到10倍。而在過去的兩三年間,JIT編譯器也變得愈來愈高效,瀏覽器中新一代的適應性JIT編譯器,如TraceMonkey,V8,還有微軟在IE 9中使用的Chakra引擎。這種適應性的JIT編譯器使用了一部分有趣的技術,如Inline Caching、Type Specialization、Hidden Classes、Tracing等等,它們能夠將開銷下降至2到3倍的範圍內,這種效率的提高可謂十分神奇。在Anders看來,JavaScript引擎可能已經接近了性能優化的極限,咱們在效率上能夠提高的空間已經很少。不過他一樣認爲,現在JavaScript語言的性能已經足夠快了,徹底有能力做爲Web客戶端的統治性語言。
動態語言的關鍵之一即是「元編程」,「元編程」其實是「代碼生成」的一種別稱,在平常應用中開發人員其實常常依賴這種作法了。在某些場景下使用動態語言會比靜態語言更加天然一些。例如在C#或Java裏使用ORM時,一種傳統作法是讓代碼生成器去觀察數據庫,並生成一大堆代碼,而後再編譯。而動態語言並無編譯期和執行期的區別,例如在Ruby on Rails中使用ActiveRecord便無須定義各式字段。
Anders談到,他和他的團隊也在努力改進靜態語言的元編程能力,如他們正在實現的「編譯器即服務(Compiler as a Service)」。傳統的編譯器是一個黑盒,一端輸入代碼,而另外一端便會生成.NET程序集等數據,開發人員很難參與或理解它的工做。可是在不少時候,開發人員並不必定須要編譯器來生成程序集,他們須要的是一些樹狀的表現形式,而後對它進行識別和重寫。所以,開發人員可能會愈來愈須要一些開放編譯器功能的API。這麼作可讓靜態類型語言得到許多有用的功能,包括元編程以及可操做的完整對象模型等等。
併發
Anders看來,多核革命的一個有趣之處在於,它會要求併發的思惟方式有所改變。傳統的併發思惟,是在單個CPU上執行多個邏輯任務,使用舊有的分時方式或是時間片模型來執行多個任務。可是現在的併發場景則正好相反,是要將一個邏輯上的任務放在多個CPU上執行。這改變了咱們編寫程序的方式,這意味着對於語言或是API來講,咱們須要有辦法來分解任務,把它拆分紅多個小任務後獨立的執行,而傳統的編程語言中並不關注這點。
使用目前的併發API來完成工做並不容易,好比Thread,ThreadPool,Monitor等等,開發人員很難走的太遠。不過在.NET 4.0中提供了一套強大的框架,即.NET並行擴展(Parallel Extensions),這是一種現代的併發模型,將邏輯上的任務併發與實際使用的的物理模型分離開來。之前的API都是直接處理線程等基礎元素,不過利用.NET並行擴展中的任務並行庫(Task Parallel Library),並行LINQ(Parallel LINQ)以及協調數據結構(Coordination Data Structures)讓開發人員能夠直接關注邏輯上的任務,而沒必要關心它們是如何運行的,或是使用了多少個線程和CPU等等。利用LINQ這樣的DSL也有助於寫出並行的代碼,若是使用普通的for循環配合線程池來實現並行,則開發人員很容易在各類API裏失去方向。
不過事實上,編寫並行的代碼依然很困難,尤爲是要識別出能夠並行的地方。Anders認爲不少時候仍是須要編程語言來關注這方面的事情。好比「隔離性(Isolation)」,即編譯器如何發現這段代碼是獨立的,即可以將其安全地併發執行。某段代碼建立了一個對象,在分享給其餘人以前,咱們對它的改變是安全的,可是一旦將其共享出去之後便徹底不一樣了。所以理想中的類型系統應該能夠跟蹤到這樣的共享,如Linear Types——這在學術界也有一些研究。編程語言也能夠在函數的純潔性(Purity)方面下功夫,如關注某個函數是否有反作用,有些時候編譯器能夠作這方面的檢查,它能夠禁止某些操做,以此保證咱們寫出無反作用的純函數。另外即是不可變性(Immutability),目前的語言,如C#或VB,咱們須要額外的工做才能寫出不可變的代碼。Anders認爲合適的作法應該是在語言層面上更好的支持不可變性。這些都是在併發方面須要考慮的問題。
Anders還提到了他在思考併發語言特性時所遵循的原則:一個語言特性不該該針對某個特定的併發模型,而應該是一種通用的,可用於各類不一樣的併發場景的特性,就像隔離性、純潔性及不可變性那樣。語言擁有這樣的特性以後,就能夠用於構建各類不一樣的API,各類併發方式均可以利用到核心的語言特性。
總結
Anders認爲,對於編程語言來講,如今出現了許多有趣的東西,也是個使人激動的時刻。在過去,大約是1995到2005年,的確能夠說是一個編程語言的黃金時期。當Java出現的時候,編程語言的門檻變得平坦了,一切都是Java,彷佛其餘編程語言都完蛋了,程序設計者也沒什麼可作的。不過你們又逐漸發現,其實這遠沒有結束。如今回顧起來,會發現這段時間又出現了許多有趣的編程語言,這其實也表明了咱們在編程領域上的進步。