Haskell中do记法是否总是基于Monad实现?
问题背景
此前一直认为Haskell的do记法只是Monad组合(>>=)的语法糖,直到遇到如下代码示例,无法确定其中使用了哪种Monad,甚至不确定是否存在对应Monad:
rollDieThreeTimes :: (Die, Die, Die) rollDieThreeTimes = do let s = mkStdGen 0 (d1, s1) = randomR (1, 6) s (d2, s2) = randomR (1, 6) s1 (d3, _) = randomR (1, 6) s2 (intToDie d1, intToDie d2, intToDie d3) intToDie :: Int -> Die -- 函数实现省略
核心疑问:
- 该示例里的
do记法底层是否真的存在对应的Monad? - 如果不存在对应Monad,是否存在特殊形式的
do记法,允许这种无Monad场景下的let语句写法? - 如果存在对应Monad,具体是哪一种?如何将这段
do记法转换为>>=组合形式?
解答
核心结论:这段代码里的do块完全没有用到任何Monad的类型类方法,不存在绑定到具体Monad实例的逻辑,它能正常编译是Haskelldo记法脱糖规则的自然结果,不是什么特殊语法特例。
do记法的标准脱糖规则
Haskell标准对do块的脱糖是逐行递归处理的,核心规则共4条:
- 单表达式的do块:
do { e }直接等价于e,不做任何包装 - 带let声明的do块:
do { let 绑定组; 剩余语句 }等价于let 绑定组 in do { 剩余语句 },直接把let绑定提升到外层let表达式 - 带
<-绑定的do块:do { 模式 <- 表达式; 剩余语句 }等价于表达式 >>= \模式 -> do { 剩余语句 },只有这条规则会用到Monad的>>= - 带无绑定单行表达式的do块:
do { 表达式; 剩余语句 }等价于表达式 >> do { 剩余语句 },这条规则会用到Monad的>>
对应代码的脱糖过程
把示例中的do块逐一套规则拆解:
- 首先处理块开头的let绑定组,按照第二条规则直接把let提升到外层,原块变成:
let s = mkStdGen 0 (d1, s1) = randomR (1, 6) s (d2, s2) = randomR (1, 6) s1 (d3, _) = randomR (1, 6) s2 in do { (intToDie d1, intToDie d2, intToDie d3) } - 剩下的内层do块只有单个三元组表达式,按照第一条规则直接脱掉do,最终整个块等价于:
rollDieThreeTimes :: (Die, Die, Die) rollDieThreeTimes = let s = mkStdGen 0 (d1, s1) = randomR (1, 6) s (d2, s2) = randomR (1, 6) s1 (d3, _) = randomR (1, 6) s2 in (intToDie d1, intToDie d2, intToDie d3)
整个脱糖过程从头到尾没有出现>>=或者>>,自然不需要任何Monad实例参与类型检查。
三个疑问的直接回应
- 针对疑问1:这个do块底层没有对应任何Monad,因为脱糖全程没有用到Monad类型类的任何方法,完全是普通的let绑定逻辑。
- 针对疑问2:不存在“无Monad场景的特殊do形式”,这就是标准do脱糖规则的正常表现——只有do块里出现
<-绑定或者无绑定的单行表达式时,才会触发对Monad方法的调用,全let+单返回值的do块会被直接脱成普通的let表达式,和Monad没有任何关系。你甚至可以写do 1 + 2这种代码,它脱糖完就是3,类型为Num a => a,根本不需要Monad约束。 - 针对疑问3:不存在对应的Monad,自然也没法转成
>>=组合的形式——这段代码本质上就是写了个多余的do关键字包裹普通let逻辑,和写id (let x=1 in x)里多余的id是一个道理。
补充说明:很多入门教程会简化说“do是Monad的语法糖”,这个说法不算错但不完整——do是通用的块结构语法,只有涉及到绑定操作的时候才会依赖Monad方法,纯let组成的do块只是块结构的普通写法,不依赖Monad。
内容的提问来源于stack exchange,提问作者mherzl
相关产品推荐
相关产品推荐

