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