阅读 677

不完整解释 Monad 有什么用

【预警】这篇文章没有详细解释 Monad,我承诺我会抽空写。

关于我为什么自己打脸回来写文章,不想解释太多。只想说掘金真香。

本打算这周末写文章解释下 Monad 的,但是最近比较忙,还是再拖一会。

新工作挑战比较大。第一次遇到这么复杂的业务和开发流程,一开始适应的不是很好。开会全程懵逼,不知道别人在讲什么。最近主要精力还是要花在熟悉业务和新的工作环境,学习分享上会缓一缓。

先简单介绍一下 Monad 的用处预热一下吧。可能看完这篇你不会全懂,那是因为我没仔细解释,留待下次吧,抱歉了。这里要展示的代码主体部分是我的练习改写,后半部辅助函数和示例是我模仿改写的。

function IO(effectFn) {
  const __val = effectFn
  const map = fn => IO(() => fn(__val()))
  const performUnsafeIO = __val
  const chain = fn => IO(() => fn(__val()).performUnsafeIO())
  return Object.freeze({
    map,
    chain,
    performUnsafeIO,
  })
}

const curry = fn => (...args) =>
  args.length >= fn.length ? fn(...args) : curry(fn.bind(undefined, ...args))

const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))

const map = curry((fn, monad) => monad.map(fn))

const chain = curry((fn, monad) => monad.chain(fn))

const setStyle = curry((sel, props) => IO(() => $(sel).css(props)))

const getItem = key => IO(() => localStorage.getItem(key))

const applyPreferences = compose(
  chain(setStyle('#main')),
  map(JSON.parse),
  getItem
)

applyPreferences('preferences').performUnsafeIO()
复制代码

这个 IO Monad 在一些 ADT 库里面也叫 Effect,它是来处理应用中的作用的。先看示例部分。这个应用的主要功能就是从 localStorage 读取用户的样式偏好,读到之后再改掉页面对应的样式。这个简单例子涉及到两个作用(effects),注意作用和副作用(side effects)是两个不同的概念。这两个作用是读取数据库和改变 DOM 节点样式属性。函数式编程的一个主要挑战就是把计算和作用分离开来,计算的过程中不能产生作用。

回到代码看是怎样做到的。首先 getItem 函数把根据传入的 key 读取 localStorage 的行为扔进了 IO 函数。IO 函数把这个会产生作用的里层函数存在闭包里,并没有立即执行。map 的作用就是,先执行传进 IO 的函数,再把计算结果传进 map 自身接受的回调函数。但是请注意,map 并没有立即执行会产生作用的函数,它只是声明了行为。接着,到了最难理解的 chain 函数(如果你理解了 chain 在干什么,你就完全理解 Monad 了)。chain 接受的回调函数自身也会返回一个 IO,这个时候就不能直接把回调函数执行的结果扔回给 IO 了,不然就是 IO 嵌套 IO,没办法 map 了。所以先把里层 IO 的作用函数执行一遍,再把结果塞回 IO。同样,这里只是声明行为,没有真的执行。

程序运行到 applyPreferences("preferences") 的时候,就把应用功能全部描述完了,但只是定义了每一步的计算,还没开始执行指令。到最后一部 performUnsafeIO 的时候,奇迹才会发生,作用才会释放。再回过头看整个程序,是不是觉得很干净?不管你有没感受到,反正我感受到了……

你可能会问:谁 TM 这样子写代码找抽啊!!!

其实,RxJS 的原理差不多就是这样的。Observable 就是个 IO Monad。RxJS 里面声明的计算,都是惰性的,只有在最后 subscribe 的时候,计算才会被触发,作用才会被释放。

本文示例主要目的是演示,还是过于简单化。注意 JSON.parse 可能会抛出异常,而抛出异常也属于作用。有一个叫 Maybe 的数据类型专用来解决这类问题。我以前在一篇介绍 Ramda 的文章最后面有演示 Maybe 的用法。

线上 Demo 戳这里

这篇文章也发表在我的中文博客上 Lambda Academy

关注下面的标签,发现更多相似文章
评论