Promise是Monad吗?

简介: 译者按: 近年来,函数式语言的特性都被其它语言学过去了。原文: Functional Computational Thinking — What is a monad?译者: Fundebug为了保证可读性,本文采用意译而非直译。

译者按: 近年来,函数式语言的特性都被其它语言学过去了。

原文: 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接收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。

我们通过如下函数读取文件内容:

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

img_5e583cf022f6093999a0e7b71ca1638b.jpe

版权声明:
转载时请注明作者Fundebug以及本文地址:
https://blog.fundebug.com/2017/06/21/write-monad-in-js/#more

目录
相关文章
|
JavaScript 前端开发 容器
promise is a monad?
态射是范畴内对象之间的映射关系。函子与它类似,函子是范畴与范畴间的映射关系,也就是可以通过一个函子,把一个范畴映射到另一个范畴。
1759 0
|
前端开发 JavaScript 开发者
前端开发中的异步编程:Promise 和 Async/Await 的比较与应用
在现代前端开发中,异步编程是不可或缺的技术。本文将深入探讨Promise和Async/Await这两种主流的异步编程方式,分析它们的优劣势及在实际项目中的应用场景。通过比较它们的语法、可读性和错误处理机制,帮助开发者更好地选择和理解如何在项目中高效地利用这些技术。
|
2月前
|
前端开发 JavaScript API
一文吃透 Promise 与 async/await,异步编程也能如此简单!建议收藏!
在前端开发中,异步编程至关重要。本文详解了同步与异步的区别,通过生活化例子帮助理解。深入讲解了 Promise 的概念、状态及链式调用,并引入 async/await 这一语法糖,使异步代码更清晰易读。还介绍了多个异步任务的组合处理方式,如 Promise.all 与 Promise.race。掌握这些内容,将大幅提升你的异步编程能力,写出更优雅、易维护的代码,助力开发与面试!
166 0
一文吃透 Promise 与 async/await,异步编程也能如此简单!建议收藏!
|
2月前
|
前端开发 JavaScript API
JavaScript异步编程:从Promise到async/await
JavaScript异步编程:从Promise到async/await
378 204
|
11月前
|
前端开发 JavaScript 开发者
Async 和 Await 是基于 Promise 实现
【10月更文挑战第30天】Async和Await是基于Promise实现的语法糖,它们通过简洁的语法形式,借助Promise的异步处理机制,为JavaScript开发者提供了一种更优雅、更易于理解和维护的异步编程方式。
204 1
|
8月前
|
前端开发
使用 async/await 结合 try/catch 处理 Promise.reject()抛出的错误时,有什么需要注意的地方?
使用 async/await 结合 try/catch 处理 Promise.reject()抛出的错误时,有什么需要注意的地方?
333 57
|
11月前
|
前端开发
如何使用async/await解决Promise的缺点?
总的来说,`async/await` 是对 Promise 的一种很好的补充和扩展,它为我们提供了更高效、更易读、更易维护的异步编程方式。通过合理地运用 `async/await`,我们可以更好地解决 Promise 的一些缺点,提升异步代码的质量和开发效率。
243 64
|
11月前
|
前端开发 JavaScript
async/await和Promise在性能上有什么区别?
性能优化是一个综合性的工作,除了考虑异步模式的选择外,还需要关注代码的优化、资源的合理利用等方面。
276 63
|
11月前
|
JSON 前端开发 JavaScript
浅谈JavaScript中的Promise、Async和Await
【10月更文挑战第30天】Promise、Async和Await是JavaScript中强大的异步编程工具,它们各自具有独特的优势和适用场景,开发者可以根据具体的项目需求和代码风格选择合适的方式来处理异步操作,从而编写出更加高效、可读和易于维护的JavaScript代码。
240 1