從Lambda語法來探討.NET LINQ的技術底蘊到底在哪裡?


語言整合查詢 LINQ(Language Integrated Query)

世界最高的山叫聖母峰(珠穆朗瑪峰),而讓主脈看起來偉大神聖的,就是環繞的支線山脈稜線風光,群山烘托出一幅一幅壯絕的美景。.NET的LINQ就像聖母峰主山脈一樣,匯集各項技術才完成整個技術的淬鍊,成就偉業。(此時腦中浮現美隊大喊:Avengers! assemble...)
就在Python、C++、C#、PHP以及Javascript這些語言都支援Lambda語法的當今,還不會寫Lambda的程式人員請考慮移民火星,因為地球越來越危險了。

然後我們不免好奇一件事,究竟Lambda(發音)的由來和原理是甚麼,我們只知道使用Lambda可以少寫很多代碼,它可以用來寫匿名函式給Delegate委派、寫Linq必用、能把函式當參數傳、可以inline寫一長串程式碼超爽der...等等神奇又便利的特性,生在現代的程式設計師是不是太幸福了呢? ...

    但也許我們就像瞎子摸象故事中那些盲(忙)人一樣,天天盲(忙)著工作,成本和速度儼然主宰了一切,漸漸對技術駕馭無能為力,直到彼此都在用荒謬溝通。

網路上大多是介紹Lambda語法的文章,完整精闢的觀念文缺乏,發現越來越多彼此對Lambda有一些認知上的差距;隨著.NET 的版本不斷演進,C#這個語言已進入一個全新的里程碑,而時下最夯的程式技術詞彙:Lambda 則是引爆一切的原點,究竟Lambda成就了C#,還是LINQ成就了這一切,卻把光環都給了Lambda,就讓我試著來整理這些資料,幫大家系統性的做一個介紹。

從Lambda起源說起

Lambda原本是一套數學邏輯表達式λ演算,我們先來看維基上的介紹:

「λ演算(英語:lambda calculus,λ-calculus)是一套從數學邏輯中發展,以變數綁定和替換的規則,來研究函式如何抽象化定義、函式如何被應用以及遞迴形式系統。它由數學家阿隆佐·邱奇在20世紀30年代首次發表。lambda演算作為一種廣泛用途的計算模型,可以清晰地定義什麼是一個可計算函式,而任何可計算函式都能以這種形式表達和求值,它能類比單一磁帶圖靈機的計算過程;儘管如此,lambda演算強調的是變換規則的運用,而非實現它們的具體機器。 「lambda演算可比擬是最根本的程式語言,它包括了一條變換規則(變數替換)和一條將函式抽象化定義的方式。因此普遍公認是一種更接近軟體而非硬體的方式。對函數式程式語言造成很大影響,比如LispML語言Haskell語言。...」(維基百科)


另一篇維基文章寫道:

「有類型lambda演算是使用lambda符號(λ)指示匿名函式抽象的一種有類型的形式化。有類型lambda演算是基礎程式語言並且是有類型的函式語言程式設計語言MLHaskell和更間接的指令式程式語言的基礎。它們通過Curry-Howard同構密切關聯於直覺邏輯並可以被認為是範疇的類的內部語言,比如簡單類型lambda演算是笛卡爾閉範疇(CCC)的語言。傳統上,有類型lambda演算被看作無類型lambda演算的精細化。更現代的觀點把有類型lambda演算看做更基礎的理論,而把無類型lambda演算看作它的只有一個類型的特殊情況。」



首先,我們由上面這段說明知道"λ"這個數學符號唸做Lambda,它是在20世紀30年代被某個數學家想對函式有一個好的抽象化定義而提出。

然後,藉著有類型Lambda(強型別的Lambda)語法推波助瀾,函數式編程(FP)得到重大的利益,連帶地影響到其他物件導向語言的設計方向。直到今天,C#這類具備一級函式的程式語言,正在將編譯器開源(Roslyn),到各平台攻城掠地。

個人對Lambda的理解為每個Lambda函式(λ)必定是一個輸入輸出值對稱的"連鎖"封閉範圍,每個(λ)都是Lambda函式,若表達以:λ.x.λ.y.x+y 這個式子等價於((x, y) => x + y),而且由於演算的函式為有類型,所以能成功證明所有(λ)的大小關係,因為這個特色而成為程式語言的最佳語法。

程式發展不過短短幾十年歷史,未來會不會有更好的演算表達式或方法出現讓程式語言更強大,誰都說不準。


語法糖


語法糖(Syntactic sugar)是由英國電腦科學家彼得·蘭丁發明的一個術語,指電腦語言中添加的某種語法,這種語法對語言的功能沒有影響,但是更方便程式設計師使用。 語法糖讓程式更加簡潔,有更高的可讀性。(維基百科)

C#史上最大顆的語法糖 - Lambda

有人說Lambda是從delegate演變而來的,可delegate至少是個類別,編譯器還是要將Lambda表達式轉換成左方(等號左邊)相應的物件,任何語法糖都會先改寫成函式庫中的類別和API才能開始編譯。

來談談出現Lambda以前,我們怎麼產生Delegate的:

