前一陣子在寫 CPU,導致一直沒有什么時間去做其他的事情,現在好不容易做完閑下來了,我又可以水文章了哈哈哈哈哈。
有關 FP 的類型部分我打算放到明年再講,因為現有的 C# 雖然有一個 pattern matching expressions,但是沒有 discriminated unions 和 records,只能說是個半殘廢,要實現 FP 那一套的類型異常的復雜。西卡西,discriminated unions 和 records 這兩個東西官方已經定到 C# 9 了,所以等明年 C# 9 發布了之后我再繼續說這部分的內容。
另外,concepts(type classes)、traits 、intersect & sum types 和高階類型也可能會隨着 C# 9、10 一並到來。因此到時候再講才會講得更爽。另外吹一波 traits類型系統,同樣是圖靈完備的類型系統,在表達力上要比OOP強太多,歡迎大家入坑,比如 Rust 和未來的 C#。
這一部分我們介紹一下 Functor、Applicative和 Monad 都是些什么。
本文試圖直觀地講,目的是讓讀者能比較容易的理解,而不是准確知道其概念如何,因此會盡量避免使用一些專用的術語,如范疇學、數學、λ 計算等等里面的東西。感興趣的話建議參考其他更專業的資料。
Functor
Functor 也叫做函子。想象一下這樣一件事情:
現在我們有一個純函數 IsOdd
bool IsOdd(int value) => (value & 1) == 1;
這個純函數只干一件事情:判斷輸入是不是奇數。
那么現在問題來了,如果我們有一個整數列表,要怎么去做上面這件事情呢?
可能會有人說這太簡單了,這樣就可:
var list = new List<int>(); return list.Select(IsOdd).ToList();
上面這句干了件什么事情呢?其實就是:我們將 IsOdd 函數應用到了列表中的每一個元素上,將產生的新的列表返回。
現在我們做一次抽象,我們將這個列表想象成一個箱子M,那么我們的需要干的事情就是:把一個裝着 A 類型東西的箱子變成一個裝着 B 類型東西的箱子(A、B類型可相同),即 fmap函數,而做這個變化的方法就是:進入箱子M,把里面的A變成B。
它分別接收一個把東西從A變成B的函數、一個裝着A的M,產生一個裝着B的M。
M<B> Fmap(this M<A> input, Func<A, B> func);
你暫且可以簡單地認為,判斷一個箱子是不是 Functor,就是判斷它有沒有 fmap這個操作。
Maybe
我們應該都接觸過 C# 的 Nullable<T>類型,比如 Nullable<int> t,或者寫成 int? t,這個t,當里面的值為 null 時,它為 null,否則他為包含的值。
此時我們把這個 Nullable<T>想象成這個箱子 M。那么我們可以這么說,這個M有兩種形式,一種是 Just<T>,表示有值,且值在 Just 里面存放;另一種是 Nothing,表示沒有值。
用 Haskell 寫這個Nullable<T>類型定義的話,大概長這個樣子:
data Nullable x = Just x | Nothing
而之所以這個Nullable<T>既可能是 Nothing,又可能是 Just<T>,只是因為 C# 的 BCL 中包含相關的隱式轉換而已。
由於自帶的 Nullable<T>不太好具體講我們的各種實現,且只接受值類型的數據,因此我們自己實現一個Maybe<T>:
public class Maybe<T> where T : notnull { private readonly T innerValue; public bool HasValue { get; } = false; public T Value => HasValue ? innerValue : throw new InvalidOperationException(); public Maybe(T value) { if (value is null) return; innerValue = value; HasValue = true; } public Maybe(Maybe<T> value) { if (!value.HasValue) return; innerValue = value.Value; HasValue = true; } private Maybe() { } public static implicit operator Maybe<T>(T value) => new Maybe<T>(value); public static Maybe<T> Nothing() => new Maybe<T>(); public override string ToString() => HasValue ? Value.ToString() : "Nothing"; }
對於 Maybe<T>,我們可以寫一下它的 fmap函數:
public static Maybe<B> Fmap<A, B>(this Maybe<A> input, Func<A, B> func) => input switch { null => Maybe<B>.Nothing(), { HasValue: true } => new Maybe<B>(func(input.Value)), _ => Maybe<B>.Nothing() }; Maybe<int> t1 = 7; Maybe<int> t2 = Maybe<int>.Nothing(); Func<int, bool> func = x => (x & 1) == 1; t1.Fmap(func); // Just True t2.Fmap(func); // Nothing
Applicative
有了上面的東西,現在我們說說 Applicative 是干什么的。
你可以非常容易的發現,如果你為 Maybe<T>實現一個 fmap,那么你可以說 Maybe<T>就是一個 Functor。
那 Applicative 也差不多,首先Applicative是繼承自Functor的,所以Applicative本身就具有了 fmap。另外在 Applicative中,我們有兩個分別叫做pure和 apply的函數。
pure干的事情很簡單,就是把東西裝到箱子里:
M<T> Pure<T>(T input);
那 apply 干了件什么事情呢?想象一下這件事情,此時我們把之前所說的那個用於變換的函數(Func<A, B>)也裝到了箱子當中,變成了M<Func<A, B>>,那么apply所做的就是下面這件事情:
M<B> Apply(this M<A> input, M<Func<A, B>> func);
看起來和 fmap沒有太大的區別,唯一的不同就是我們把func也裝到了箱子M里面。
以 Maybe<T>為例實現 apply:
public static Maybe<B> Apply<A, B>(this Maybe<A> input, Maybe<Func<A, B>> func) => (input, func) switch { _ when input is null || func is null => Maybe<B>.Nothing(), ({ HasValue: true }, { HasValue: true }) => new Maybe<B>(func.Value(input.Value)), _ => Maybe<B>.Nothing() };
然后我們就可以干這件事情了:
Maybe<int> input = 3; Maybe<Func<int, bool>> isOdd = new Func<int, bool>(x => (x & 1) == 1); input.Apply(isOdd); // Just True
我們的這個函數 isOdd本身可能是 Nothing,當 input和isOdd任何一個為Nothing的時候,結果都是Nothing,否則是Just,並且將值存到這個 Just里面。
Monad
Monad 繼承自 Applicative,並另外包含幾個額外的操作:returns、bind和then。
returns干的事情和上面的Applicative中pure干的事情沒有區別。
public static Maybe<A> Returns<A>(this A input) => new Maybe<A>(input);
bind干這么一件事情 :
M<B> Bind<A, B>(this M<A> input, Func<A, M<B>> func);
它用一個裝在 M中的A,和一個A -> M<B>這樣的函數,產生一個M<B>。
then用來充當膠水的作用,將一個個操作連接起來:
M<B> Then(this M<A> a, M<B> b);
為什么說這是充當膠水的作用呢?想象一下如果我們有兩個 Monad,那么使用 then,就可以將上一個 Monad和下一個Monad利用函數組合起來將其連接,而不是寫為兩行語句。
實現以上操作:
public static Maybe<B> Bind<A, B>(this Maybe<A> input, Func<A, Maybe<B>> func) => input switch { { HasValue: true } => func(input.Value), _ => Maybe<B>.Nothing() }; public static Maybe<B> Then<A, B>(this Maybe<A> input, Maybe<B> next) => next;
完整Maybe<T>實現
public class Maybe<T> where T : notnull { private readonly T innerValue; public bool HasValue { get; } = false; public T Value => HasValue ? innerValue : throw new InvalidOperationException(); public Maybe(T value) { if (value is null) return; innerValue = value; HasValue = true; } public Maybe(Maybe<T> value) { if (!value.HasValue) return; innerValue = value.Value; HasValue = true; } private Maybe() { } public static implicit operator Maybe<T>(T value) => new Maybe<T>(value); public static Maybe<T> Nothing() => new Maybe<T>(); public override string ToString() => HasValue ? Value.ToString() : "Nothing"; } public static class MaybeExtensions { public static Maybe<B> Fmap<A, B>(this Maybe<A> input, Func<A, B> func) => input switch { null => Maybe<B>.Nothing(), { HasValue: true } => new Maybe<B>(func(input.Value)), _ => Maybe<B>.Nothing() }; public static Maybe<B> Apply<A, B>(this Maybe<A> input, Maybe<Func<A, B>> func) => (input, func) switch { _ when input is null || func is null => Maybe<B>.Nothing(), ({ HasValue: true }, { HasValue: true }) => new Maybe<B>(func.Value(input.Value)), _ => Maybe<B>.Nothing() }; public static Maybe<A> Returns<A>(this A input) => new Maybe<A>(input); public static Maybe<B> Bind<A, B>(this Maybe<A> input, Func<A, Maybe<B>> func) => input switch { { HasValue: true } => func(input.Value), _ => Maybe<B>.Nothing() }; public static Maybe<B> Then<A, B>(this Maybe<A> input, Maybe<B> next) => next; }
以上方法可以自行柯里化后使用,以及我調換了一些參數順序便於使用,所以可能和定義有所出入。
有哪些常見的 Monads
- Maybe
- Either
- Try
- Reader
- Writer
- State
- IO
- List
- ......
C# 中有哪些 Monads
Task<T>Nullable<T>IEnumerable<T>+SelectMany- ......
為什么需要 Monads
想象一下,現在世界上只有一種函數:純函數。它接收一個參數,並且對於每一個參數值,給出固定的返回值,即 f(x)對於相同參數恆不變。
那現在問題來了,如果我需要可空的值 Maybe或者隨機數Random等等,前者除了值本身之外,還帶有一個是否有值的狀態,而后者還跟計算機的運行環境、時間等隨機數種子的因素有關。如果我們所有的函數都是純函數,那么我們如何用一個函數去產生 Maybe 和 Random 呢?
前者可能只需要給函數增加一個參數:是否有值,然而后者呢?牽扯到時間、硬件、環境等等一切和產生隨機數種子有關的狀態,我們當然可以將所有狀態都當作參數傳入,然后生成一個隨機數,那更復雜的,IO如何處理?
這類函數都是與環境和狀態密切相關的,狀態是可變的,並不能簡單的由參數做映射產生固定的結果,即這類函數具有副作用。但是,我們可以將狀態和值打包起來裝在箱子里,這個箱子即 Monad,這樣我們所有涉及到副作用的操作都可以在這個箱子內部完成,將可變的狀態隔離在其中,而對外則為一個單體,仍然保持了其不變性。
以隨機數 Random為例,我們想給隨機數加 1。(下面的代碼我就用 Haskell 放飛自我了)
我們現在已經有兩個函數,nextRandom用於產生一個 Random Int,plusOne用於給一個 Int 加 1:
nextRandom :: Random Int // 返回值類型為 Random Int plusOne :: Int -> Int // 參數類型為 Int,返回值類型為 Int
然后我們有 bind和returns操作,那我們只需要利用着兩個操作將我們已有的兩個函數組合即可:
bind (nextRandom (returns plusOne))
利用符號表示即為:
nextRandom >>= plusOne
這樣我們將狀態等帶有副作用的操作全部隔離在了 Monad 中,我們接觸到的東西都是不變的,並且滿足 f(g(x)) = g(f(x))!
當然這個例子使用Monad的bind操作純屬小題大做,此例子中只需要利用Functor的 fmap操作能搞定:
fmap plusOne nextRandom
利用符號表示即為:
plusOne <$> nextRandom
