一個Monad的不嚴謹介紹

来源:http://www.cnblogs.com/skabyy/archive/2017/05/26/6896421.html
-Advertisement-
Play Games

一個單子(Monad)說白了不過就是自函子範疇上的一個么半群而已,這有什麼難以理解的? " " 之前瞭解了下Monad,後來一段時間沒碰,最近研究Parser用到Monad時發現又不懂了。現在重新折騰,趁著記憶還熱乎,趕緊寫下來。本文不會完整講解Monad,而只介紹Monad相關的思想與編程技巧。 ...


一個單子(Monad)說白了不過就是自函子範疇上的一個么半群而已,這有什麼難以理解的?*

之前瞭解了下Monad,後來一段時間沒碰,最近研究Parser用到Monad時發現又不懂了。現在重新折騰,趁著記憶還熱乎,趕緊寫下來。本文不會完整講解Monad,而只介紹Monad相關的思想與編程技巧。

不要被唬人的數學概念嚇唬到了。對於程式員來說,Monad不過就是一種編程技巧,或者說是一種設計模式。 Monad並非Haskell特有。實際上,大部分語言都有應用過Monad的思想。下麵我將主要使用Scheme來解釋Monad。

Monad是什麼

Monad是一種數據類型,它有以下兩個特點:

  • Monad封裝了一個值。

    這個封裝的含義比較廣義,它既可以是用數據結構包涵了一個值,也可以是一個函數(通過返回值來表達被封裝的值)。所以一般也說Monad是一個“未計算的值”、“包含在上下文(context)中的值”。

  • 存在兩個Monad相關的函數: 提升(return函數)與綁定(>>=函數)。

    -- 提升 --
    return :: a -> M a
    -- 綁定 --
    >>= :: M a -> (a -> M b) -> M b

    代碼中ab表示兩種數據類型,M a表示封裝了a類型的Monad類型,M b表示封裝了b類型的Monad類型。提升函數將一個值封裝成一個Monad。而綁定函數就像一個管道,它解封一個Monad,將裡面的值傳到第二個參數表示的函數,生成另一個Monad。

以上是一個粗淺的定義。想要進一步瞭解的朋友可以去查看維基的Monad詞條。

另外有一點要註意,Monad的兩個操作中的提升操作做了封裝,但是並沒有提供解封的操作(M a -> a類型的操作)。下圖展示了Monad兩個操作的關係:

monad

下麵我們來看看Monad的應用。

Maybe

Maybe是最簡單,也是最常被提起的一個例子。Maybe類似C#中的Nullabe類型,表示有一個值,或者沒有值。我們可以在Scheme這樣表示Maybe類型:

