天天看點

Promise是Monad嗎?

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

原文:

Functional Computational Thinking — What is a monad? 譯者: Fundebug

為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用于學習。

如果你使用函數式程式設計,不管有沒有用過函數式語言,在某總程度上已經使用過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

接收

f

g

兩個參數,然後傳回值是一個函數。該函數接收一個參數

x

, 先将函數

g

作用到

x

, 其傳回值作為另一個函數

f

的輸入。

addOneThenMul3

是我們通過

composeF

定義的一個新的函數:由

mul3

add1

複合而成。

接下來看另一個實際的例子:我們有兩個檔案,第一個檔案存儲了第二個檔案的路徑,第二個檔案包含了我們想要取出來的内容。使用剛剛定義的複合函數

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。

我們通過如下函數讀取檔案内容:

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

的全棧BUG監控交流群: 622902485。

Promise是Monad嗎?

版權聲明:

轉載時請注明作者Fundebug以及本文位址:

https://blog.fundebug.com/2017/06/21/write-monad-in-js/#more

繼續閱讀