學習函數式編程 Monad

上一篇文章中,咱們討論了經常使用的函數式編程案例,一些同窗反饋沒有講到底層概念,想了解一下什麼是 Monad?基於這個問題,咱們來探究一下。javascript

在函數式編程中,Monad 是一種結構化程序的抽象,咱們經過三個部分來理解一下。前端

  • Monad 定義
  • Monad 使用場景
  • Monad 一句話解釋

Monad 定義

根據維基百科的定義,Monad 由如下三個部分組成:java

  • 一個類型構造函數(M),能夠構建出一元類型 M<T>
  • 一個類型轉換函數(return or unit),可以把一個原始值裝進 M 中。ajax

    • unit(x) : T -> M T
  • 一個組合函數 bind,可以把 M 實例中的值取出來,放入一個函數中去執行,最終獲得一個新的 M 實例。編程

    • M<T> 執行 T-> M<U> 生成 M<U>

除此以外,它還遵照一些規則:promise

  • 單位元規則,一般由 unit 函數去實現。
  • 結合律規則,一般由 bind 函數去實現。
單位元:是 集合裏的一種特別的元素,與該集合裏的 二元運算有關。當單位元和其餘元素結合時,並不會改變那些元素。

乘法的單位元就是 1,任何數 x 1 = 任何數自己、1 x 任何數 = 任何數自己。安全

加法的單位元就是 0,任何數 + 0 = 任何數自己、0 + 任何數 = 任何數自己。網絡

這些定義很抽象,咱們用一段 js 代碼來模擬一下。異步

class Monad {
  value = "";
  // 構造函數
  constructor(value) {
    this.value = value;
  }
  // unit,把值裝入 Monad 構造函數中
  unit(value) {
    this.value = value;
  }
  // bind,把值轉換成一個新的 Monad
  bind(fn) {
    return fn(this.value);
  }
}

// 知足 x-> M(x) 格式的函數
function add1(x) {
  return new Monad(x + 1);
}
// 知足 x-> M(x) 格式的函數
function square(x) {
  return new Monad(x * x);
}

// 接下來,咱們就能進行鏈式調用了
const a = new Monad(2)
                    .bind(square)
                    .bind(add1);
                    //...

console.log(a.value === 5); // true

上述代碼就是一個最基本的 Monad,它將程序的多個步驟抽離成線性的流,經過 bind 方法對數據流進行加工處理,最終獲得咱們想要的結果。函數式編程

Ok,咱們已經明白了 Monad 的內部結構,接下來,咱們再看一下 Monad 的使用場景。

Monad 使用場景

經過 Monad 的規則,衍生出了許多使用場景。

  • 組裝多個函數,實現鏈式操做。

    • 鏈式操做能夠消除中間狀態,實現 Pointfree 風格。
    • 鏈式操做也能避免多層函數嵌套問題 fn1(fn2(fn3()))
    • 若是你用過 rxjs,就能體會到鏈式操做帶來的快樂。
  • 處理反作用。

    • 包裹異步 IO 等反作用函數,放在最後一步執行。

還記得 Jquery 時代的 ajax 操做嗎?

$.ajax({
  type: "get",
  url: "request1",
  success: function (response1) {
    $.ajax({
      type: "get",
      url: "request2",
      success: function (response2) {
        $.ajax({
          type: "get",
          url: "request3",
          success: function (response3) {
            console.log(response3); // 獲得最終結果
          },
        });
      },
    });
  },
});

上述代碼中,咱們經過回調函數,串行執行了 3 個 ajax 操做,但一樣也生成了 3 層代碼嵌套,這樣的代碼不只難以閱讀,也不利於往後維護。

Promise 的出現,解決了上述問題。

fetch("request1")
  .then((response1) => {
    return fetch("request2");
  })
  .then((response2) => {
    return fetch("request3");
  })
  .then((response3) => {
    console.log(response3); // 獲得最終結果
  });

