編程語言的發(fā)展趨勢及未來方向(2):聲明式編程與DSL
這是Anders Hejlsberg(不用介紹這是誰了吧)在比利時TechDays 2010所做的開場演講。由于最近我在博客上關(guān)于語言的討論比較多,出于應(yīng)景,也打算將Anders的演講完整地聽寫出來。在上一部分中,Anders指出語言本身在過去的數(shù)十年里并沒有明顯的發(fā)展,并給出了他眼中編程語言發(fā)展趨勢的預測。在現(xiàn)在的第2部分中,Anders將闡述聲明式編程的理念及DSL,并演示C#中一種內(nèi)部DSL的形式:LINQ。
本文引用地址:http://cafeforensic.com/article/201703/346085.htm如果沒有特別說明,所有的文字都直接翻譯自Anders的演講,并使用我自己的口語習慣表達出來,對于Anders的口誤及反復等情況,必要時在譯文中自然也會進行忽略。為了方便理解,我也會將視頻中關(guān)鍵部分進行截圖,而某些代碼演示則會直接作為文章內(nèi)容發(fā)表。
(聽寫開始,接上篇)
這里先從聲明式(Declarative)編程談起。
目前我們在編寫軟件時大量使用的是命令式(Imperative)編程語言,例如C#,Java或是C++等等。這些語言的特征在于,寫出的代碼除了表現(xiàn)出“什么(What)”是你想做的事情之外,更多的代碼則表現(xiàn)出實現(xiàn)的細節(jié),也就是“如何(How)”完成工作。這部分代碼有時候多到掩蓋了我們原來問題的解決方案。比如,你會在代碼里寫for循環(huán),if語句,a等于b,i加一等等,這體現(xiàn)出機器是如何處理數(shù)據(jù)。首先,這種做法讓代碼變得冗余,而且它也很難讓執(zhí)行代碼的基礎(chǔ)設(shè)施更聰明地判斷該如何去執(zhí)行代碼。當你寫出這樣的命令是代碼,然后把編譯后的中間語言交給虛擬機去執(zhí)行,此時虛擬機并沒有多少空間可以影響代碼的執(zhí)行方式,它只能根據(jù)指令一條一條老老實實地去執(zhí)行。例如,我們現(xiàn)在想要并行地執(zhí)行程序就很困難了,因為更高層次的一些信息已經(jīng)丟失了。這樣,我們只能在代碼里給出“How”,而不能體現(xiàn)出“What”的信息。
有多種方式可以將“What”轉(zhuǎn)化為更為“聲明式”的編程風格,我們只要能夠在代碼中體現(xiàn)出更多“What”,而不是“How”的信息,這樣執(zhí)行環(huán)境便可以更加聰明地去適應(yīng)當前的執(zhí)行要求。例如,它可以決定投入多少CPU進行計算,你的當前硬件是什么樣的,等等。
我之前提到過,現(xiàn)在有兩種比較重要的成果,一是DSL(Domain Specific Language,領(lǐng)域特定語言),另一個則是函數(shù)式編程。
其實DSL不是什么新鮮的玩意兒,我們平時一直在用類似的東西,比如,SQL,CSS,正則表達式,有的可能更加專注于一個方面,例如Mathematica,LOGO等等。這些語言的目標都是特定的領(lǐng)域,與之相對的則是GPPL(General Purpose Programming Language,通用目的編程語言)。
對于DSL而言其實并沒有一個明確的定義,在這里我也不打算為它下個定義,例如UML甚至根本沒有特定的語法。不過我這里會談一些我覺得比較重要的東西。
Martin Fowler提出DSL應(yīng)該分為外部DSL及內(nèi)部DSL兩種,我認為這種劃分方式還是比較有意義的。外部DSL是自我包含的語言,它們有自己特定語法、解析器和詞法分析器等等,它往往是一種小型的編程語言,甚至不會像GPPL那樣需要源文件。與之相對的則是內(nèi)部DSL。內(nèi)部DSL其實更像是種別稱,它代表一類特別API及使用模式。這里我會給你們看一些示例。
這些是我們平時會遇到的一些外部DSL,如這張幻燈片上表現(xiàn)的XSLT,SQL或是Unix腳本。外部DSL的特點是,你在構(gòu)建這種DSL時,其實扮演的是編程語言設(shè)計者的角色,這個工作并不會交給普通人去做。外部DSL一般會直接針對特定的領(lǐng)域設(shè)計,而不考慮其他東西。James Gosling曾經(jīng)說過這樣的話,每個配置文件最終都會變成一門編程語言。你一開始可能只會用它表示一點點東西,然后慢慢你便會想要一些規(guī)則,而這些規(guī)則則變成了表達式,可能你還會定義變量,進行條件判斷等等。而最終它就變成了一種奇怪的編程語言,這樣的情況屢見不鮮。
事實上,現(xiàn)在有一些公司也在關(guān)注DSL的開發(fā)。例如以前在微軟工作的Charles Simonyi提出了Intentional Programming的概念,還有一個叫做JetBrains的公司提供一個叫做MPS(Meta Programming System)的產(chǎn)品。最近微軟也提出了自己的Oslo項目,而在Eclipse世界里也有個叫做Xtext的東西,所以其實在這方面現(xiàn)在也有不少人在嘗試。
我在觀察外部DSL時,往往會關(guān)注它的語法到底提供了多少空間,例如一種XML的方言,利用XML方言的好處在于有不少現(xiàn)成的工具可用,這樣可以更快地定義自己的語法。
而內(nèi)部DSL,正像我之前說的那樣,它其實只是一系列特別的API及使用模式的別稱。這里則是一些LINQ查詢語句,Ruby on Rails以及jQuery代碼。內(nèi)部DSL的特點是,它其實只是一系列API,但是你可以“假裝”它們一種DSL。內(nèi)部DSL往往會利用一些“流暢化”的技巧,例如像這里的LINQ或jQuery那樣把一些方法通過“點”連接起來。有些則利用了元編程的方式,如這里的Ruby on Rails就涉及到了一些元編程。這種DSL可以訪問語言中的代碼或變量,以及利用如代碼補全,重構(gòu)等母語言的所有特性。
現(xiàn)在我會花幾分鐘時間演示一下我所創(chuàng)建的DSL,也就是LINQ。我相信你們也已經(jīng)用過不少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
}
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
List
List
foreach (Product p in products)
{
if (p.UnitPrice > 20) result.Add(p);
}
GridView1.DataSource = result;
GridView1.DataBind();
}
}
這里有許多Product對象,那么現(xiàn)在我要篩選出所有單價大于20的那些, 再把他們顯示在一個GridView中。傳統(tǒng)的做法就是這樣,我先得到所有的Product對象,然后foreach遍歷每個對象,再判斷每個對象的單價,最終把數(shù)據(jù)綁定到GridView里。運行這個程序……(打開頁面)這就是就能得到結(jié)果。
好,那么現(xiàn)在我要做一些稍微復雜的事情??赡芪也皇且故締蝺r超過20的Product對象,而是要查看每個分類中究竟有多少個單價超過20的對象,然后根據(jù)數(shù)量進行排序。如果不用DSL完成這個工作,那么我可能會先定義一個對象來表示結(jié)果:
class Grouping
{
public string CategoryName { get; set; }
public int ProductCount { get; set; }
}
這是個表示分組的對象,用于保存分類的名稱和產(chǎn)品數(shù)量。然后我們就會寫一些十分丑陋的代碼:
Dictionary
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
result.Sort(delegate(Grouping x, Grouping y)
{
return
x.ProductCount > y.ProductCount ? -1 :
x.ProductCount < y.ProductCount ? 1 :
0;
});
我先創(chuàng)建一個新的字典,用于保存分類名稱到分組的對應(yīng)關(guān)系。然后我遍歷每個Product對象,對于每個單價大于20的對象,如果字典中還沒有保存對應(yīng)的分組則創(chuàng)建一個,然后將數(shù)量加一。然后為了排序,我調(diào)用Sort方法,于是我要提供一個委托作為排序方法,然后blablablabla……執(zhí)行之后……(打開頁面)我自然可以得到想要的結(jié)果。
但是,首先這些代碼寫起來需要花費一些時間,很顯然。然后仔細觀察,你會發(fā)現(xiàn)這寫代碼幾乎都是在表示“How”,而“What”基本已經(jīng)丟失了。假設(shè)我離開了,現(xiàn)在新來了一個程序員要維護這段代碼,他會需要一點時間才能完整理解這段代碼,因為他無法直接看清代碼的目標。
不過如果這里我們使用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……先調(diào)用Where……blablabla……再GroupBy等等。由于我們這里可以使用DSL來表示高階的術(shù)語,用以體現(xiàn)我們想做的事情。于是這段代碼則更加關(guān)注于“What”而不是“How”。我這里不會明確地指示我想要過濾的方式,我也不會明確地說我要建立字典和分類,這樣基礎(chǔ)結(jié)構(gòu)就可以聰明地,或者說更加聰明地去確定具體的執(zhí)行方式。你可能比較容易想到我們可以并行地執(zhí)行這段代碼,因為我沒有顯式地指定做事方式,我只是表示出我的意圖。
我們打開頁面……(打開頁面)很顯然我們得到了相同的結(jié)果。
這里比較有趣的是,內(nèi)部DSL是如何設(shè)計進C#語法中的,為此我們?yōu)镃# 3.0添加了一系列的特性,例如Lambda表達式,擴展方法,類型推斷等等。這些特性統(tǒng)一起來之后,我們就可以設(shè)計出更為豐富的API,組合之后便成為一種內(nèi)部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() };
編譯器會簡單地將這種形式轉(zhuǎn)化為前一種形式。不過,這里我認為有意思的地方在于,你完全可以創(chuàng)建一門和領(lǐng)域編程語言完全無關(guān)的語法,然后等這種語法和API變得流行且豐富起來之后,再來創(chuàng)一種新的表現(xiàn)形式,就如這里的LINQ查詢語法。我頗為中意這種語言設(shè)計的交流方式。
OK,現(xiàn)在我們回到下面的內(nèi)容。
(未完待續(xù))
評論