一直以來,我覺得 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 進行操做:異步
// 請在 Xamarin WorkBook 中執行 var taskA = Task.FromResult(12); var taskB = Task.FromResult(12); // 使用 async/await 計算 taskA 跟 taskB 的和 var a = await taskA; var b = await taskB; var r = a + b; // 若是爲 Task 實現了 LINQ 拓展方法,就能夠這麼寫: var r = from a in taskA from b in taskB select a + b;
那麼咱們就來看看如何實現一個很是簡單的 LINQ to Task 吧。async
首先咱們要定義一個 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 語句了:ide
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 呢?工具
var taskA = Task.FromResult(12); var taskB = Task.FromResult(12); PrintExpr((int _) => from a in taskA from b in taskB select a * b );
若是你嘗試運行這段代碼,你應該會遇到一個錯誤——缺乏對應的 SelectMany
方法,下面給出的就是這個 SelectMany
方法的實現:學習
static async Task<TR> SelectMany<TV, TS, TR>(this Task<TV> task, Func<TV, Task<TS>> selector, Func<TV,TS, TR> projector){ var value = await task; var selected = await selector(value); return projector(value, selected); }
這個 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 呢?爲了弄清楚這個問題,我試着推導了一番:
// 首先簡單粗暴的用兩個 Select 來實現這個功能 Task<Task<int>> r = taskA.Select(a => b.Select(b => a + b)); // r 被包裹了兩層 Task,咱們能夠用 SelectMany 來去掉一層 Task 包裝 // 這時 TValue 是 Task<int>, TResult 是 int // // 那麼 Task<Task<int>> // 將經過 Func<Task<int>, Task<int>> // 轉換成 Task<int> Task<int> result = r.SelectMany(x => x, (_, x) => x);
結果比 LINQ 還多調用了兩次 Select
。仔細看的話,就會發現,咱們所寫的第二個 Select
其實就是 SelectMany
,的第二個參數,而對於第一個 Select
來講,由於 b 是一個 Task,因此 b.Select(xxx)
的返回值確定是一個 Task,而這又剛好符合 SelectMany
函數的第一個參數的特徵。
有了上面的經驗,咱們不難推斷出,當 from x in y
語句的個數超過 2 個的時候,LINQ 仍然會只使用 SelectMany
來進行翻譯。由於 SelectMany
能夠被看做爲把兩層 Task 轉換成單層 Task,例如:
var taskA = Task.FromResult(12); var taskB = Task.FromResult(12); var taskC = Task.FromResult(12); PrintExpr((int _) => from a in taskA from b in taskB from c in taskC select a * b + c ); // 個人推斷: var r = taskA.SelectMany(a => taskB, (a, b) => new {a, b}).SelectMany(temp => taskC, (temp, c) => temp.a * temp.b + c); // 實際的輸出: // _ => taskA.SelectMany(a => taskB, (a, b) => new <>f__AnonymousType0#1`2(a = a, b = b)).SelectMany(<>h__TransparentIdentifier0 => taskC, (<>h__TransparentIdentifier0, c) => ((<>h__TransparentIdentifier0.a * <>h__TransparentIdentifier0.b) + c))
這裏 LINQ 爲第一個 SelectMany
的結果生成了一個匿名的中間類型,將 taskA 跟 taskB 的結果組合成了 Task<{a, b}>,方便在第二個 SelectMany
中使用。
至此,一個很是簡單的 LINQ to Task 就完成了,經過這個小工具,咱們能夠實現不使用 async/await
就對類型進行操做。然而這並無什麼卵用,由於 async/await
確實要比 from x in y
這種語法要來的更加簡單。不過觸類旁通,咱們能夠根據上面的經驗來實現一個更加使用的小功能。
在一些比較函數式的語言(如 F#,Rust)中,會使用一種叫作 Result<TValue, TError>
的類型來進行異常處理。這個類型一般用來描述一個操做結果以及錯誤信息,幫助咱們遠離 Exception 的同時,還能保證咱們全面的處理可能出現的錯誤。若是使用 C# 實現的話,一個 Result 類型能夠被這麼來定義:
class Result<TValue, TError> { public TValue Value {get; private set;} public TError ErrorMsg {get; private set;} public bool IsSuccess {get; private set;} public override string ToString() { if(this.IsSuccess) return "Success: " + Value.ToString(); return "Error: " + ErrorMsg.ToString(); } public static Result<TValue, TError> OK(TValue value) { return new Result<TValue, TError> {Value = value, ErrorMsg = default(TError), IsSuccess = true}; } public static Result<TValue, TError> Error(TError error) { return new Result<TValue, TError> {Value = default(TValue), ErrorMsg = error, IsSuccess = false}; } }
接着仿照上面爲 Task 定義 LINQ 拓展方法,爲了 Result 設計 Select
跟 SelectMany
:
static Result<TR, TE> Select<TV,TR, TE>(this Result<TV, TE> result, Func<TV, TR> selector) => result.IsSuccess ? Result<TR, TE>.OK(selector(result.Value)) : Result<TR, TE>.Error(result.ErrorMsg); static Result<TR, TE> SelectMany<TV, TS, TR, TE>(this Result<TV, TE> result, Func<TV, Result<TS, TE>> selector, Func<TV, TS, TR> projector){ if (result.IsSuccess) { var tempResult = selector(result.Value); if (tempResult.IsSuccess) { return Result<TR, TE>.OK(projector(tempResult.Value, tempResult.Value)); } return Result<TR, TE>.Error(tempResult.ErrorMsg); } return Result<TR, TE>.Error(result.ErrorMsg); }
那麼 LINQ to Result 在實際中的應用是什麼樣子的呢,接下來我用一個小例子來講明:
某公司爲感謝廣大新老用戶對 「5 元 30 M」流量包的支持,準備給餘額在 350 元用戶的以上的用戶送 10% 話費。可是呢,若是用戶在收到贈送的話費後餘額會超出 600 元,就不送話費了。
using Money = Result<double, string>; // 查找指定 Id 的用戶是否存在 Result<int, string> GetUserById(int id) { if(id % 7 == 0) { // 正常的用戶 return Result<int,string>.OK(id); } if(id % 2 == 0) { return Result<int, string>.Error("用戶已被凍結"); } return Result<int, string>.Error("用戶不存在"); } // 查找指定用戶的餘額 Money GetMoneyFromUser(int id) { if (id >= 35) { return Money.OK(id * 10); } return Money.Error("窮逼用戶不參與此次活動"); } // 給用戶轉帳 Money Transfer(double money, double amount) { return from canTransfer in CheckForTransfer(money, amount) select canTransfer ? money + amount : money; } // 檢查用戶是否知足轉帳條件,若是轉帳後的餘額超過了 600 元,則終止轉帳 Result<bool, string> CheckForTransfer(double a, double b) { if (a + b >= 600) { return Result<bool,string>.Error("超出餘額限制"); } return Result<bool,string>.OK(true); } Money SendGift(int userId) { return // 查詢用戶信息 from user in GetUserById(userId) // 獲取該用戶的餘額 from money in GetMoneyFromUser(user) // 給這個用戶轉帳 from transfer in Transfer(money, money * 0.1) // 獲取結果 select transfer; } SendGift(42) // Success: 462 SendGift(56) // Error: 超出餘額限制 SendGift(1) // Error: 用戶不存在 SendGift(14) // Error: 窮逼用戶不參與此次活動 SendGift(16) // Error: 用戶已被凍結
能夠看到,使用 Result 可以讓咱們更加清晰地用代碼描述業務邏輯,並且若是咱們須要向現有流程中添加新的驗證邏輯,只須要在合適地地方插入 from result in validate(xxx)
就能夠了,換句話說,咱們的代碼變得更加「聲明式」了。
細心的你可能已經發現了,不論是 LINQ to Task 仍是 LINQ to Result,咱們都使用了某種特殊的類型(如:Task,Result)對值進行了包裝,而後編寫了特定的拓展方法 —— SelectMany
,爲這種類型定義了一個重要的基本操做。在函數式編程的裏面,咱們把這種特殊的類型統稱爲「Monad」,所謂「Monad」,不過是自函子範疇上的半幺羣而已。
在高中數學,咱們學習了一個概念——集合,這是範疇的一種。
對於咱們程序員來講,int
類型的所有實例構成了一個集合(範疇),若是咱們爲其定義了一些函數,並且它們之間的複合運算知足結合律的話,咱們就能夠把這種函數叫作 int
類型範疇上的「態射」,態射講的是範疇內部元素間的映射關係,例如:
// f(x) = x * 2 Func<int, int> f = (int x) => x * 2; // g(x) = x + 1 Func<int, int> g = (int x) => x + 1; // h(x) = x + 10 Func<int, int> h = (int x) => x + 10; // 將函數 g 與 f 複合,(g ∘ f)(x) = g(f(x)) Func<X, Z> Compose<X, Y, Z>(Func<Y, Z> g, Func<X, Y> f) => (X x) => g(f(x)); Compose(h, Compose(g, f))(42) == Compose(Compose(h, g), f)(42) // true
f
,g
,h
都是 int
類型範疇上的態射,由於函數的複合運算是知足結合律的。
咱們還能夠定義一種範疇間進行元素映射的函數,例如:
Func<int, double> ToDouble = x => Convert.ToDouble(x);
這裏的函數 Select
實現了 int
範疇到 double
範疇的一個映射,不過光映射元素是不夠的,要是有一種方法可以幫咱們把 int
中的態射(f
,g
,h
),映射到 double
範疇中,那該多好。那麼下面的函數 F
就幫助咱們實現了這了功能。
// 爲了方便使用 Compose 進行演示,故定義了一個比較函數式的 ToInt 函數 Func<double, int> ToInt = x => Convert.ToInt32(x); // 一個將 int -> int 轉換爲 double -> double 的函數 Func<double, double> F(Func<int, int> selector) => x => Compose(Compose(ToDouble, selector), ToInt)(x); // 在範疇間映射 f var Ff = F(f); Ff(42.0); // 84.00 // 在範疇間映射 g var Fg = F(g); Fg(42.0); // 43.00 // 在範疇間映射 h var Fh = F(h); Fh(42.0); // 52.00 // Ff, Fg, Fh 之間仍然保持結合律,由於他們是 `double` 範疇上的態射 Compose(Fh, Compose(Fg, Ff))(42) == Compose(Compose(Fh, Fg), Ff)(42)
由於 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
拍扁:
Func<List<TV>, List<TR>> FF<TV, TR>(Func<List<TV>, List<TR>> selector) { return xl => xl.SelectMany(x => selector(new List<int>() {x})).ToList(); } var ToDoubleListAgain = FF(ToDoubleList); ToDoubleListAgain(new List<int>{1})
這樣,咱們就實現了 (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 語法進行復合運算,例如上面的:
// 原版 var result = from a in taskA from b in taskB from c in taskC select a * b + c; // 1. 知足結合律 var left = from a in taskA from t in ( from b in taskB from c in taskC select new {b, c} ) select a * t.b + t.c; var left = from t in ( from a in taskA from b in taskB select new {a, b} ) from c in taskC select t.a * t.b + c; left == right // true // 2. 存在單位元 var left = from a in Task.FromException(null) from b in taskB select a + b; var right = from b in taskB from a in Task.FromException(null) select a + b; // 由於 left right 獲得的都是 Task.FromException(null) 的返回值,故 Task.FromException(null) 是單位元
因爲這種做用在兩個 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」(挖坑預備)。