拿 C# 搞函數語言程式設計 - 3
阿新 • • 發佈:2020-03-29
## 前言
今天和某個人聊天聊到了 C# 的 LINQ,發現我認識的 LINQ 似乎和大多數人認識的 LINQ 不太一樣,怎麼個不一樣法呢?其實 LINQ 也可以用來搞函數語言程式設計。
當然,並不是說寫幾個 `lambda` 和用用像 Java 那樣的 `stream` 之類的就算叫做 LINQ 了,LINQ 其實是一個另外的一些東西。
## LINQ
在 C# 中,相信大家都見過如下的 LINQ 寫法:
```csharp
IEnumerable EvenNumberFilter(IEnumerable list)
{
return from c in list where c & 1 == 0 select c;
}
```
以上程式碼藉助 LINQ 的語法實現了對一個列表中的偶數的篩選。
LINQ 只是一個用於方便對集合進行操作的工具而已,如果我們如果想讓我們自己的型別支援 LINQ 語法,那麼我們需要讓我們的型別實現 `IEnumerable`,然後就可以這麼用了。。。
哦,原來是這樣的嗎?那我全都懂了。。。。。。
???哦,我的老天,當然不是!
其實 LINQ 和 `IEnumerable` 完全沒有關係!LINQ 只是一組擴充套件方法而已,它主要由以下方法組成:
| 方法名稱 | 方法說明 |
| ------- | ------- |
| Where | 資料篩選 |
| Select/SelectMany | 資料投影 |
| Join/GroupJoin | 資料聯接 |
| OrderBy/ThenBy/OrderByDescending/ThenByDescending | 資料排序 |
| GroupBy | 資料分組 |
| ...... |
以上方法對應 LINQ 關鍵字:`where`, `select`, `join`, `orderby`, `group`...
在編譯器編譯 C# 程式碼時,會將 LINQ 語法轉換為擴充套件方法呼叫的語法,例如:
```csharp
from c in list where c > 5 select c;
```
會被編譯成:
```csharp
list.Where(c => c > 5).Select(c => c);
```
再例如:
```csharp
from x1 in list1 join x2 in list2 on x1.k equals x2.k into g select g.u;
```
會被編譯成:
```csharp
list1.GroupJoin(list2, x1 => x1.k, x2 => x2.k, (x1, g) => g.u);
```
再例如:
```csharp
from x in list orderby x.k1, x.k2, x.k3;
```
會被編譯成:
```csharp
list.OrderBy(x => x.k1).ThenBy(x => x.k2).ThenBy(x => x.k3);
```
再有:
```csharp
from c in list1
from d in list2
select c + d;
```
會被編譯成:
```csharp
list1.SelectMany(c => list2, (c, d) => c + d);
```
停停停!
此外,編譯器在編譯的時候總是會先將 LINQ 語法翻譯為方法呼叫後再編譯,那麼,只要有對應名字的方法,不就意味著可以用 LINQ 語法了(逃
那麼你看這個 `SelectMany` 是不是。。。
![jpg](https://img2020.cnblogs.com/blog/1590449/202003/1590449-20200329000822036-790619986.jpg)
## `SelectMany` is `Monad`
哦我的上帝,你瞧瞧這個可憐的 `SelectMany`,這難道不是 `Monad` 需要的 `bind` 函式?
事情逐漸變得有趣了起來。
我們繼承上一篇的精神,再寫一次 `Maybe`。
## `Maybe`
首先,我們寫一個抽象類 `Maybe`。
首先我們給它加一個 `Select` 方法用於選擇 `Maybe` 中的資料,如果是 `T`,那麼返回一個 `Just`,如果是 `Nothing`,那麼返回一個 `Nothing`。相當於我們的 `returns` 函式:
```csharp
public abstract class Maybe
{
public abstract Maybe Select(Func> f);
}
```
然後我們實現我們的 `Just` 和 `Nothing`:
```csharp
public class Just : Maybe
{
private readonly T value;
public Just(T value) { this.value = value; }
public override Maybe Select(Func> f) => f(value);
public override string ToString() => $"Just {value}";
}
public class Nothing : Maybe
{
public override Maybe Select(Func> _) => new Nothing();
public override string ToString() => "Nothing";
}
```
然後,我們給 `Maybe` 實現 `bind` —— 即給 `Maybe` 加上一個叫做 `SelectMany` 的方法。
```csharp
public abstract class Maybe
{
public abstract Maybe Select(Func> f);
public Maybe SelectMany(Func> k, Func s)
=> Select(x => k(x).Select(y => new Just(s(x, y))));
}
```
至此,`Maybe` 實現完了!什麼,就這??那麼怎麼用呢?激動人心的時刻來了!
首先,我們建立幾個 `Maybe`:
```csharp
var x = new Just(3);
var y = new Just(7);
var z = new Nothing();
```
然後我們分別利用 LINQ 計算 `x + y`, `x + z`:
```csharp
var u = from x0 in x from y0 in y select x0 + y0;
var v = from x0 in x from z0 in z select x0 + z0;
Console.WriteLine(u);
Console.WriteLine(v);
```
輸出結果:
```
Just 10
Nothing
```
完美!上面的 LINQ 被編譯成了:
```csharp
var u = x.SelectMany(_ => y, (x0, y0) => x0 + y0);
var v = x.SelectMany(_ => z, (x0, z0) => x0 + z0);
```
此時,函式 `k` 為 `int -> Maybe`,而函式 `s` 為`(int, int) -> int`,是一個加法函式。
函式 `k` 的引數我們並不關心,它用作一個 `selector`,我們只需要讓它產生一個 `Maybe`,然後利用函式 `s` 將兩個 `int` 的值做加法運算,並把結果包裝到一個 `Just` 裡面即可。
這個過程中,如果有任何一方產生了 `Nothing`,則後續運算結果永遠都是 `Nothing`,因為 `Nothing.Select(...)` 還是 `Nothing`。
## 一點擴充套件
我們再給這個 `Maybe` 加一個 `Where`:
```csharp
public abstract class Maybe
{
public abstract Maybe Select(Func> f);
public Maybe SelectMany(Func> k, Func s)
=> Select(x => k(x).Select(y => new Just(s(x, y))));
public Maybe Where(Func, bool> f) => f(this) ? this : new Nothing();
}
```
然後我們就可以玩:
```csharp
var just = from c in x where true select c;
var nothing = from c in x where false select c;
Console.WriteLine(just);
Console.WriteLine(nothing);
```
當滿足條件的時候返回 `Just`,否則返回 `Nothing`。上述程式碼將輸出:
```
Just 3
Nothing
```
有內味了(逃
## 後記
該系列的後續文章將按揭編寫,如果 C# 爭氣一點,把 Discriminated Unions、Higher Kinded Generics 和 Type Classes 特性加上了,我們再繼續。