Haskell中单个monad对应的monad transformer是否唯一?
结论
同一个monad并不对应唯一的monad transformer,完全可以存在多个行为不同的transformer,在作用于Identity monad时得到完全一致的基monad,但作用于其他monad时表现出可观测的行为差异。
反例判定规则
若两个monad transformer满足以下两个条件,即可作为非唯一性的有效证明:
- 二者应用于
Identitymonad时,生成的monad行为完全一致(存在合法monad同构)- 存在至少一个其他monad
m,使得二者应用于m时生成的monad行为存在可观测差异
示例无需具备工程实用性,若有实际应用价值则参考性更强。
最简可验证反例
我们以NonEmpty(非空列表)monad为例,构造两个完全符合monad定律的合法transformer:
第一个:标准NonEmptyT
这是Haskell生态中通用的非空列表transformer实现:
import Data.List.NonEmpty (NonEmpty(..)) newtype NonEmptyT m a = NonEmptyT { runNonEmptyT :: m (NonEmpty a) }
- 作用于
Identitymonad时,类型为Identity (NonEmpty a),与原生NonEmpty a完全同构,行为一致。 - 该实现提供符合定律的
lift操作与Monad实例,是生产环境中常用的transformer实现。
第二个:AltNonEmptyT
我们构造另一个结构完全不同的transformer:
newtype AltNonEmptyT m a = AltNonEmptyT { runAltNonEmptyT :: (m a, [m a]) }
它的合法Monad实例与transformer标准lift操作实现如下:
instance Monad m => Monad (AltNonEmptyT m) where return a = AltNonEmptyT (return a, []) AltNonEmptyT (mHead, mTail) >>= f = let lift ma = AltNonEmptyT (ma, []) (newHead, newTailFromHead) = runAltNonEmptyT $ f =<< lift mHead tailResults = map (runAltNonEmptyT . f <=< lift) mTail newTail = newTailFromHead ++ concatMap (\(h, t) -> h : t) tailResults in AltNonEmptyT (newHead, newTail) lift :: Monad m => m a -> AltNonEmptyT m a lift ma = AltNonEmptyT (ma, [])
- 作用于
Identitymonad时,类型为(Identity a, [Identity a]),与原生NonEmpty a(定义为a :| [a])完全同构,return、(>>=)等所有monad操作的行为和标准NonEmpty monad完全一致。
二者的可观测行为差异
将两个transformer应用到IO monad时,行为差异非常明显:
NonEmptyT IO a的本质是单次IO动作,执行后一次性返回所有非空结果,所有结果的生成逻辑都被包裹在同一个IO过程中,无法拆分执行,也无法在拿到部分结果后决定后续逻辑。AltNonEmptyT IO a的本质是一个首元素IO动作加多个独立的尾部元素IO动作,你可以单独执行首元素IO拿到第一个结果,再按需决定是否执行、执行多少个尾部IO动作,甚至可以提前终止不执行后续动作,和标准NonEmptyT的行为有本质区别。
该反例完全符合判定规则,且两个transformer都严格满足monad与monad transformer的所有定律要求。
内容的提问来源于stack exchange,提问作者QuantumWiz
相关产品推荐
相关产品推荐

