天天看點

教你一招看清程式設計語言的發展趨勢及未來方向聲明式程式設計與DSL函數式程式設計動态語言與元程式設計并發總結

程式設計離不開程式設計語言,但是程式設計語言在國内的大環境中似乎一直是個二等公民。國内的計算機教育和工程教育訓練,似乎一直在宣傳“語言不重要,重要的是思想”,“語言一通百通”等觀點,甚至在許多人眼中“語言的讨論”完全是不入流的,但其實“程式設計語言”與“工具”、“架構”或是“開發方法”等事物一樣,都對生産力有着重要的影響。事實上,語言的發展曆史比其他方面更為悠久,并且在過去十幾年,甚至最近幾年中都依然在不斷的碰撞,演變。期間一些新的語言誕生了,而另一些在當時看來陽春白雪的語言和程式設計範式也重新獲得了人們的重視。

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包含了無數強大的功能,例如文法提示,重構,調試器等等。與此相比,程式設計語言的改進的确很不明顯。

教你一招看清程式設計語言的發展趨勢及未來方向聲明式程式設計與DSL函數式程式設計動态語言與元程式設計并發總結

在過去5、60年的程式設計曆史中,程式設計語言的抽象級别不斷提高,人們都在努力讓程式設計語言更有表現力,這樣我們可以用更少的代碼完成更多的工作。我們一開始使用彙編,然後使用面向過程的語言(如Pascal和C),然後是面向對象語言(如C++),随後便進入了托管時代,語言運作于受托管的執行環境上(如C#,Java),它們的主要特性有自動的垃圾收集,類型安全等等。Anders認為這樣的趨勢還會繼續保持下去,我們還會看到抽象級别越來越高的語言,而語言的設計者則必須了解并預測下一個抽象級别是什麼樣子的。另一方面,如.NET,Java等架構的重要性提高了許多,程式設計語言往往都傾向于建構于現有的工具上,而不會從頭寫起。現在出現的程式設計語言,例如F#,以及Java領域的Scala,Clojure等等,它們都是基于現有架構建構的,每次從頭開始的代價實在太高。

在Anders眼中,如今影響力較大的趨勢主要有三種(如下圖),它們分别是“聲明式的程式設計風格”(包括“領域特定語言”及“函數式程式設計”)、過去的五年非常火熱的“動态語言”(其最重要的方面便是“元程式設計”能力)以及多核環境下的“并發程式設計。此外随着語言的發展,原本常用的“面向對象”語言,“動态語言”或是“函數式”等邊界也變得越來越模糊,例如各種主要的程式設計語言都受到函數式語言的影響。是以,“多範式”程式設計語言也是一個愈發明顯的趨勢。

教你一招看清程式設計語言的發展趨勢及未來方向聲明式程式設計與DSL函數式程式設計動态語言與元程式設計并發總結

聲明式程式設計與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的産品,并對所屬種類進行分組,并降序地列出每組的分類名稱及産品數量。如果是用指令式的程式設計方式,則可能是這樣的:

Dictionary<string, Grouping> groups = new Dictionary<string, Grouping>();
foreach (Product p in products)
{
    if (p.UnitPrice >= 20)
    {
        if (!groups.ContainsKey(p.CategoryName))
        {
            Grouping r = new Grouping();
            r.CategoryName = p.CategoryName;
            r.ProductCount = 0;
            groups[p.CategoryName] = r;
        }
        groups[p.CategoryName].ProductCount++;
    }
}

List<Grouping> result = new List<Grouping>(groups.Values);
result.Sort(delegate(Grouping x, Grouping y)
{
    return
        x.ProductCount > y.ProductCount ? -1 :
        x.ProductCount < y.ProductCount ? 1 :
        0;
});
           

顯然這些代碼編寫起來需要一點時間,且很難直接看出它的真實目的,換言之“What”幾乎完全被“How”所代替了。這樣,一個新的程式員必須花費一定時間才能了解這段代碼的目的。但如果使用LINQ,代碼便可以改寫成:

var result = products
    .Where(p => p.UnitPrice >= 20)
    .GroupBy(p => p.CategoryName)
    .OrderByDescending(g => g.Count())
    .Select(g => new { CategoryName = g.Key, ProductCount = g.Count() });
           

這段代碼更加關注的是“What”而不是“How”,它不會明确地給出過濾的“操作方式”,也沒有涉及到建立字典這樣的細節。這段代碼還可以利用C# 3.0中内置的DSL,即LINQ查詢語句來改寫:

var result =
    from p in products
    where p.UnitPrice >= 20
    group p by p.CategoryName into g
    orderby g.Count() descending
    select new { CategoryName = g.Key, ProductCount = g.Count() };
           

編譯器會簡單地将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#中如果要計算一個清單所有元素之和,也可以使用指令式的風格來編寫代碼:

let sumSquaresI l = 
    let mutable acc = 0
    for x in l do
        acc <- acc + sqr x
    acc
           

隻不過,F#中的一切預設都是不可變的,開發人員需要使用mutable關鍵字來聲明一個可變的狀态。事實上,在F#中更典型做法是:

let rec sumSquaresF l = 
    match l with
    | [] -> 0
    | head :: tail -> sqr head + sumSquaresF tail
           

在數學裡我們經常使用遞歸,把一個公式分解成幾個變化的形式,以此進行遞歸的定義。純函數式的代碼其“數學性”較強,如果您分析上面這段代碼,會發現它幾乎就是标準的數學定義。在程式設計時我們也使用遞歸的做法,編譯器會設法幫我們轉化成尾調用或是循環語句。

動态語言與元程式設計

動态語言不會嚴格區分“編譯時”和“運作時”。對于一些靜态程式設計語言(如C#),往往是先進行編譯,此時可能會得到一些編譯期錯誤,而對于動态語言來說這兩個階段便混合在一起了。常見的動态語言有JavaScript,Python,Ruby,LISP等等。動态語言和靜态語言各有一些優勢,這也是兩個陣營争論多年的内容。不過Anders認為它們各自都有十分重要的優點,而未來不屬于其中任何一方。他表示,從程式設計語言發展過程中可以觀察到兩種特點正在合并的趨勢,未來應該屬于兩者的雜交産物。

許多人認定動态語言執行起來很慢,也沒有類型安全等等。例如有這樣一段代碼:

var a = 0, n = 10;
for (var i = 0; i < n; i++) {
    a += i;
}
           

這段代碼在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認為合适的做法應該是在語言層面上更好的支援不可變性。這些都是在并發方面需要考慮的問題。

教你一招看清程式設計語言的發展趨勢及未來方向聲明式程式設計與DSL函數式程式設計動态語言與元程式設計并發總結

Anders還提到了他在思考并發語言特性時所遵循的原則:一個語言特性不應該針對某個特定的并發模型,而應該是一種通用的,可用于各種不同的并發場景的特性,就像隔離性、純潔性及不可變性那樣。語言擁有這樣的特性之後,就可以用于建構各種不同的API,各種并發方式都可以利用到核心的語言特性。如果大家對Python感興趣的話,可以加一下我們的學習交流摳摳群哦:649825285,免費領取一套學習資料和視訊課程喲~

總結

Anders認為,對于程式設計語言來說,現在出現了許多有趣的東西,也是個令人激動的時刻。在過去,大約是1995到2005年,的确可以說是一個程式設計語言的黃金時期。當Java出現的時候,程式設計語言的門檻變得平坦了,一切都是Java,似乎其他程式設計語言都完蛋了,程式設計者也沒什麼可做的。不過大家又逐漸發現,其實這遠沒有結束。現在回顧起來,會發現這段時間又出現了許多有趣的程式設計語言,這其實也代表了我們在程式設計領域上的進步。