拿 C# 搞函數式編程 - 2

前一陣子在寫 CPU,致使一直沒有什麼時間去作其餘的事情,如今好不容易作完閒下來了,我又能夠水文章了哈哈哈哈哈。express

有關 FP 的類型部分我打算放到明年再講,由於現有的 C# 雖然有一個 pattern matching expressions,可是沒有 discriminated unions 和 records,只能說是個半殘廢,要實現 FP 那一套的類型異常的複雜。西卡西,discriminated unions 和 records 這兩個東西官方已經定到 C# 9 了,因此等明年 C# 9 發佈了以後我再繼續說這部分的內容。數組

另外,conceptstype classes)、traits 、intersect & sum types 和高階類型也可能會隨着 C# 九、10 一併到來。所以到時候再講纔會講得更爽。另外吹一波 traits類型系統,一樣是圖靈完備的類型系統,在表達力上要比OOP強太多,歡迎你們入坑,好比 Rust 和將來的 C#。app

這一部分咱們介紹一下 FunctorApplicative和 Monad 都是些什麼。dom

本文試圖直觀地講,目的是讓讀者能比較容易的理解,而不是準確知道其概念如何,所以會盡可能避免使用一些專用的術語,如範疇學、數學、λ 計算等等裏面的東西。感興趣的話建議參考其餘更專業的資料。ide

Functor

Functor 也叫作函子。想象一下這樣一件事情:函數

如今咱們有一個純函數 IsOddthis

bool IsOdd(int value) => (value & 1) == 1;

這個純函數只幹一件事情:判斷輸入是否是奇數。spa

那麼如今問題來了,若是咱們有一個整數列表,要怎麼去作上面這件事情呢?code

可能會有人說這太簡單了,這樣就可:blog

var list = new List<int>();
return list.Select(IsOdd).ToList();

上面這句幹了件什麼事情呢?其實就是:咱們將 IsOdd 函數應用到了列表中的每個元素上,將產生的新的列表返回。

如今咱們作一次抽象,咱們將這個列表想象成一個箱子M,那麼咱們的須要乾的事情就是:把一個裝着 A 類型東西的箱子變成一個裝着 B 類型東西的箱子(AB類型可相同),即 fmap函數,而作這個變化的方法就是:進入箱子M,把裏面的A變成B

它分別接收一個把東西從A變成B的函數、一個裝着AM,產生一個裝着BM

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,當 inputisOdd任何一個爲Nothing的時候,結果都是Nothing,不然是Just,而且將值存到這個 Just裏面。

Monad

Monad 繼承自 Applicative,並另外包含幾個額外的操做:returnsbindthen

returns乾的事情和上面的Applicativepure乾的事情沒有區別。

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 IntplusOne用於給一個 Int 加 1:

nextRandom :: Random Int // 返回值類型爲 Random Int
plusOne :: Int -> Int // 參數類型爲 Int,返回值類型爲 Int

而後咱們有 bindreturns操做,那咱們只須要利用着兩個操做將咱們已有的兩個函數組合便可:

bind (nextRandom (returns plusOne))

利用符號表示即爲:

nextRandom >>= plusOne

這樣咱們將狀態等帶有反作用的操做所有隔離在了 Monad 中,咱們接觸到的東西都是不變的,而且知足 f(g(x)) = g(f(x))

固然這個例子使用Monadbind操做純屬小題大作,此例子中只須要利用Functor的 fmap操做能搞定:

fmap plusOne nextRandom

利用符號表示即爲:

plusOne <$> nextRandom
相關文章
相關標籤/搜索