Promise是Monad嗎?

譯者按: 近年來,函數式語言的特性都被其它語言學過去了。javascript

爲了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原做者全部,翻譯僅用於學習。java

若是你使用函數式編程,無論有沒有用過函數式語言,在某總程度上已經使用過Monad。可能大多數人都不知道什麼叫作Monad。在這篇文章中,我不會用數學公式來解釋什麼是Moand,也不使用Haskell,而是用JavaScript直接寫Monad。程序員

做爲一個函數式程序員,我首先來介紹一下基礎的複合函數:編程

const add1 = x => x + 1
const mul3 = x => x * 3

const composeF = (f, g) => {
  return x => f(g(x))
}

const addOneThenMul3 = composeF(mul3, add1)
console.log(addOneThenMul3(4)) // 打印 15
複製代碼

複合函數composeF接收fg兩個參數,而後返回值是一個函數。該函數接收一個參數x, 先將函數g做用到x, 其返回值做爲另外一個函數f的輸入。小程序

addOneThenMul3是咱們經過composeF定義的一個新的函數:由mul3add1複合而成。微信小程序

接下來看另外一個實際的例子:咱們有兩個文件,第一個文件存儲了第二個文件的路徑,第二個文件包含了咱們想要取出來的內容。使用剛剛定義的複合函數composeF, 咱們能夠簡單的搞定:數組

const readFileSync = path => {
  return fs.readFileSync(path.trim()).toString()
}

const readFileContentSync = composeF(readFileSync, readFileSync)
console.log(readFileContentSync('./file1'))
複製代碼

readFileSync是一個阻塞函數,接收一個參數path,並返回文件中的內容。咱們使用composeF函數將兩個readFileSync複合起來,就達到咱們的目的。是否是很簡潔?微信

但若是readFile函數是異步的呢?若是你用Node.js 寫過代碼的話,應該對回調很熟悉。在函數式語言裏面,有一個更加正式的名字:continuation-passing style 或則 CPS。markdown

咱們經過以下函數讀取文件內容:異步

const readFileCPS = (path, cb) => {
  fs.readFile(
    path.trim(),
    (err, data) => {
      const result = data.toString()
      cb(result)
    }
  )
}
複製代碼

可是有一個問題:咱們不能使用composeF了。由於readCPS函數自己不在返回任何東西。 咱們能夠從新定義一個複合函數composeCPS,以下:

const composeCPS = (g, f) => {
  return (x, cb) => {
    g(x, y => {
      f(y, z => {
        cb(z)
      })
    })
  }
}

const readFileContentCPS = composeCPS(readFileCPS, readFileCPS)
readFileContentCPS('./file1', result => console.log(result))
複製代碼

注意:在composeCPS中,我交換了參數的順序。composeCPS會首先調用函數g,在g的回調函數中,再調用f, 最終經過cb返回值。

接下來,咱們來一步一步改進咱們定義的函數。

第一步,咱們稍微改寫一下readFIleCPS函數:

const readFileHOF = path => cb => {
  readFileCPS(path, cb)
}
複製代碼

HOF是 High Order Function (高階函數)的縮寫。咱們能夠這樣理解readFileHOF: 接收一個爲path的參數,返回一個新的函數。該函數接收cb做爲參數,並調用readFileCPS函數。

而且,定義一個新的複合函數:

const composeHOF = (g, f) => {
  return x => cb => {
    g(x)(y => {
      f(y)(cb)
    })
  }
}

const readFileContentHOF = composeHOF(readFileHOF, readFileHOF)
readFileContentHOF('./file1')(result => console.log(result))
複製代碼

第二步,咱們接着改進readFileHOF函數:

const readFileEXEC = path => {
  return {
    exec: cb => {
      readFileCPS(path, cb)
    }
  }
}
複製代碼

readFileEXEC函數返回一個對象,對象中包含一個exec屬性,並且exec是一個函數。

一樣,咱們再改進複合函數:

const composeEXEC = (g, f) => {
  return x => {
    return {
      exec: cb => {
        g(x).exec(y => {
          f(y).exec(cb)
        })
      }
    }
  }
}

