C# 函数式编程:LINQ
一直以來,我以為 LINQ 是專門用來對不同數據源進行查詢的工具,直到我看了這篇十多年前的文章,才發現 LINQ 的功能遠不止 Query。這篇文章的內容比較高級,主要寫了用 C# 3.0 推出的 LINQ 語法實現了一套“解析器組合子(Parser Combinator)”的過程。那么這個組合子是用來干什么的呢?簡單來說,就是把一個個小型的語法解析器組裝成一個大的語法解析器。當然了,我本身水平有限,暫時還寫不出來這么高級的代碼,不過這篇文章中的一段話引起了我的注意:
Any type which implements Select, SelectMany and Where methods supports (part of) the "query pattern" which means we can write C#3.0 queries including multiple froms, an optional where clause and a select clause to process objects of this type.
大意就是,任何實現了?Select,SelectMany?等方法的類型,都是支持類似于?from x in y select x.z?這樣的 LINQ 語法的。比如說,如果我們為?Task?類型實現了上面提到的兩個方法,那么我們就可以不借助?async/await?來對 Task 進行操作:
那么我們就來看看如何實現一個非常簡單的 LINQ to Task 吧。
LINQ to Task
首先我們要定義一個?Select?拓展方法,用來實現通過一個?Func<TValue, TResult>?將?Task<TValue>?轉換成?Task<TResult>?的功能。
static async Task<TR> Select<TV,TR>(this Task<TV> task, Func<TV, TR> selector) { ? ?var value = await task; ? ?// 取出 task 中的值return selector(value); ? ?// 使用 selector 對取出的值進行變換}這個函數非常簡單,甚至可以簡化為一行代碼,不過僅僅這是這樣就可以讓我們寫出一個非常簡單的 LINQ 語句了:
var taskA = Task.FromResult(12);var r = from a in taskA select a * a;那么實際上 C# 編譯器是如何工作的呢?我們可以借助下面這個有趣的函數來一探究竟:
void PrintExpr<T1,T2>(Expression<Func<T1, T2>> expr) {Console.WriteLine(expr.ToString()); }熟悉 LINQ 的人肯定對 Expression 不陌生,Expressing 給了我們在運行時解析代碼結構的能力。在 C# 里面,我們可以非常輕松地把一個 Lambda 轉換成一個 Expression,然后調用轉換后的 Expression 對象的?ToString()?方法,我們就可以在運行時以字符串的形式獲取到 Lambda 的源碼。例如:
var taskA = Task.FromResult(12); PrintExpr((int _) => from a in taskA select a * a);// 輸出: _ => taskA.Select(a => (a * a))可以看到,Expression 把這段 LINQ 的真面目給我們揭示出來了。那么,更加復雜一點的 LINQ 呢?
如果你嘗試運行這段代碼,你應該會遇到一個錯誤——缺少對應的?SelectMany?方法,下面給出的就是這個?SelectMany?方法的實現:
這個?SelectMany?實現的功能就是,通過一個?Func<TValue, Task<TResult>>?將?Task<TValue>?轉換成?Task<TResult>。有了這個之后,你就可以看到上面的那個較為復雜的 LINQ to Task 語句編譯后的結果:
_ => taskA.SelectMany(a => taskB, (a, b) => (a * b))可以看到,當出現了兩個 Task 之后,LINQ 就會使用?SelectMany?來代替?Select。可是我想為什么 LINQ 不像之前那樣,用兩個?Select?分別處理兩個 Task 呢?為了弄清楚這個問題,我試著推導了一番:
結果比 LINQ 還多調用了兩次?Select。仔細看的話,就會發現,我們所寫的第二個?Select?其實就是?SelectMany,的第二個參數,而對于第一個?Select?來說,因為 b 是一個 Task,所以?b.Select(xxx)?的返回值肯定是一個 Task,而這又恰好符合?SelectMany?函數的第一個參數的特征。
有了上面的經驗,我們不難推斷出,當?from x in y?語句的個數超過 2 個的時候,LINQ 仍然會只使用?SelectMany?來進行翻譯。因為?SelectMany可以被看作為把兩層 Task 轉換成單層 Task,例如:
這里 LINQ 為第一個?SelectMany?的結果生成了一個匿名的中間類型,將 taskA 跟 taskB 的結果組合成了 Task<{a, b}>,方便在第二個?SelectMany?中使用。
至此,一個非常簡單的 LINQ to Task 就完成了,通過這個小工具,我們可以實現不使用?async/await?就對類型進行操作。然而這并沒有什么卵用,因為?async/await?確實要比?from x in y?這種語法要來的更加簡單。不過舉一反三,我們可以根據上面的經驗來實現一個更加使用的小功能。
LINQ to Result
在一些比較函數式的語言(如 F#,Rust)中,會使用一種叫做?Result<TValue, TError>?的類型來進行異常處理。這個類型通常用來描述一個操作結果以及錯誤信息,幫助我們遠離 Exception 的同時,還能保證我們全面的處理可能出現的錯誤。如果使用 C# 實現的話,一個 Result 類型可以被這么來定義:
接著仿照上面為 Task 定義 LINQ 拓展方法,為了 Result 設計?Select?跟?SelectMany:
那么 LINQ to Result 在實際中的應用是什么樣子的呢,接下來我用一個小例子來說明:
某公司為感謝廣大新老用戶對 “5 元 30 M”流量包的支持,準備給余額在 350 元用戶的以上的用戶送 10% 話費。但是呢,如果用戶在收到贈送的話費后余額會超出 600 元,就不送話費了。
可以看到,使用 Result 能夠讓我們更加清晰地用代碼描述業務邏輯,而且如果我們需要向現有流程中添加新的驗證邏輯,只需要在合適地地方插入?from result in validate(xxx)?就可以了,換句話說,我們的代碼變得更加“聲明式”了。
函數式編程
細心的你可能已經發現了,不管是 LINQ to Task 還是 LINQ to Result,我們都使用了某種特殊的類型(如:Task,Result)對值進行了包裝,然后編寫了特定的拓展方法 ——?SelectMany,為這種類型定義了一個重要的基本操作。在函數式編程的里面,我們把這種特殊的類型統稱為“Monad”,所謂“Monad”,不過是自函子范疇上的半幺群而已。
范疇(Category)與函子(Functor)
在高中數學,我們學習了一個概念——集合,這是范疇的一種。
對于我們程序員來說,int?類型的全部實例構成了一個集合(范疇),如果我們為其定義了一些函數,而且它們之間的復合運算滿足結合律的話,我們就可以把這種函數叫做?int?類型范疇上的“態射”,態射講的是范疇內部元素間的映射關系,例如:
f,g,h?都是?int?類型范疇上的態射,因為函數的復合運算是滿足結合律的。
我們還可以定義一種范疇間進行元素映射的函數,例如:
Func<int, double> ToDouble = x => Convert.ToDouble(x);這里的函數?Select?實現了?int?范疇到?double?范疇的一個映射,不過光映射元素是不夠的,要是有一種方法能夠幫我們把?int?中的態射(f,g,h),映射到?double?范疇中,那該多好。那么下面的函數?F?就幫助我們實現了這了功能。
因為?F?能夠將一個范疇內的態射映射為另一個范疇內的態射,ToDouble?可以將一個范疇內的元素映射為另一個范疇內的元素,所以,我們可以把?F與?ToDouble?的組合稱作“函子”。函子體現了兩個范疇間元素的抽象結構上的相似性。
相信看到這里你應該對范疇跟函子這兩個概念有了一定的了解,現在讓我們更進一步,看看 C# 中泛型與范疇之間的關系。
類型與范疇
在之前,我們是以數值為基礎來理解范疇這個概念的,那么現在我們從類型的層面來理解范疇。
泛型是我們非常熟悉的 C# 語言特性了,泛型類型與普通類型不一樣,泛型類型可以接受一個類型參數,看起來就像是類型的函數。我們把接受函數作為參數的函數稱為高階函數,依此類推,我們就把接受類型作為參數的類型叫做高階類型吧。這樣,我們就可以從這個層面把 C# 的類型分為兩類:普通類型(非泛型)和高階類型(泛型)。
前面的例子中,我列出的?f,g,h?能夠完成?int -> int?的轉換,因為它們是?int?范疇內的態射。而?ToDouble?能夠完成?int -> double?的轉換,那我們就可以將他看作是普通類型范疇的態射,類似的,我們還可以定義出?ToInt32,ToString?這樣的函數,它們都能完成兩個普通類型之間的轉換,所以也都可以看作是普通類型范疇的態射。
那么對于高階類型(也就是泛型)范疇來說,是不是也存在態射這樣的東西呢?答案是肯定的,舉個例子,用 LINQ 把?List<int>?轉換成?List<double>?:
Func<List<int>, List<double>> ToDoubleList = x => x.Select(ToDouble).ToList();不難發現,這里的?ToDoubleList?是?List<T>?類型范疇內的一個態射。不過你可能已經注意到了我們使用的?ToDouble?函數,它是普通類型范疇內的一個態射,我們僅僅通過一個?Select?函數就把普通類型范疇內的一個態射映射成了?List<T>?范疇內的一個態射(上面的例子中,是把?(int -> double)?轉換成了?(List<int> -> List<double>)),而且?List<T>?還提供了能夠把?int?類型轉換成?List<int>?類型(type)的方法:new List<int>{ intValue },那么我們就可以把?List<T>?類(class)稱為“函子”。事情變得有趣了起來。
自函子
List<T>?還有一個構造函數可以允許我們使用另一個 List 對象創建一個新的 List 對象:new List<T>(list),這完成了?List<T> -> List<T>?轉換,這看起來像是把?List<T>?范疇中的元素重新映射到了?List<T>?范疇中。有了這個構造函數的幫助,我們就可以試著使用?Select?來映射?List<T>中的態射(比如,ToDoubleList):
// 這個映射后的 ToDoubleListAgain 仍然能夠正常的工作Func<List<int>, List<List<double>>> ToDoubleListAgain = x => x.Select(e => ToDoubleList(new List<int>(){e})).ToList();這里的返回值類型看起來有些奇怪,我們得到了一個嵌套兩層的?List,如果你熟悉 LINQ 的話,馬上就會想到?SelectMany?函數——它能夠把嵌套的?List?拍扁:
這樣,我們就實現了?(List<T1> -> List<T2>) -> (List<T1> -> List<T2>)?的映射,雖然功能上并沒有什么卵用,但是卻實現了把?List<T>?范疇中的態射映射到了?List<T>?范疇中的功能。現在看來,List<T>?類不僅是普通類型映射到?List<T>?的一個函子,它也是?List<T>?映射到?List<T>?的一個函子。這種能夠把一個范疇映射到該范疇本疇上的函子也被稱為“自函子”。
我們可以發現,C# 中大部分的自函子都通過 LINQ 拓展方法實現了?SelectMany?函數,其簽名是:
SomeType<TR> SelectMany<TV, TR>(SomeType<TV> source, Func<TV, SomeType<TR>> selector);List<T>?還有一個不接受任何參數的構造函數,它會創建出一個空的列表,我們可以把這個函數稱作?unit,因為它的返回值在?List<T>?相關的一些二元運算中起到了單位 1 的作用。比如,concat(unit(), someList)?與?concat(someList, unit())?得到的列表,在結構上是等價的。擁有這種性質的元素被稱為“單位元”。
在函數式編程中,我們把擁有?SelectMany(也被叫做?bind),unit?函數的自函子稱為“Monad”。
但是 C# 中并不是所有的泛型類是自函子,例如?Task<T>,如果我們不為它添加?Select?拓展方法,它連函子都算不上。所以如果把 C# 中全部的自函子類型放在一個集合中,然后把這些自函子類型之間用來做類型轉換的全部函數(例如,list.ToArray()?等)看作是態射,那么我們就構建出來了一個 C# 中的“自函子范疇”。在這個范疇上,我們只能對 Monad 類型使用 LINQ 語法進行復合運算,例如上面的:
由于這種作用在兩個 Monad 上面的二元運算滿足交換律且 Monad 中存在單位元,與群論中幺半群的定義比較類似,所以,我們也把 Monad 稱為“自函子范疇上的幺半群”。盡管這句話聽起來十分的高大上,但是卻并沒有說明 Monad 的特征所在。就好比別人跟你介紹手機運營商,說這是一個提供短信、電話業務的公司,你肯定不知道他到底再說哪一家,不過他要是說,這是一個提供 5 元 30 M 流量包的手機運營商,那你就知道了他指的是中國移動。
個人體會
其實我一開始想寫的內容只有 LINQ to Result 跟 LINQ to Task 的,但是在編寫代碼的過程中,種種跡象都表明著 LINQ 跟函數式編程中的 Monad 有不少關系,所以就把剩下的函數式編程這一部分給寫出來了。
Monad 作為函數式編程中一種重要的數據類型,可以用來表達計算中的每一小步的功能,通過 Monad 之間的復合運算,我們可以靈活的將這些小的功能片段以一種統一的方式重組、復用,除此之外,我們還可以針對特定的需求(異步、錯誤處理、懶惰計算)定義專門的 Monad 類型,幫助我們以一種統一的形式將這些特別的功能嵌入到代碼之中。在傳統的面向對象的編程語言中 Monad 這個概念確實是不太好表達的,不過有了 LINQ 的幫助,我們可以比較優雅地將各種 Monad 組合起來。
用 LINQ 來對 Monad 進行運算的缺點,主要就是除了?SelectMany?之外的,我們沒辦法定義其他的能在 Query 語法中使用的函數了,要解決這個問題,請關注我的下一篇文章:“F# 函數式編程:Computational Expression”(挖坑預備)。
參考資料
https://zh.wikipedia.org/zh-hans/函子
https://en.wikipedia.org/wiki/Monad_(functional_programming)
http://hongjiang.info/understand-monad-4-what-is-functor/
原文地址:?https://www.cnblogs.com/JacZhu/p/9729587.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的C# 函数式编程:LINQ的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法工程师的危机
- 下一篇: C# 中使用面向切面编程(AOP)中实践