這是Anders Hejlsberg在比利時TechDays 2010所作的開場演講。javascript
編程語言的發展很是緩慢,期間也固然出現了一些東西,例如面向對象等等,你可能會想,那麼我麼這麼多年的努力都到哪裏去了呢?事實上這些努力沒有體如今編程語言上,而是出如今框架及工具等方面了。若是你關注現在咱們使用的框架,它們的體積的確有很大的增加。例如當年Turbo Pascal所帶的框架大約有,好比說100個功能,而如今的.NET Framework裏則有一萬個類,十萬個方法,的確有1000倍的增加。與此相似,若是你觀察如今的IDE,咱們如今已經有了無數強大的功能,例如語法提示,重構,調試器,探測器等等,這方面的新東西有不少。與此相比,編程語言的改進的確很不明顯。java
另外一方面,如.NET,Java等框架的重要性提升了許多。而編程語言每每都傾向於構建於現有的工具上,而不會從頭寫起。如今出現的編程語言,例如F#,若是你關注Java領域那麼還有Scala,Clojure等等,它們都是基於現有框架構建的。如今已經有太多東西能夠直接利用了,每次從頭開始的代價實在過高。jquery
還有件事,即是在過去五、60年的編程歷史中,咱們都不斷地提升抽象級別,咱們都在不斷地讓編程語言更有表現力,讓咱們能夠用更少的代碼完成更多的工做。咱們一開始先使用匯編,而後使用面向過程的語言,例如Pascal和C,而後即是面嚮對象語言,如C++,隨後就進入了託管時代──受託管的執行環境,例如.NET,Java,它們的主要特性有自動的垃圾收集,類型安全等等。我目前尚未看出這樣的趨勢有中止的跡象,所以咱們還會看到抽象級別愈來愈高的語言,而語言的設計者則必須理解並預測下一個抽象級別是什麼樣子的。程序員
咱們會愈來愈多地使用聲明式的編程風格。這裏我主要會提到例如DSL(Domain Specific Language,領域特定語言)以及函數式編程。而後在過去的五年裏,我發現對於動態語言的研究變得很是火熱,其中對咱們產生重大影響的無疑是動態語言所擁有的良好的元編程能力,還有一些很是有趣的東西,例如JavaScript引擎的發展。而後即是併發編程,不管咱們願不肯意,多核的產生都在迫使咱們不得不重視併發編程。web
有一點值得一提,那即是隨着語言的發展,本來的編程語言分類方式也要有所改變了。之前咱們常常說面嚮對象語言,動態語言或是函數式語言。可是咱們如今發現,這些邊界變得愈來愈模糊,常常會互相學習各自的範式。靜態語言中出現了動態類型,動態語言裏也出現了靜態能力,而現在全部主要的編程語言都受到函數式語言的影響。所以,一個愈來愈明顯的趨勢是「多範式程序設計語言」。正則表達式
目前咱們在編寫軟件時大量使用的是命令式(Imperative)編程語言,例如C#,Java或是C++等等。這些語言的特徵在於,寫出的代碼除了表現出「什麼(What)」是你想作的事情以外,更多的代碼則表現出實現的細節,也就是「如何(How)」完成工做。這部分代碼有時候多到掩蓋了咱們原來問題的解決方案。好比,你會在代碼裏寫for循環,if語句,a等於b,i加一等等,這體現出機器是如何處理數據。首先,這種作法讓代碼變得冗餘,並且它也很難讓執行代碼的基礎設施更聰明地判斷該如何去執行代碼。當你寫出這樣的命令是代碼,而後把編譯後的中間語言交給虛擬機去執行,此時虛擬機並無多少空間能夠影響代碼的執行方式,它只能根據指令一條一條老老實實地去執行。例如,咱們如今想要並行地執行程序就很困難了,由於更高層次的一些信息已經丟失了。這樣,咱們只能在代碼裏給出「How」,而不能體現出「What」的信息。數據庫
有多種方式能夠將「What」轉化爲更爲「聲明式」的編程風格,咱們只要可以在代碼中體現出更多「What」,而不是「How」的信息,這樣執行環境即可以更加聰明地去適應當前的執行要求。例如,它能夠決定投入多少CPU進行計算,你的當前硬件是什麼樣的,等等。編程
如今有兩種比較重要的成果,一是DSL(Domain Specific Language,領域特定語言),另外一個則是函數式編程。瀏覽器
其實DSL不是什麼新鮮的玩意兒,咱們平時一直在用相似的東西,好比,SQL,CSS,正則表達式,有的可能更加專一於一個方面,例如Mathematica,LOGO等等。這些語言的目標都是特定的領域,與之相對的則是GPPL(General Purpose Programming Language,通用目的編程語言)。緩存
Martin Fowler提出DSL應該分爲外部DSL及內部DSL兩種,我認爲這種劃分方式仍是比較有意義的。外部DSL是自我包含的語言,它們有本身特定語法、解析器和詞法分析器等等,它每每是一種小型的編程語言,甚至不會像GPPL那樣須要源文件。與之相對的則是內部DSL。內部DSL其實更像是種別稱,它表明一類特別API及使用模式。
這些是咱們平時會遇到的一些外部DSL,如這張幻燈片上表現的XSLT,SQL或是Unix腳本。外部DSL的特色是,你在構建這種DSL時,其實扮演的是編程語言設計者的角色,這個工做並不會交給普通人去作。外部DSL通常會直接針對特定的領域設計,而不考慮其餘東西。James Gosling曾經說過這樣的話,每一個配置文件最終都會變成一門編程語言。你一開始可能只會用它表示一點點東西,而後慢慢你便會想要一些規則,而這些規則則變成了表達式,可能你還會定義變量,進行條件判斷等等。而最終它就變成了一種奇怪的編程語言,這樣的狀況家常便飯。
事實上,如今有一些公司也在關注DSL的開發。例如之前在微軟工做的Charles Simonyi提出了Intentional Programming的概念,還有一個叫作JetBrains的公司提供一個叫作MPS(Meta Programming System)的產品。最近微軟也提出了本身的Oslo項目,而在Eclipse世界裏也有個叫作Xtext的東西,因此其實在這方面如今也有很多人在嘗試。
我在觀察外部DSL時,每每會關注它的語法到底提供了多少空間,例如一種XML的方言,利用XML方言的好處在於有很多現成的工具可用,這樣能夠更快地定義本身的語法。
而內部DSL,正像我以前說的那樣,它其實只是一系列特別的API及使用模式的別稱。這裏則是一些LINQ查詢語句,Ruby on Rails以及jQuery代碼。內部DSL的特色是,它其實只是一系列API,可是你能夠「僞裝」它們一種DSL。內部DSL每每會利用一些「流暢化」的技巧,例如像這裏的LINQ或jQuery那樣把一些方法經過「點」鏈接起來。有些則利用了元編程的方式,如這裏的Ruby on Rails就涉及到了一些元編程。這種DSL能夠訪問語言中的代碼或變量,以及利用如代碼補全,重構等母語言的全部特性。
如今我會花幾分鐘時間演示一下我所建立的DSL,也就是LINQ。我相信大家也已經用過很多LINQ了,不過這裏我仍是快速的展現一下我所表達的更爲「聲明式」的編程方式。
public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public string CategoryName { get; set; }
public int UnitPrice { get; set; }
public static List<Product> GetProducts() { /* ... */ }
}
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
List<Product> products = Product.GetProducts();
List<Product> result = new List<Product>();
foreach (Product p in products)
{
if (p.UnitPrice > 20) result.Add(p);
}
GridView1.DataSource = result;
GridView1.DataBind();
}
}
這裏有許多Product對象,那麼如今我要篩選出全部單價大於20的那些, 再把他們顯示在一個GridView中。傳統的作法就是這樣,我先獲得全部的Product對象,而後foreach遍歷每一個對象,再判斷每一個對象的單價,最終把數據綁定到GridView裏。運行這個程序……(打開頁面)這就是就能獲得結果。
好,那麼如今我要作一些稍微複雜的事情。可能我不是要展現單價超過20的Product對象,而是要查看每一個分類中究竟有多少個單價超過20的對象,而後根據數量進行排序。若是不用DSL完成這個工做,那麼我可能會先定義一個對象來表示結果:
class Grouping
{
public string CategoryName { get; set; }
public int ProductCount { get; set; }
}
這是個表示分組的對象,用於保存分類的名稱和產品數量。而後咱們就會寫一些十分醜陋的代碼:
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;
});
我先建立一個新的字典,用於保存分類名稱到分組的對應關係。而後我遍歷每一個Product對象,對於每一個單價大於20的對象,若是字典中尚未保存對應的分組則建立一個,而後將數量加一。而後爲了排序,我調用Sort方法,因而我要提供一個委託做爲排序方法,而後blablablabla……執行以後……(打開頁面)我天然能夠獲得想要的結果。
可是,首先這些代碼寫起來須要花費一些時間,很顯然。而後仔細觀察,你會發現這寫代碼幾乎都是在表示「How」,而「What」基本已經丟失了。假設我離開了,如今新來了一個程序員要維護這段代碼,他會須要一點時間才能完整理解這段代碼,由於他沒法直接看清代碼的目標。
不過若是這裏咱們使用DSL,也就是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() });
products……先調用Where……blablabla……再GroupBy等等。因爲咱們這裏能夠使用DSL來表示高階的術語,用以體現咱們想作的事情。因而這段代碼則更加關注於「What」而不是「How」。我這裏不會明確地指示我想要過濾的方式,我也不會明確地說我要創建字典和分類,這樣基礎結構就能夠聰明地,或者說更加聰明地去肯定具體的執行方式。你可能比較容易想到咱們能夠並行地執行這段代碼,由於我沒有顯式地指定作事方式,我只是表示出個人意圖。
咱們打開頁面……(打開頁面)很顯然咱們獲得了相同的結果。
這裏比較有趣的是,內部DSL是如何設計進C#語法中的,爲此咱們爲C# 3.0添加了一系列的特性,例如Lambda表達式,擴展方法,類型推斷等等。這些特性統一塊兒來以後,咱們就能夠設計出更爲豐富的API,組合以後便成爲一種內部DSL,就像這裏的LINQ查詢語言。
除了使用API的形式以外,咱們還能夠這樣作:
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() };
編譯器會簡單地將這種形式轉化爲前一種形式。不過,這裏我認爲有意思的地方在於,你徹底能夠建立一門和領域編程語言徹底無關的語法,而後等這種語法和API變得流行且豐富起來以後,再來創一種新的表現形式,就如這裏的LINQ查詢語法。我頗爲中意這種語言設計的交流方式。
關於聲明式編程的還有一部分重要的內容,那即是函數式編程。函數式編程已經有很長時間的歷史了,當年LISP即是個函數式編程語言。除了LISP之外咱們還有其餘許多函數式編程語言,如APL、Haskell、Scheme、ML等等。關於函數式編程在學術界已經有過許多研究了,在大約5到10年前許多人開始吸取和整理這些研究內容,想要把它們融入更爲通用的編程語言。如今的編程語言,如C#、Python、Ruby、Scala等等,它們都受到了函數式編程語言的影響。
我想在這裏先花幾分鐘時間簡單介紹一下我眼中的函數式編程語言。我發現不少人據說過函數式編程語言,但還不十分清楚它們和普通的命令式編程語言究竟有什麼區別。現在咱們在使用命令式編程語言寫程序時,咱們常常會寫這樣的語句,嗨,x等於x加一,此時咱們大量依賴的是狀態,可變的狀態,或者說變量,它們的值能夠隨程序運行而改變。
可變狀態很是強大,但隨之而來的即是叫作「反作用」的問題。在使用可變狀態時,你的程序則會包含反作用,好比你會寫一個無需參數的void方法,而後它會根據你的調用次數或是在哪一個線程上進行調用對程序產生影響,由於void方法會改變程序內部的狀態,從而影響以後的運行效果。
而在函數式編程中則不會出現這個狀況,由於全部的狀態都是不可變的。你能夠聲明一個狀態,可是不能改變這個狀態。並且因爲你沒法改變它,因此在函數式編程中不須要變量。事實上對函數式編程的討論更像是數學、公式,而不像是程序語句。若是你把x = x + 1這句話交給一個程序員看,他會說「啊,你在增長x的值」,而若是你把它交給一個數學家看,他會說「嗯,我知道這不是true」。
然而,若是你給他看這條語言,他會說「啊,y等於x加一,就是把x + 1的計算結果交給y,你是爲這個計算指定了一個名字」。這時候在思考時就是另外一種方式了,這裏y不是一個變量,它只是x + 1的名稱,它不會改變,永遠表明了x + 1。
因此在函數式編程語言中,當你寫了一個函數,接受一些參數,那麼當你調用這個函數時,影響函數調用的只是你傳進去的參數,而你獲得的也只是計算結果。在一個純函數式編程語言中,函數在計算時不會對進行一些神奇的改變,它只會使用你給它的參數,而後返回結果。在函數式編程語言中,一個void方法是沒有意義的,它惟一的做用只是讓你的CPU發熱,而不能給你任何東西,也不會有反作用。固然如今你可能會說,這個CPU發多少熱也是一個反作用,好吧,不過咱們如今先不討論這個問題。
這裏的關鍵在於,你解決問題的方法和之前大不同了。我這裏仍是用代碼來講明問題。使用函數式語言寫沒有反作用的代碼,就比如在Java或C#中使用final或是readonly的成員。
例如這裏,咱們有一個Point類,構造函數接受x和y,還有一個MoveBy方法,能夠把一個點移動一些位置。 在傳統的命令式編程中,咱們會改變Point實例的狀態,這麼作在平時可能不會有什麼問題。可是,若是我把一個Point對象同時交給3個API使用,而後我修改了Point,那麼如何才能告訴它們狀態改變了呢?可能咱們能夠使用事件,blablabla,若是咱們沒有事件,那麼就會出現那些不愉快的反作用了。
那麼使用函數式編程的形式寫代碼,你的Point類仍是能夠包含狀態,例如x和y,不過它們是readonly的,一旦初始化之後就不能改變了。MoveBy方法不能改變Point對象,它只能建立一個新的Point對象並返回出來。這就是一個建立新Point對象的函數,不是嗎?這樣就可讓調用者來決定是使用新的仍是舊的Point對象,但這裏不會有產生反作用的狀況出現。
在函數式編程裏天然不會只有Point對象,例如咱們會有集合,如Dictionary,Map,List等等,它們都是不可變的。在函數式編程中,當咱們向一個List裏添加元素時,咱們會獲得一個新的List,它包含了新增的元素,但以前的List依然存在。因此這些數據結構的實現方式是有根本性區別的,它們的內部結構會設法讓這類操做變的儘量高效。
在函數式編程中訪問狀態是十分安全的,由於狀態不會改變,我能夠把一個Point或List對象交給任意多的地方去訪問,徹底不用擔憂反作用。函數式編程的十分容易並行,由於我在運行時不會修改狀態,所以不管多少線程在運行時均可以觀察到正確的狀態。兩個函數徹底無關,所以它們是並行仍是順序地執行便沒有什麼區別了。咱們還能夠有延遲計算,能夠進行Memorization,這些都是函數式編程中十分有趣的方面。
你可能會說,那麼咱們爲何不都用這種方法來寫程序呢?嗯,最終,就像我以前說的那樣,咱們不能只讓CPU發熱,咱們必需要把計算結果表現出來。那麼咱們在屏幕上打印內容時,或者把數據寫入文件或是Socket時,其實就產生了反作用。所以真實世界中的函數式編程,每每都是把純粹的部分進行隔離,或是進行更細緻的控制。事實上也不會有真正純粹的函數式編程語言,它們都會帶來必定的反作用或是命令式編程的能力。可是,它們默認是函數式的,例如在函數式編程語言中,全部東西默認都是不可變的,你必須作些額外的事情才能使用可變狀態或是產生危險的反作用。此時你的編程觀念便會有所不一樣了。
咱們在本身的環境中開發出了這樣一個函數式編程語言,F#,已經包含在VS 2010中了。F#誕生於微軟劍橋研究院,由Don Syme提出,他在F#上已經工做了5到10年了。F#使用了另外一個函數式編程語言OCaml的常見核心部分,所以它是一個強類型語言,並支持一些如模式匹配,類型推斷等現代函數式編程語言的特性。在此之上,F#又增長了異步工做流,度量單位等較爲前沿的語言功能。
而F#最爲重要的一點多是,在我看來,它是第一個和工業級的框架和工具集,如.NET和Visual Studio,有深刻集成的函數式編程語言。F#容許你使用整個.NET框架,它和C#也有相似的執行期特徵,例如強類型,並且都會生成高效的代碼等等。我想,如今應該是展現一些F#代碼的時候了。
首先我想先從F#中我最喜歡的特性講起,這是個F#命令行……(打開命令行窗口以及一個F#源文件)……F#包含了一個交互式的命令行,這容許你直接輸入代碼並執行。例如輸入5……x等於5……而後x……顯示出x的值是5。而後讓sqr x等於x乘以x,因而我這裏定義了一個簡單的函數,名爲sqr。因而咱們就能夠計算sqr 5等於25,sqr 10等於100。
F#的使用方式十分動態,但事實上它是一個強類型的編程語言。咱們再來看看這裏。這裏我定義了一個計算平方和的函數sumSquares,它會遍歷每一個列表中每一個元素,平方後再把它們相加。讓我先用命令式的方式編寫這個函數,再使用函數式的方式,這樣你能夠看出其中的區別。
let sumSquaresI l =
let mutable acc = 0
for x in l do
acc <- acc + sqr x
acc
這裏先是命令式的代碼,咱們先建立一個累加器acc爲0,而後遍歷列表l,把平方加到acc中,而後最後我返回acc。有幾件事情值得注意,首先爲了建立一個可變的狀態,我必須顯式地使用mutable進行聲明,在默認狀況下這是不可變的。
還有一點,這段代碼裏我沒有提供任何的類型信息。當我把鼠標停留在方法上時,就會顯示sumSquaresI方法接受一個int序列做爲參數並返回一個int。你可能會想int是哪裏來的,嗯,它是由類型推斷而來的。編譯器從這裏的0發現acc必須是一個int,因而它發現這裏的加號表示兩個int的相加,因而sqr函數返回的是個int,再接下來blablabla……最終它發現這裏處處都是int。
若是我把這裏修改成浮點數0.0,鼠標再停留一下,你就會發現這個函數接受和返回的類型都變成float了。因此這裏的類型推斷功能十分強大,也十分方便。
如今我能夠選擇這個函數,讓它在命令行裏執行,而後調用sumSquaresI,提供1到100的序列,就能獲得結果了。
let rec sumSquaresF l =
match l with
| [] -> 0
| h :: t -> sqr h + sumSquaresF t
那麼如今咱們來換一種函數式的風格。這裏是另外一種寫法,能夠說是純函數式的實現方式。若是你去理解這段代碼,你會發現有很多數學的感受。這裏我定義了sumSqauresF函數,輸入一個l列表,而後使用下面的模式去匹配l。若是它爲空,則結果爲0,不然把列表匹配爲頭部和尾部,而後便將頭部的平方和尾部的平方和相加。
你會發現,在計算時我不會去改變任何一個變量的值,我只是建立新的值。我這裏會使用遞歸,就像在數學裏咱們常用遞歸,把一個公式分解成幾個變化的形式,以此進行遞歸的定義。在編程時咱們也使用遞歸的作法,而後編譯器會設法幫咱們轉化成尾遞歸或是循環等等。
因而咱們即可以執行sumSquaresF函數,也能夠獲得相同的結果。固然實際上可能你並不會像以前這樣寫代碼,你可能會使用高階函數:
let sumSquares l = Seq.sum (Seq.map (fun x -> x * x) l )
例如這裏,我只是把函數x乘以x映射到列表上,而後相加。這樣也能夠獲得相同的結果,並且這多是更典型的作法。我這裏只是想說明,這個語言在編程時可能會給你帶來徹底不一樣的感覺,雖然它的執行期特徵和C#比較接近。
我下面繼續要講的是動態語言,這也是我以前提到的三種趨勢之一。
我仍是嘗試着去找到動態語言的定義,可是你也知道……通常地說,動態語言是一些不對編譯時和運行時進行嚴格區分的語言。這不像一些靜態編程語言,好比C#,你先進行編譯,而後會獲得一些編譯期錯誤,稍後再執行,而對於動態語言來講這兩個階段便混合在一塊兒了。咱們都熟悉一些動態語言,好比JavaScript,Python,Ruby,LISP等等。
動態語言有一些優點,而靜態語言也有着另外一些優點,這也是兩個陣營爭論多年的內容。老實講,我認爲結果不是二者中的任意一個,它們都有各自十分重要的優勢,而長期來看,我認爲結果應該是二者的雜交產物,我認爲在語言發展中也能夠看到這樣的趨勢,這兩部份內容正在合併。
許多人認定動態語言執行起來很慢,也沒有類型安全等等。我想在這裏觀察並比較一下,到底是什麼緣由會讓靜態語言和動態語言在這方面有不一樣的性質。這裏有一段有趣的代碼,它的語法在JavaScript和C#裏都是正確的,這樣咱們便能比較兩種語言是如何處理這段代碼的。
首先咱們把它看做是一段C#代碼,它只是用for循環把一堆整數相加,你確定不會這麼作,這只是一個示例。在C#中,當咱們使用var關鍵字時,它表示「請爲我推斷這裏的類型」,因此在這裏a和i的類型都是int。
這斷代碼在執行的時候,這兩個值都是32位整數,而for循環只是簡單的使用ADD指令便可,執行起來天然效率很高。
但若是從JavaScript或是動態語言的角度來看……或者說對於動態類型的語言來講,var只表明了「一個值」,它能夠是任意類型,咱們不知道它到底是什麼。因此當咱們使用var a或var i時,咱們只是定義了兩個值,其中包含了一個「類型」標記,代表在運行時它是個什麼類型。在這裏它是一個int,所以包含了存儲int值的空間。但有些時候,例如要存儲一個double值,那麼可能便須要更多的空間,還多是一個字符串,因而便包含一個引用。
因此二者的區別之一即是,表示一樣的值在動態語言中會有一些額外的開銷,代價較高。而在現在的CPU中,「空間」便等於「速度」,因此較大的值便須要較長時間進行處理,這裏便損失了一部分效率。
在JavaScript中,咱們若是要處理a加i,那麼便不只僅是一個ADD指令。首先它必須查看兩個變量中的類型標記,而後根據類型選擇合適的相加操做。因而再去加載兩個值,而後再進行加法操做。這裏還須要進行越界檢查,由於在JavaScript中一旦越界了便要使用double,等等。很明顯在這裏也有許多開銷。通常來講,動態語言是使用解釋器來執行的,所以還有一些解釋器須要的二進制碼。你把這些開銷所有加起來之後,便會發現執行代碼時須要10倍到100倍的開銷。
不過因爲近幾年來出現的一些動態虛擬機或引擎,目前這些狀況改善了許多。比方說,這是傳統的狀況(上圖左),如在IE 6或IE 7裏使用的很是緩慢的解釋器。目前的狀況是,大部分的JavaScript引擎使用了JIT編譯器(上圖中),因而便省下了解釋器的開銷,這樣性能損失便會減少至3到10倍。而在過去的兩三年間,JIT編譯器也變得愈來愈高效,瀏覽器中新一代的適應性JIT編譯器(上圖右),如TraceMonkey,V8,還有現在微軟在IE 9中使用的Chakra引擎。這種適應性的JIT編譯器使用了一部分有趣的技術,如Inline Caching、Type Specialization、Hidden Classes、Tracing等等,它們能夠將開銷下降至2到3倍的範圍內,這種效率的提高可謂十分神奇。
在我看來,JavaScript引擎可能已經接近了性能優化的極限,咱們在效率上能夠提高的空間已經很少。不過我一樣認爲,現在JavaScript語言的性能已經足夠快了,徹底有能力統治Web客戶端。
有人認爲,JavaScript歷來不是一種適合進行大規模編程的語言。現在也有一些有趣的工具,如Google Web Tookit,在微軟Nikhil Kothari也建立了Script#,讓你能夠編寫C#或Java代碼,而後將代碼編譯成JavaScript,這就像是將JavaScript看成是一種中間語言。Google Wave的全部代碼都用GWT寫成,它的團隊堅持認爲用JavaScript不可能完成這樣的工做,由於複雜度實在過高了。現在在這方面還有一些有趣的開發成果,我不清楚何時會結束。不過我認爲,這些都不算是大規模的JavaScript開發方案,而編寫C#或Java代碼再生成JavaScript的方式也不能算是徹底正確的作法。咱們能夠關注這方面的走向。
在.NET 4.0的運行時進行動態編程時,咱們引入了一個新功能:動態語言運行時。能夠這樣理解,CLR的目的是爲靜態類型的編程語言提供一個統一的框架或編程模型,而DLR即是在.NET平臺上爲動態語言提供了統一的編程模型。CLR自己已經有一些支持動態編程能力,如反射,Emit等等。不過在.NET上實現動態語言的時候,總會一遍又一遍地去實現某些功能,還有如動態語言如何與靜態語言進行交互,這些都由DLR來提供。DLR的特性包含了,如表達式樹、動態分發、Call Site緩存,這能夠提升動態代碼的執行效率。
在.NET 4.0中咱們使用了DLR,不只僅是IronPython和IronRuby,還有C# 4和VB.NET 10,它們使用DLR實現動態分發功能。所以咱們共享了語言的動態能力實現方式,因而這些語言之間能夠輕鬆地進行交互。一樣咱們能夠與其餘多樣性的技術進行交互,例如使用JavaScript操做Silverlight的DOM,或是與Ruby、Python代碼溝通,甚至用來控制Office等自動化服務。
動態語言的另外一個關鍵和有趣之處在於「元編程」。「元編程」其實是「代碼生成」的一種別稱,其實在平常應用中咱們也常常依賴這種作法。觀察動態語言適合元編程的緣由也是件十分有趣的事情。
在這個藍框中是一段Ruby on Rails代碼(見上圖)。簡單地說,這裏定義了一個Order類,繼承了ActiveRecord,也定義了一些關係,如belongs_to和has_many關係。Ruby這種動態語言的關鍵之處,在於一切事物都是經過執行而獲得的,包括類型聲明。好比這裏的類型申明執行了belongs_to和has_many方法的調用,執行belongs_to會截獲一對多或一對一關係所須要的信息,所以在這裏語言是在運行的時候,動態爲自身生成了代碼。
實現這點在動態語言裏天然會更容易一些,由於它們沒有編譯期和執行期的區別。靜態類型語言在這方面會比較困難。例如在C#或Java裏使用ORM時,傳統的作法是讓代碼生成器去觀察數據庫,生成一大堆代碼,而後再編譯,有些複雜。不過我時常想着去改善這一點。
其中一種作法,是咱們正在努力實現的「編譯器即服務」,我如今先對它進行一些簡單的介紹。傳統的編譯器像是一個黑盒,你在一端輸入代碼,而另外一端便會生成.NET程序集或是對象代碼等等。而這個黑盒卻很神祕,你目前很難參與或理解它的工做。
你能夠想象,一些代碼每每是不包含在源文件中的。若是你想要交互式編程的體驗,例如一個交互式的提示符,那麼代碼不是保存在源文件中而是由用戶輸入的。若是您在實現一個DSL,例如Windows Workflow或是Biztalk,則可能用C#或VB實現了一些須要動態執行的規則,它們也不是保存在源文件中,而多是放在XML屬性中的。此時你想編譯它們卻作不到,你仍是要把它們放入源文件,這就變的複雜了。
另外一方面,對於編譯器來講,咱們不必定須要它生成程序集,有時候須要的是一些樹狀的表現形式。例如一些由用戶反射生成的代碼,即可能不要程序集而是一個解析樹,而後能夠對它進行識別和重寫。所以,咱們可能愈來愈須要的是一些API,以此開放編譯器的功能。
例如,你能夠給它一小段代碼,讓它返回一段可執行的程序,或是一個能夠識別或重寫的解析樹。這麼作可讓靜態類型語言得到許多有用的功能,例如元編程,以及可操做的完整的對象模型等等。
好,最後我想談的內容是「併發」。
據說過摩爾定律的請舉手……幾乎是全部人。那麼多少人據說了摩爾定律已經結束了呢?嗯,仍是有不少人。我有好消息,也有壞消息。我認爲摩爾定律並無中止。摩爾定律說的是:能夠在集成電路上低成本地放置晶體管的數目,約每兩年便會增長一倍。有趣的是,這個定律從60年代持續到如今,而從一些跡象上來看,這個定律會繼續保持20到30年。
摩爾定理有個推論,即是說時鐘速度將根據相同的週期提升,也就是說每隔大約24個月,CPU的速度便會加倍──而這點已經中止了。再來統計一下,大家之中有誰的機器裏有20GHz的CPU?看到了沒?一我的都沒有。但若是你從五年前開始計算的話,如今咱們應該已經在使用20GHz的CPU了,但事實並不是如此。這點在五年前就中止了,並且事實上最大速度還有些降低,由於發熱量實在太大了,會消耗許多能源,讓電池用的太快。
有些物理方面的基礎因素讓CPU不能運行的太快。然而,另外一意義上的摩爾定理出現了。咱們仍是能夠看到容量的增長,由於能夠在同一個錶盤上放置多個CPU了。目前已經有了雙核、四核,Intel的CTO在三年前說,十年後咱們能夠出現80核的處理器。
到了那個時候,你的任務管理器中就多是這樣的。彷佛有些嚇人,不過這是咱們實驗室中真實存在的128核機器。你能夠看到,計算能力已經徹底用上了。這即是個問題,好比你在這臺強大的機器上進行一個實驗,你天然但願看到100%的使用情況,不過傳統的實驗都是在一個核上執行的,因此咱們面臨的挑戰是,咱們須要換一種寫程序的方式來利用此類機器。
個人一個同事,Herb Sutter,他寫過一篇文章,談到「免費的午飯已經結束了」。沒錯,咱們已經不能寫一個程序,而後對客戶說:啊,將來的硬件會讓它運行的愈來愈快,咱們不用關心太多──不,已經不會這樣了,除非你換種不一樣的寫法。實話說,這是個挑戰,也是個機遇。說它是個挑戰,是由於併發十分困難,至今咱們對此尚未簡單的答案,稍後我會演示一些正有所改善的東西,但……這也是一個機遇,在這樣的機器上,你的確能夠用完全部的核,這樣便能得到性能提升,不過作法須要有所不一樣。
多核革命的一個有趣之處在於,它對於併發的思惟方式會有所改變。傳統的併發思惟是在單個CPU上執行多個邏輯任務,使用舊有的分時方式、時間片模型來執行多個任務。可是,你想一下便會發現現在的併發狀況正好相反,如今是要將一個邏輯上的任務放在多個CPU上執行。這改變了咱們編寫程序的方式,這意味着對於語言或是API來講,咱們須要有辦法來分解任務,把它拆分紅多個小任務後獨立的執行,而傳統的編程語言中並不關注這點。
使用目前的併發API來完成工做並不容易,好比使用Thread,ThreadPool,lock,Monitor等等,你沒法太好的進展。不過.NET 4.0提供了一些美妙的事物,咱們稱之爲.NET並行擴展。它是一種現代的併發模型,將邏輯上的任務併發與咱們實際使用的的物理模型分離開來。之前咱們的API都是直接處理線程,也就是(上圖)下方橙色的部分,不過有了.NET並行擴展以後,你能夠使用更爲邏輯化的編程風格。任務並行庫(Task Parallel Library),並行LINQ(Parallel LINQ)以及協調數據結構(Coordination Data Structures)讓你能夠直接關注邏輯上的任務,而沒必要關心它們是如何運行的,或是使用了多少個線程和CPU等等。
下面我來簡單演示一下它們的使用方式。我帶來了一個PLINQ演示,這裏是一些代碼,讀取XML文件的內容。這有個50M大小的popname.xml文件,保存了美國社會安全數據庫裏的信息,包含某個洲在某一年的人口統計信息。這個程序會讀取這個XML文件,把它轉化成一系列對象,並存放在一個List中。而後對其執行一個LINQ語句,查找全部在華盛頓名叫Robert的人,再根據年份進行排序:
Console.WriteLine("Loading XML data...");
var popNames =
(from e in XElement.Load("popnames.xml").Elements("Name")
select new
{
Name = (string)e.Attribute("Name"),
State = (string)e.Attribute("State"),
Year = (int)e.Attribute("Year"),
Count = (int)e.Attribute("Count")
})
.ToList();
Console.WriteLine(popNames.Count + " records");
Console.WriteLine();
string targetName = "Robert";
string targetState = "WA";
var querySequential =
from n in popNames
where n.Name == targetName && n.State == targetState
orderby n.Year
select n;
咱們來執行一下……首先加載XML文件,而後進行查詢。利用PLINQ咱們能夠作到並行地查詢。咱們只要拷貝一份代碼……改爲queryParallel……如今我惟一要作的只是在數據源上使用AsParallel擴展方法,這樣便會引入一套新的類型和實現,此時相同的LINQ操做使用的即是並行的實現:
var queryParallel =
from n in popNames.AsParallel()
where n.Name == targetName && n.State == targetState
orderby n.Year
select n;
咱們從新執行兩個查詢。
再次加載XML數據……並行實現使用了1.5秒,咱們再試着運行一次,通常結果會更好一些,如今可能恰好在執行一些後臺任務。通常咱們能夠獲得更快的結果……此次比較接近了。如今你能夠觀察到,咱們並不須要作太多事情,即可以在個人雙核機器上獲得併發的效果。
這裏我沒法保證說,咱們只要隨時加上AsParallel即可以獲得兩倍的性能,有時能夠有時不行,有些查詢可以被並行,有的則不能夠。然而,我想你必定贊成一點,使用如LINQ這樣的DSL可以方便咱們編寫並行的代碼,也更有可能利用起並行效果。雖然不是每次都有效,可是嘗試的成本也很低。若是咱們使用普通的for循環來編寫代碼,在某個地方使用線程池等等,便很容易在這些API裏失去方向。而這裏咱們只要簡單地嘗試一下,便能知道是否能夠提升性能了。
這裏你已經看到我使用的LINQ查詢,而如今也有不少工做是經過循環來完成的。你能夠想象主要的運算是從哪裏來的,很天然會是在循環裏操做數據。若是循環的每一個迭代都是獨立的,便有很大的機會能夠利用併發操做──我知道這裏是「若是」,不過長期來看則必定會出現這樣的狀況。這時候即可以使用並行擴展,或者說是.NET並行擴展裏的新API,把循環轉化成並行的循環,只要簡單的改變……幾乎只要用一樣的循環體把for重構成Parallel.For就好了。若是你有foreach操做就能夠使用Parallel.ForEach,或是一系列順序執行的語句也能夠用上Parallel.Invoke。此時任務並行庫會接管並執行這些任務,根據你的CPU數量使用最優化的線程數量,你不須要關注更深的細節,只須要編寫邏輯就能夠了。
就像我說的那樣,可能你會有獨立的任務但也可能沒有,因此不少時候咱們須要編程語言來關注這方面的事情。好比「隔離性(Isolation)」。例如,編譯器如何發現這段代碼是獨立的,能夠安全地併發執行,比如我建立了一個對象,在分享給其餘人以前,我對它的改變是安全的。可是我一旦把它們共享出去了,那麼它們便不安全了。因此若是咱們的類型系統能夠跟蹤到這樣的共享,如Linear Types──這在學術界也有一些研究。咱們也能夠在函數的純潔性(Purity)方面下功夫,如關注某個函數是否有反作用,有些時候編譯器能夠作這方面的檢查,它能夠禁止某些操做,以此保證咱們寫出純函數。還有即是不可變性(Immutability),目前的C#或VB,咱們須要額外的工做才能寫出不可變的代碼──但本不應這樣,咱們應該在語言層面上更好的支持不可變性。這些都是在併發方面須要考慮的問題。
若是說有哪一個語言特性超出這個範疇,我想說這裏還有一個原則:你不應指望C#中出現某個特別的併發模型,而應該是一種通用的,可用於各類不一樣的併發場景的特性,就像隔離性、純潔性及不可變性那樣。語言擁有這樣的特性以後,就能夠用於構建各類不一樣的API,各類併發方式均可以利用到核心的語言特性。
OK,我想如今已經講的差很少了,我來作個總結吧。
在我看來,對於編程語言來講,如今出現了許多有趣的東西,也是使人激動的時刻。在過去,大約1995-2005年,的確能夠說是一個有些特別的編程語言的黃金時期。你知道,當Java出現的時候,編程語言的門檻變得平坦了,一切都是Java,天啊其餘編程語言都完蛋了,咱們也沒什麼可作的了。而後咱們又逐漸發現,這遠沒有結束,如今回顧起來,會發現又出現了許多有趣的編程語言。我很興奮,由於新語言表明了咱們在編程領域上的進步。
若是要我歸納在將來十年編程語言會變成什麼樣,首先,我認爲編程語言應該變得更加「聲明式」,咱們須要設法爲語言引入一些如元編程,函數式編程的能力,同時可能也要尋找讓用戶有辦法擴展語法,使他們能夠構造領域特定語言等等。我想在十年之後,動態語言和靜態語言的區別也差很少會消失了,這二者會合併爲一種單一的常見的編程範式。在併發方面,語言會採納一些特性,能夠利用起隔離性,函數式的純粹性,以及更好的不可變數據類型的編寫方式。不過整體來講我想強調的是,對於編程語言,新的範式則是「多範式」編程語言。