(以下內容具備有中階語言的基礎即可理解)
我們設計Delegate這個過程是透過宣告具名函式,並將這個具名函式的「函式參考指標」指定給我們事先定義好,具有相同函式簽章的委派具名變數,此變數為一函式指標,換句話說「函式參考指標」就是C#的函式「Reference Type - 參考型別」,而它的另一個代名詞就是我們說的“delegate”。
當編譯器編譯某具名函式後能提供它的靜態的函式位址參考給特定函式指標變數(使用delegate關鍵字來宣告此函式指標變數),該變數可以用來傳遞給其它物件當Callback或註冊到某個「EventHandler(一種事件觸發和Delegate註冊器)」,得到它的程式可以呼叫此函式參考的Invoke"方法來執行此函式,或使用變數名稱+"()"的語法來執行。

傳統的Delegate的宣告(這裡我們不使用Inline):
//委派需宣告方法簽章作為編譯型別識別,與lambda相比自定義的delegate沒有參數最多16個的限制,而且參數不但可以具名和選擇性參數(能設置預設值)
private delegate int my_delegate(int a1, int a2 = 0);

//宣告方法簽章一樣的具名函式My_function
private static int My_function(int a1, int a2) {
 return a1 + a2;
}
用法:
//My_function在C#語法裡代表此函式在編譯後的的動態連結位址(編譯階段給定),執行時期會將my_delegate型別變數mydel的內容指向左邊具名函式動態連結位址。
my_delegate mydel = My_function;

//mydel的內容指向左邊具名函式,因此可以執行mydel等價於執行My_function
int myFunc_num = My_function(1, 2); //return 3
int myDel_num = mydel(1, 2); //return 3;


C#編譯器會根據一段賦值語句中左值所代表的型別或.NET函式庫中物件來轉譯出右邊lambda表達式所需的最終程式碼。

事實上使用lambda表達式來宣告delegate函式時,所轉譯出來的程式碼與上述傳統宣告只使用靜態函式的寫法是完全不同的,透過以下程式碼範例說明了用lambda表達式轉譯成delegate型別其實是利用Func/Action物件;Func和Action都是.NET事先定義好的delegate物件(C#封裝了具有0-16個參數並傳回 TResult 參數所指定之型別值的17個方法和動作,ex: public delegate TResult Func<in T,out TResult>(T arg);),它們的存在原因是因為在運行LINQ時需要C#進化為具有一級函式語言能力,而C#物件都必須是強式型別,開發人員用多載的方式產生了它們的泛型物件來模擬一級函式物件,它是LINQ to Everything的迭代器設計最佳解法。

再來,基於Func/Action規格限制(18種函式簽章),所以lamda產生的delegate的函式功能是一些限制的,譬如:最多只能有16個傳入參數,而且沒有正常函式的參數具名、選擇性參數與預設值可以使用,這是進階開發者必要常識。





Lambda表達式更像是C#編譯器的語法糖,但不單單只是delegate的專屬語法糖
LINQ的維基裡提到:
「While LINQ is primarily implemented as a library for .NET Framework 3.5, it also defines optional language extensions that make queries a first-class language construct and provide syntactic sugar for writing queries. These language extensions have initially been implemented in C# 3.0, VB 9.0, F#[5] and Oxygene, with other languages like Nemerle having announced preliminary support. ...」

原文說在介紹LINQ定義了可選的語言擴展時,說道:「這些擴展使查詢成為一流的語言構造,並為編寫查詢提供了語法糖

這語法糖指的不外乎就是依賴System.Linq.Expressions.Lambda的API來支持語法的編譯轉換,我用以下程式碼來驗證這個事實:


結論

不論是System.Linq.IQueryable<T>、System.Linq.IEnumerable<T>這種強大的迭代器,亦或是實現了能描述程式碼與程式模塊的語法樹(Syntex Tree)這種先進編譯技術的System.Linq.Expressions API,透過演算樹(Expressions Tree),程式不僅可以做到執行時期(run-time)動態編譯,也使得LINQ to SQL的資料庫SQL語法轉譯成為簡單的工作,更讓.NET有辦法讓一份程式碼跨平台編譯。

因此,真正挹注整個C#語言一級函式能力的是後端的LINQ,因為幕後實力堅強(重量級的API都在System.Linq底下),於此同時,Lambda則用它優雅的語法征服我們,就像是網站前端盡情綻放UIUX的魅力。

感受LINQ所完成的技術挑戰,再去想想C#.NET在3.0做了甚麼重大革新,你會很驚訝。

感謝您撥冗閱讀,本文歡迎散播分享,也期盼您的指正與賜教,再次感謝~

留言

  1. 昨天針對lambda和delegate的原理做範例說明上的補強

    回覆刪除
  2. 也是拉 可以移民火星的人也不是靠寫程式過活 ㄎㄎ

    回覆刪除

張貼留言

這個網誌中的熱門文章

揭開 C#.NET 官方原始碼 System.Func、System.Action、System.Delegate 及 delegate 關鍵字背後的技術面紗

C# 物件屬性的建構賦值與初始化 - C# constructor and object initializer