; 有一個值
(define (just a) `(Just ,a))
; 沒有值
(define nothing 'Nothing)

可以看到,Maybe類型封裝了值a,只缺提升和綁定操作就可以作為Monad了。定義提升和綁定如下:

; 提升
(define return just)
; 綁定
(define (>>= ma f)
  (if (eq? ma nothing)
    nothing
    (f (cadr ma))))

接下來我們看一個求倒數的例子。我們定義一個inv函數,該函數接收一個數字x作為參數。當x等於0時,輸出Nothing;當x不為0時,計算x的倒數1/x,並封裝為(Just 1/x)

(define (inv x)
  (if (zero? x) nothing (return (/ 1.0 x))))

定義完inv後,我們就能通過>>=將它應用到Maybe類型來求倒數了。測試一下:

(pretty-print (>>= (just 10) inv))
; > (Just 0.1)

(pretty-print (>>= (just 0) inv))
; > Nothing

(pretty-print (>>= nothing inv))
; > Nothing

Maybe這個例子還揭示了為什麼Monad沒有粗暴地提供一個解封的函數:並非所有Monad都能解封,(Just a)能解封,但是Nothing不能解封!因此只能通過綁定函數來訪問封裝裡面的值。

狀態

Monad最出名的用法是模擬狀態。眾所周知,Haskell是一門純函數語言,因而Haskell不得不大量使用Monad來模擬副作用。然而,Monad也僅僅是模擬,而非真正實現了副作用。應用了Monad技巧的函數仍然是純函數。王垠在他的《對函數式語言的誤解》準確了描述了Monad模擬副作用的本質:

為了讓 random 在每次調用得到不同的輸出,你必須給它“不同的輸入”。那怎麼才能給它不同的輸入呢?Haskell 採用的辦法,就是把“種子”作為輸入,然後返回兩個值:新的隨機數和新的種子,然後想辦法把這個新的種子傳遞給下一次的 random 調用。

現在問題來了。得到的這個新種子,必須被準確無誤的傳遞到下一個使用 random 的地方,否則你就沒法生成下一個隨機數。因為沒有地方可以讓你“暫存”這個種子,所以為了把種子傳遞到下一個使用它的地方,你經常需要讓種子“穿過”一系列的函數,才能到達目的地。種子經過的“路徑”上的所有函數,必須增加一個參數(舊種子),並且增加一個返回值(新種子)。這就像是用一根吸管扎穿這個函數,兩頭通風,這樣種子就可以不受干擾的通過。

為了減輕視覺負擔和維護這些進進出出的“狀態”,Haskell 引入了一種叫 monad 的概念。它的本質是使用類型系統的“重載”(overloading),把這些多出來的參數和返回值,掩蓋在類型裡面。這就像把亂七八糟的電線塞進了接線盒似的,雖然錶面上看起來清爽了一些,底下的複雜性卻是不可能消除的。

雖然用Monad模擬狀態既複雜、用處也不多,但是學習一下既有樂趣又不乏啟發,所以姑且來看一下事情是怎麼做的。

為了調試與演示方便,我們這裡不用random函數作為例子,而是實現一個sequence函數。該函數不接收參數,每次調用的返回值都是上一次的返回值加1。

我們先考慮沒有實用Monad的情況。在這種情況下,sequence函數以及其他所有相關的函數需要一個狀態參數,並返回返回值與新狀態兩個值。現在我們考慮Monad的類型。我們要把返回的新狀態隱藏起來,很自然的思路就是將新狀態當作用來封裝返回值的Monad殼子(也可以理解為這個新狀態表達了一個上下文)。用一個pair來表示這個封裝:

(cons value new-state)

另外,還有一個要隱藏的,就是輸入到函數的狀態參數。如何將參數隱藏到Monad比較費腦。事實上,在我們編寫函數代碼時,我們根本就不知道這個狀態參數是從哪裡傳過來的,我們對狀態參數一無所知。既然我們對這個狀態參數一無所知,那我們對這個狀態參數的處理就是先不處理,等程式執行到這裡的時候再計算(這有點像惰性求值,聯想下非惰性求值語言是怎麼實現惰性求值的?),也就是說,我們要把與狀態參數相關的計算過程整個封裝起來,只有獲取到狀態參數時才能解封得到實際的值。用什麼來表示“計算過程”呢?答案是函數(lambda)。到這裡就清晰了,要同時隱藏返回值、返回的新狀態以及狀態參數,我們需要的Monad類型是個函數類型,它大概長這個樣子:

old-state -> (cons value new-state)
; type: number -> number * number

接下來定義提升函數,提升函數返回輸入的值i,並保持狀態不變:

 (define (return i)
   (lambda (state) (cons i state)))

綁定函數先利用狀態參數state解封m計算得m中的值與新狀態,再將f應用到解封得到的值和新的狀態

(define (>>= m f)
  (lambda (state)
    (let ([p (m state)])
      ((f (car p)) (cdr p)))))

為了實現sequence函數,我們還需要一個獲取狀態的函數get-state和一個“設置”狀態的函數set-stateget-state返回狀態值並保持狀態不變。set-state接收一個參數,將狀態設置為該參數,並返回(void)。代碼如下:

(define (get-state)
  (lambda (state) (cons state state)))
(define (set-state state)
  (lambda (old-state) (cons (void) state)))

萬事俱備!可以來實現sequence了。sequence依次做了以下事情:

  1. 獲取狀態state
  2. 設置新狀態為state+1
  3. 返回state+1

代碼如下:

(define (sequence)
  (>>= (get-state)
       (lambda (state)
         (>>= (set-state (+ state 1))
              (lambda (_)
                (return (+ state 1)))))))

為了簡化嵌套回調,我寫了一個巨集來處理嵌套回調:

(define-syntax do/m
  (syntax-rules (<-)
    [(_ bind e) e]
    [(_ bind (v <- e0) e e* ...)
     (bind e0 (lambda (v)
                (do/m bind e e* ...)))]
    [(_ bind e0 e e* ...)
     (bind e0 (lambda (_)
                (do/m bind e e* ...)))]))

這樣sequence的實現可以簡化為:

(define (sequence1)
  (do/m >>=
        (state <- (get-state))
        (set-state (+ state 1))
        (return (+ state 1))))

有沒有很像命令式的寫法?下麵來測試一下:

; 方便展示用的輔助函數,請忽視它是個有副作用的函數。
(define (printi v) (return (pretty-print v)))

(define run-program
  (do/m >>=
        (i1 <- (sequence))
        (i2 <- (sequence))
        (printi i1)
        (printi i2)
        (i3 <- (sequence))
        (printi i3)))

註意到這裡的Monad是一個接受狀態參數的函數,我們要傳入初始的狀態參數來讓這段代碼真正跑起來。我們傳入初始狀態0

(run-program 0)

;output:
; > 1
; > 2
; > 3

其他應用

Continuation

熟悉continuation的朋友可以看出continuation也是一種Monad。

JavaScript

根據JavaScript面向對象的特性,綁定函數可以定義為Monad的一個方法。下麵定義了一個簡單的Monad類型,它單純封裝了一個值作為value屬性:

var Monad = function (v) {
    this.value = v;
    return this;
};

Monad.prototype.bind = function (f) {
    return f(this.value)
};

var lift = function (v) {
    return new Monad(v);
};

我們將一個除以2的函數應用的這個Monad:

console.log(lift(32).bind(function (a) {
    return lift(a/2);
}));

// > Monad { value: 16 }

是不是有點像Promise?

連續應用除以2的函數:

// 方便展示用的輔助函數,請忽視它是個有副作用的函數。
var print = function (a) {
    console.log(a);
    return lift(a);
};

var half = function (a) {
    return lift(a/2);
};

lift(32)
    .bind(half)
    .bind(print)
    .bind(half)
    .bind(print);
    
//output:
// > 16
// > 8

這是鏈式編程。

結尾

Monad雖然曲高和寡,但其思想悄悄地融入到了各個語言中。本文到此結束,希望對你能有所幫助。

相關鏈接

Wiki的Monad詞條

Functor、Applicative 和 Monad

對函數式語言的誤解

陳年譯稿——一個面向Scheme程式員的monad介紹


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

-Advertisement-
Play Games
更多相關文章
  • 控制套接字的行為(如修改緩衝區的大小)。 int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen); int setsockopt(int sockfd,int level,int optname, ...
  • 作者Brett Slatkin是 Google公司高級軟體工程師。他是Google消費者調查項目的工程主管及聯合創始人,曾從事Google App Engine的Python基礎架構工作,並利用Python來管理眾多的Google伺服器。Slatkin也是PubSubHubbub協議的聯合創始人,還 ...
  • “註解”這個詞,可謂是在Java編程中出鏡率比較高,而且也是一個老生常談的話題。我們之前在聊Spring相關的東西時,註解是無處不在,之前我們簡單的聊過一些“註解”的相關內容,比如在Spring中是如何進行“註解”組合的。因為註解在Java編程中還是比較重要的,所以我們今天的博客就把註解的東西給系統 ...
  • 問題:Firemonkey Android 平臺顯示斜粗體文字時,文字右方會有顯示不全的問題。 修正代碼: 請將 FMX.FontGlyphs.Android.pas 複製到自己的工程目錄下,再修改如下代碼: 修正效果: ...
  • 數據源連接池配置 ...
  • 列印流 在整個 包中,列印流是輸出信息最方便的類,主要包含 位元組列印流 ( )和 字元列印流 ( )。列印流提供了非常方便的列印功能,可以列印任何的數據類型,例如:小數、整數、字元串等等,相對於前面學習的幾個文件的操作來說,這裡的列印流是最簡便的一個類了 PrintStream 主要功能是格式化的將 ...
  • 記憶體操作流 之前的所有的流操作都是針對文件的,但是有時候只是想要實現數據間轉換,此時如果我們想要創建一個文件然後再刪除文件,那樣顯得有點麻煩,因此此時的記憶體操作流就顯得很適合這類的操作,因為它只是在記憶體中存儲,並不會真正的創建文件,記憶體操作流涉及的兩個類是 ,`ByteArrayOutputStre ...
  • 在上一回合談到,客戶端應用程式的所有操作都在主線程上進行,所以一些比較耗時的操作可以在非同步線程上去進行,充分利用CPU的性能來達到程式的最佳性能。對於Unity而言,又提供了另外一種『非同步』的概念,就是協程( ),通過反編譯,它本質上還是在主線程上的優化手段,並不屬於真正的多線程( )。那麼問題來了 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...