咱們經過 Promise,將多個步驟封裝到多個 then 方法中去執行,不只消除了多層代碼嵌套問題,並且也讓代碼劃分更加天然,大大提升了代碼的可維護性。

想想,爲何 Promise 能夠不斷執行 then 方法?

其實,Promise 和 Monad 很相似,它知足了多條 Monad 規則。

  1. Promise 自己就是一個構造函數。
  2. Monad 中的 unit,在 Promise 中能夠看爲: x => Promise.resolve(x)
  3. Monad 中的 bind,在 Promise 中能夠看爲:Promise.prototype.then

咱們用代碼來驗證一下。

// 首先定義 2 個異步處理函數。

// 延遲 1s 而後 加一
function delayAdd1(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(x + 1);
    });
  }, 1000);
}

// 延遲 1s 而後 求平方
function delaySquare(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(x * x);
    });
  }, 1000);
}
/****************************************************************************************/

// 單位元 e 規則,知足:e*a = a*e = a
const promiseA = Promise.resolve(2).then(delayAdd1);
const promiseB = delayAdd1(2);
// promiseA === promiseB,故 promise 知足左單位元。

const promiseC = Promise.resolve(2);
const promiseD = a.then(Promise.resolve);
// promiseC === promiseD,故 promise 知足右單位元。

// promise 既知足左單位元,又知足右單位元,故 Promise 知足單位元。
// ps:但一些特殊的狀況不知足該定義,下文中會講到

/****************************************************************************************/

// 結合律規則:(a * b)* c = a *(b * c)
const promiseE = Promise.resolve(2);
const promiseF = promiseE.then(delayAdd1).then(delaySquare);
const promiseG = promiseE.then(function (x) {
  return delayAdd1(x).then(g);
});

// promiseF === promiseG,故 Promise 是知足結合律。
// ps:但一些特殊的狀況不知足該定義,下文中會講到

看完上面的代碼,不由感受很驚訝,Promise 和 Monad 也太像了吧,不只能夠實現鏈式操做,也知足單位元和結合律,難道 Promise 就是一個 Monad?

其實否則,Promise 並不徹底知足 Monad:

  • Promise.resolve 若是傳入一個 Promise 對象,會等待傳入的 Promise 執行,並將執行結果做爲外層 Promise 的值。
  • Promise.resolve 在處理 thenable 對象時,一樣不會直接返回該對象,會將對象中的 then 方法當作一個 Promise 等待結果,並做爲外層 Promise 的值。

若是是這兩種狀況,那就沒法知足 Monad 規則。

// Promise.resolve 傳入一個 Promise 對象
const functionA = function (p) {
  // 這時 p === 1
  return p.then((n) => n * 2);
};
const promiseA = Promise.resolve(1);
Promise.resolve(promiseA).then(functionA);
// RejectedPromise TypeError: p.then is not a function
// 因爲 Promise.resolve 對傳入的 Promise 進行了處理,致使直接運行報錯。違背了單位元和結合律。

// Promise.resolve 傳入一個 thenable 對象
const functionB = function (p) {
  // 這時 p === 1
  alert(p);
  return p.then((n) => n * 2);
};
const obj = {
  then(r) {
    r(1);
  },
};
const promiseB = Promise.resolve(obj);
Promise.resolve(promiseB).then(functionB);
// RejectedPromise TypeError: p.then is not a function
// 因爲 Promise.resolve 對傳入的 thenable 進行了處理,致使直接運行報錯。違背了單位元和結合律。

看到這裏,相信你們對 Promise 也有了一層新的瞭解,正是藉助了 Monad 同樣的鏈式操做,才使 Promise 普遍應用在了前端異步代碼中,你是否也和我同樣,對 Monad 充滿了好感?

Monad 處理反作用

接下來,咱們再看一個常見的問題:爲何 Monad 適合處理反作用?

