C# 函數式編程:LINQ

一直以來,我覺得 LINQ 是專門用來對不一樣數據源進行查詢的工具,直到我看了這篇十多年前的文章,才發現 LINQ 的功能遠不止 Query。這篇文章的內容比較高級,主要寫了用 C# 3.0 推出的 LINQ 語法實現了一套「解析器組合子(Parser Combinator)」的過程。那麼這個組合子是用來幹什麼的呢?簡單來講,就是把一個個小型的語法解析器組裝成一個大的語法解析器。固然了,我自己水平有限,暫時還寫不出來這麼高級的代碼,不過這篇文章中的一段話引發了個人注意:html

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 對取出的值進行變換
}async

這個函數很是簡單,甚至能夠簡化爲一行代碼,不過僅僅這是這樣就可讓咱們寫出一個很是簡單的 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 組合起來。
 
原連接地址:https://www.cnblogs.com/JacZhu/p/9729587.html
相關文章
相關標籤/搜索