为何使用monad transformers时表达式结果会随参数顺序变化?
问题原因解析
这个现象的核心是Monad Transformer 实例的短路规则和求值策略,和你手动拆包后组合的规则完全不同,我们逐层拆解:
基础定义梳理
你定义的类型F[X] = OptionT[Either[String, *], X]本质是对Either[String, Option[X]]的包装,但是 Cats 为OptionT提供的Monad/Applicative实例有固定的短路优先级:
- 优先检查是否存在
Option层的None,只要出现None就直接短路,不再执行后续计算 - 其次检查是否存在
Either层的Left,出现Left也会短路后续计算
且基于 Monad 实现的 Applicative 是严格按顺序求值,短路后后面的表达式完全不会被计算。
三个示例的执行逻辑拆解
第一个示例:(success, empty, failed).tupled.value返回Right(None)
执行顺序严格按参数顺序走:
- 第一步求值
success,结果为Right(Some(1)),无短路,继续执行 - 第二步求值
empty,结果为Right(None),触发OptionT层的None短路,后续的failed完全不会被求值,所以Left("Boom!")根本没有机会出现 - 最终直接返回
Right(None)
第二个示例:(success, failed, empty).tupled.value返回Left(Boom!)
执行顺序调整后:
- 第一步求值
success通过,继续执行 - 第二步求值
failed,结果为Left("Boom!"),触发Either层的Left短路,后续的empty不会被求值 - 直接返回
Left("Boom!")
第三个示例:(success.value, empty.value, failed.value).tupled返回Left(Boom!)
这里和前两个示例的核心区别是:你在调用tupled之前,已经主动对三个表达式都调用了.value,三个值都已经完成了求值,不管顺序如何,failed.value的Left("Boom!")已经被计算出来了。
此时你调用的是Either的tupled方法,它的短路规则是按顺序检查所有已求值的Either,碰到第一个Left就返回,所以走到第三个参数时就会返回Left("Boom!")。
补充说明
Monad Transformer 本身是对堆叠 Monad 的合法封装,它的行为完全符合 Monad 定律,只是使用时需要明确两个核心点:
- Transformer 堆叠的顺序决定了短路的优先级
- 基于 Monad 的组合都是懒求值的,短路后后续代码不会执行
如果需要先计算所有值再做组合,要么提前拆包求值,要么使用对应 Transformer 的Parallel实例做并行求值,就不会出现顺序导致的结果差异。
内容的提问来源于stack exchange,提问作者Piotr Jósiak
相关产品推荐
相关产品推荐