ps:這裏說的反作用,指的是違反 純函數原則的操做,咱們應該儘量避免這些操做,或者把這些操做放在最後去執行。

例如:

var fs = require("fs");

// 純函數,傳入 filename,返回 Monad 對象
var readFile = function (filename) {
  // 反作用函數:讀取文件
  const readFileFn = () => {
    return fs.readFileSync(filename, "utf-8");
  };
  return new Monad(readFileFn);
};

// 純函數,傳入 x,返回 Monad 對象
var print = function (x) {
  // 反作用函數:打印日誌
  const logFn = () => {
    console.log(x);
    return x;
  };
  return new Monad(logFn);
};

// 純函數,傳入 x,返回 Monad 對象
var tail = function (x) {
  // 反作用函數:返回最後一行的數據
  const tailFn = () => {
    return x[x.length - 1];
  };
  return new Monad(tailFn);
};

// 鏈式操做文件
const monad = readFile("./xxx.txt").bind(tail).bind(print);
// 執行到這裏,整個操做都是純的,由於反作用函數一直被包裹在 Monad 裏,並無執行
monad.value(); // 執行反作用函數

上面代碼中,咱們將反作用函數封裝到 Monad 裏,以保證純函數的優良特性,巧妙地化解了反作用存在的安全隱患。

Ok,到這裏爲止,本文的主要內容就已經分享完了,但在學習 Monad 中的某一天,忽然發現有人用一句話就解釋清楚了 Monad,自嘆不如,簡直太厲害了,咱們一塊兒來看一下吧!

Warning:下文的內容偏數學理論,不感興趣的同窗跳過便可。

Monad 一句話解釋

早在 10 多年前,Philip Wadler 就對 Monad 作了一句話的總結。

原文:_A monad is a monoid in the category of endofunctors_。

翻譯:Monad 是一個 自函子 範疇 上的 幺半羣」 。

這裏標註了 3 個重要的概念:自函子、範疇、幺半羣,這些都是數學知識,咱們分開理解一下。

  • 什麼是範疇?

任何事物都是對象,大量的對象結合起來就造成了集合,對象和對象之間存在一個或多個聯繫,任何一個聯繫就叫作態射。

一堆對象,以及對象之間的全部態射所構成的一種代數結構,便稱之爲 範疇

  • 什麼是函子?

咱們將範疇與範疇之間的映射稱之爲 函子。映射是一種特殊的態射,因此函子也是一種態射。

  • 什麼是自函子?

自函子就是一個將範疇映射到自身的函子。

  • 什麼是幺半羣 Monoid?

幺半羣是一個存在 單位元半羣

  • 什麼是半羣?

若是一個集合,知足結合律,那麼就是一個半羣

  • 什麼是單位元?

單位元是集合裏的一種特別的元素,與該集合裏的二元運算有關。當單位元和其餘元素結合時,並不會改變那些元素。

如:
任何一個數 + 0 = 這個數自己。 那麼 0 就是單位元(加法單位元)
任何一個數 * 1 = 這個數自己。那麼 1 就是單位元(乘法單位元)

Ok,咱們已經瞭解了全部應該掌握的專業術語,那就簡單串解一下這段解釋吧:

一個 自函子 範疇 上的 幺半羣 ,能夠理解爲,在一個知足結合律和單位元規則的集合中,存在一個映射關係,這個映射關係能夠把集合中的元素映射成當前集合自身的元素。

相信掌握了這些理論知識,確定會對 Monad 有一個更加深刻的理解。

總結

本文從 Monad 的維基百科開始,逐步介紹了 Monad 的內部結構以及實現原理,並經過 Promise 驗證了 Monad 在實戰中發揮的重大做用。

文中包含了許多數學定義、函數式編程的理論等知識,大可能是參考網絡資料和自我經驗得出的,若是有錯誤的地方,還望你們多多指點 🙏

最後,若是你對此有任何想法,歡迎留言評論!

相關文章
相關標籤/搜索