const readFileContentEXEC = composeEXEC(readFileEXEC, readFileEXEC)
readFileContentEXEC('./file1').exec(result => console.log(result))
複製代碼

如今咱們來定義一個幫助函數:

const createExecObj = exec => ({exec})
複製代碼

該函數返回一個對象,包含一個exec屬性。 咱們使用該函數來優化readFileEXEC函數:

const readFileEXEC2 = path => {
  return createExecObj(cb => {
    readFileCPS(path, cb)
  })
}
複製代碼

readFileEXEC2接收一個path參數,返回一個exec對象。

接下來,咱們要作出重大改進,請注意! 迄今爲止,因此的複合函數的兩個參數都是huan'hnh函數,接下來咱們把第一個參數改爲exec對象。

const bindExec = (execObj, f) => {
  return createExecObj(cb => {
    execObj.exec(y => {
      f(y).exec(cb)
    })
  })
}
複製代碼

bindExec函數返回一個新的exec對象。

咱們使用bindExec來定義讀寫文件的函數:

const readFile2EXEC2 = bindExec(
  readFileEXEC2('./file1'),
  readFileEXEC2
)
readFile2EXEC2.exec(result => console.log(result))
複製代碼

若是不是很清楚,咱們能夠這樣寫:

bindExec(
  readFileEXEC2('./file1'),
  readFileEXEC2
)
.exec(result => console.log(result))
複製代碼

咱們接下來把bindExec函數放入exec對象中:

const createExecObj = exec => ({
  exec,
  bind(f) {
    return createExecObj(cb => {
      this.exec(y => {
        f(y).exec(cb)
      })
    })
  }
})
複製代碼

如何使用呢?

readFileEXEC2('./file1')
.bind(readFileEXEC2)
.exec(result => console.log(result))
複製代碼

這已經和在函數式語言Haskell裏面使用Monad幾乎如出一轍了。

咱們來作點重命名:

  • readFileEXEC2 -> readFileAsync
  • bind -> then
  • exec -> done
readFileAsync('./file1')
.then(readFileAsync)
.done(result => console.log(result))
複製代碼

發現了嗎?居然是Promise!

Monad在哪裏呢?

composeCPS開始,都是Monad.

  • readFIleCPS是Monad。事實上,它在Haskell裏面被稱做Cont Monad
  • exec 對象是一個Monad。事實上,它在Haskell裏面被稱做IO Monad

Monad 有什麼性質呢?

  1. 它有一個環境;
  2. 這個環境裏面不必定有值;
  3. 提供一個獲取該值的方法;
  4. 有一個bind函數能夠把值從第一個參數Monad中取出來,並調用第二個參數函數。第二個函數要返回一個Monad。而且該返回的Monad類型要和第一個參數相同。

數組也能夠成爲Monad

Array.prototype.flatMap = function(f) {
  const r = []
  for (var i = 0; i < this.length; i++) {
    f(this[i]).forEach(v => {
      r.push(v)
    })
  }
  return r
}

const arr = [1, 2, 3]
const addOneToThree = a => [a, a + 1, a + 2]

console.log(arr.map(addOneToThree))
// [ [ 1, 2, 3 ], [ 2, 3, 4 ], [ 3, 4, 5 ] ]

console.log(arr.flatMap(addOneToThree))
// [ 1, 2, 3, 2, 3, 4, 3, 4, 5 ]
複製代碼

咱們能夠驗證:

  1. [] 是環境
  2. []能夠爲空,值不必定存在;
  3. 經過forEach能夠獲取;
  4. 咱們定義了flatMap來做爲bind函數。

結論

  • Monad是回調函數 ? 根據性質3,是的。

  • 回調函數式Monad? 不是,除非有定義bind函數。

關於Fundebug

Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了6億+錯誤事件,獲得了Google、360、金山軟件等衆多知名用戶的承認。歡迎免費試用!

版權聲明

轉載時請註明做者Fundebug以及本文地址:
blog.fundebug.com/2017/06/21/…

相關文章
相關標籤/搜索