为何Haskell中(>>)不定义为(*>)?GHC现有实现存疑
(>>)不使用(*>)实现? 这是个非常敏锐的问题,你已经抓住了(>>)和(*>)在类型兼容上的特点,也摸到了类型类语义设计的关键点,下面我来拆解几个核心原因:
1. 历史优先级:Monad早于Applicative诞生
Haskell的类型类有明确的发展时间线:Monad在1990年代就被引入标准库,而Applicative直到2010年左右才正式加入,并且后来才确立了Monad作为Applicative子类的关系。
最初设计(>>)的时候,(*>)这个操作根本不存在——连Applicative类型类都还没诞生,自然不可能用一个尚未出现的函数来实现(>>)。即便现在类型类体系更完善了,为了向后兼容老代码、保留历史设计的连贯性,这个默认实现也一直被保留了下来。
2. 避免循环依赖的风险
现在Applicative的(*>)默认实现其实是依赖Monad的(>>)的:
(*>) :: Applicative f => f a -> f b -> f b (*>) = (>>)
如果我们把(>>)的默认实现改成依赖(*>),就会形成一个死循环:(>>)调用(*>),而(*>)又调用(>>)。这会导致编译器无法解析默认实现,除非每个Monad实例都手动实现(*>)或者(>>),这无疑增加了不必要的开发复杂度。
3. 语义确定性与开发者控制权
虽然从类型类法则上来说,合法的Monad实例必须满足m *> k = m >>= \_ -> k,但实际开发中,开发者可能会为Applicative自定义(*>)的实现(比如为了性能优化)。如果(>>)依赖(*>),那么(>>)的行为就会被(*>)的自定义逻辑影响,这可能违背开发者对Monad绑定操作的预期——毕竟(>>)本质上是Monad绑定操作忽略结果的简化版,开发者更希望它直接和>>=的行为保持强一致。
反过来,用>>=实现(>>)的话,只要>>=的实现符合Monad法则,(>>)的行为就完全确定,不需要依赖Applicative层的任何自定义逻辑,这给了Monad实例开发者更直接的控制权。
补充:关于类型语义的疑问
你提到“或许Monad实例存在Applicative实例不具备的计算逻辑,但这可能破坏类型语义”——其实根据Monad作为Applicative子类的法则,Monad的计算逻辑必须完全兼容Applicative的语义:pure对应return,<*>对应ap,*>对应>>。所以合法的实例不会出现语义不一致的情况,但默认实现的选择还是要考虑历史、依赖和灵活性这些现实因素。
内容的提问来源于stack exchange,提问作者Yolo Voe

