前一陣子在寫 CPU,致使一直沒有什麼時間去作其餘的事情,如今好不容易作完閒下來了,我又能夠水文章了哈哈哈哈哈。express
有關 FP 的類型部分我打算放到明年再講,由於現有的 C# 雖然有一個 pattern matching expressions
,可是沒有 discriminated unions
和 records
,只能說是個半殘廢,要實現 FP 那一套的類型異常的複雜。西卡西,discriminated unions
和 records
這兩個東西官方已經定到 C# 9 了,因此等明年 C# 9 發佈了以後我再繼續說這部分的內容。數組
另外,concepts
(type classes
)、traits
、intersect & sum types
和高階類型也可能會隨着 C# 九、10 一併到來。所以到時候再講纔會講得更爽。另外吹一波 traits
類型系統,一樣是圖靈完備的類型系統,在表達力上要比OOP
強太多,歡迎你們入坑,好比 Rust 和將來的 C#。app
這一部分咱們介紹一下 Functor
、Applicative
和 Monad
都是些什麼。dom
本文試圖直觀地講,目的是讓讀者能比較容易的理解,而不是準確知道其概念如何,所以會盡可能避免使用一些專用的術語,如範疇學、數學、λ 計算等等裏面的東西。感興趣的話建議參考其餘更專業的資料。ide
Functor 也叫作函子。想象一下這樣一件事情:函數
如今咱們有一個純函數 IsOdd
this
bool IsOdd(int value) => (value & 1) == 1;
這個純函數只幹一件事情:判斷輸入是否是奇數。spa
那麼如今問題來了,若是咱們有一個整數列表,要怎麼去作上面這件事情呢?code
可能會有人說這太簡單了,這樣就可:blog
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
這個操做。
咱們應該都接觸過 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
是幹什麼的。
你能夠很是容易的發現,若是你爲 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 繼承自 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; }
以上方法能夠自行柯里化後使用,以及我調換了一些參數順序便於使用,因此可能和定義有所出入。
Task<T>
Nullable<T>
IEnumerable<T>
+SelectMany
想象一下,如今世界上只有一種函數:純函數。它接收一個參數,而且對於每個參數值,給出固定的返回值,即 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