Picture of the author

Monad的理解
2025-09-10
web开发者眼中的Monad

haskell断断续续学了好几年,今年终于看到了Monad Transformer。也终于感觉对Monad有一个完整的理解,以下的记录,只为了我以后能回忆起这段时间的感觉,不做对外分享😅,主要是没有能力

初次接触Monad

刚开始编程,学的javascript, 当时有个叫promise的对象,当时为了解决回调地狱,引入了它,那时看来很神奇,也很先进,后来也有了async,await。 promise的then有点像bind(>>=),又有点像map。记得当时看了一篇文章说,then扁平化的promise,和join比较类似,那时也第一次听到了haskell,也了解到promise是一个monad,这是我对monad的第一印象

const getAcc = new Promise((resolve, reject) => {
  resolve(1)
})
.then((v) => {
  return v + 1
})
.then((v) => {
  return Promise.resolve(v+1)
})


(async function () {
  const v = await getAcc;
})()

前端应该是对monad最熟悉的,因为每天都在使用。 而async await就像monad的do表达式,每后一行都是可以引用前几行的变量定义

fn :: Num a => Maybe a
fn = do
  a <- Just 1
  b <- Just 2
  return a + b
const a = await Promise.resolve(1)
const b = await Promise.resolve(2)
// console.log(a + b)

Promise.resolve(1).then((a) => Promise.resolve(2).then(b => a + b))

这样do表达式和命令行语句有点类似,当然只是在读取上下文时类似

Monad Transformer

data MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) } deriving (Functor)

一开始学Transformer时,总搞不清楚签名的顺序和类型类实现的顺序,就如上面这个,MaybeT在外,而m在内,但是实现有时m在外,Maybe在内。 后来想通了,把monad想成一个容器,我们从容器中取值,需要先把内部容器打开,拿到值后应用外部转化器。 所以为什么是这个签名m (Maybe a),简单言之因为我们先要处理m,万一失败,那它还是一个m。 像 Maybe、Either 这样由多形式构造器定义的类型,在参与 Monad 组合时,Nothing 零元构造器会使组合它的 monad 转换器的行为消失。 而如果组合的多个 Monad 均仅通过单一构造器定义,并且在定义只引用了返回类型参数一次,如 State、Writer、Reader 等,那么在使用相对应的转换器组合时,它们之间组合的顺序可以交换而不影响组合后 Monad 的功能。 简言而止,单一构造器,可以交换顺序,有交换律。

同时Monad Transformer也让我想到,函数的compose,koa的洋葱模型,以及服务器程序的AOP切面编程,切面编程本质就是函数的不可变性,只能用外层包裹内层来处理,也是设计准则里的SOLID吻合, 单一职责原则 (SRP, Single Responsibility Principle),开闭原则 (OCP, Open/Closed Principle),关注点分离 (Separation of Concerns, SoC) 这些都是Monad Transformer的在设计上和传统语言的趋同。

Misc

假如Maybe不是Show的实例,同时也不支持解构模式匹配,但是又要在main这个IO里面打印Maybe包含的值,只能通过IO Maybe IO a来执行副作用

结束语

每种Monad都可以研究很长时间,但是只要抓住上面这些核心要点,能很快的上手不同的Monad。 此外,在日常编程中,之前不懂的设计模式,为什么有洋葱模型,本质上,因为haskell的数据不可变性导致的,从数据角度出发,而不是从程序角度,能更清楚的看清为什么这么设计。