拿 C# 搞函數式編程 - 2

来源:https://www.cnblogs.com/hez2010/archive/2019/12/08/12008677.html
-Advertisement-
Play Games

前一陣子在寫 CPU,導致一直沒有什麼時間去做其他的事情,現在好不容易做完閑下來了,我又可以水文章了哈哈哈哈哈。 有關 FP 的類型部分我打算放到明年再講,因為現有的 C# 雖然有一個 pattern matching expressions,但是沒有 discriminated unions 和  ...


前一陣子在寫 CPU,導致一直沒有什麼時間去做其他的事情,現在好不容易做完閑下來了,我又可以水文章了哈哈哈哈哈。

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

另外,conceptstype classes)、traits 、intersect & sum types 和高階類型也可能會隨著 C# 9、10 一併到來。因此到時候再講才會講得更爽。另外吹一波 traits類型系統,同樣是圖靈完備的類型系統,在表達力上要比OOP強太多,歡迎大家入坑,比如 Rust 和未來的 C#。

這一部分我們介紹一下 FunctorApplicative和 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 類型東西的箱子(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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本篇文章我們主要探討 一下如果 語句中有 ,這種情況下 語句還會執行嗎?其實JVM規範是對這種情況有特殊規定的,那我就先上代碼吧! 對於上述代碼,我們有以下幾個問題,來自測一下吧: 1. 如果在 try 語句塊里使用 return 語句,那麼 finally 語句塊還會執行嗎? 2. 如果執行,那麼 ...
  • 題目要求: 分析文件’課程成績.xlsx’,至少要完成內容:分析1)每年不同班級平均成績情況、2)不同年份總體平均成績情況、3)不同性別學生成績情況,並分別用合適的圖表展示出三個內容的分析結果。 廢話不多,直接上代碼 1每年不同班級平均成績情況: # 導入xlrd模塊import xlrdfrom ...
  • 前言本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理。作者:weixin_45189038直接上知識點: 1. 註釋 單行註釋:在一行文字前面加#(快捷鍵:ctrl+/) 多行註釋:將註釋內容寫在三個英文雙引號或者單引號裡面(但是一 ...
  • 這個問題,是python與Pycharm不相容導致,解決辦法將Pycharm升級最新版本 ...
  • Shiro是一個功能強大且易於使用的Java安全框架,主要功能有身份驗證、授權、加密和會話管理,本文實現一個簡單的身份驗證例子。 ...
  • 人生從來沒有固定的路線,決定你能夠走多遠的,並不是年齡,而是你的努力程度。無論到了什麼時候,只要你還有心情對著糟糕的生活揮拳宣戰,都不算太晚。遲做,總比不做好! ...
  • # 集美大學各省錄取分數分析(學號尾數為2,3同學完成) # 分析文件‘集美大學各省錄取分數.xlsx’,完成: # 1)集美大學2015-2018年間不同省份在本一批的平均分數,柱狀圖展示排名前10的省份, # 2)分析福建省這3年各批次成績情況,使用折線圖展示結果,並預測2019年錄取成績(數據... ...
  • 本文中 $n$ 代表著待排序序列的長度。 演算法是否穩定:若 $a_i = a_j \ , \ i 1; merge(l,mid),merge(mid+1,r); mergesort(l,r,mid);return;//遞歸,先給小區間排序後大區間。 } merge(1,n); 上張圖理解一下: 可用 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...