如何实现不短路、延迟到值使用时收集错误的Haskell Monad实例?
实现非短路错误收集的解决方案
要实现你需要的非短路错误收集(仅在值被实际使用时触发短路并返回所有累积错误),核心问题在于Monad的>>=语义天然依赖前序计算的结果,无法直接做到非短路的错误累积。但我们可以通过GHC的ApplicativeDo语言扩展,让do语句编译为Applicative风格的操作,从而复用你已实现的Errors类型Applicative实例,完美满足需求。
代码实现
1. 定义Errors类型及基础实例
{-# LANGUAGE ApplicativeDo #-} -- 启用ApplicativeDo扩展 newtype Errors e a = Errors { runErrors :: ([e], Maybe a) } deriving (Show, Eq) -- Functor实例:仅映射值,保留错误列表 instance Functor (Errors e) where fmap f (Errors errs val) = Errors errs (fmap f val) -- Applicative实例:累积双方错误,仅当双方都有值时才计算结果 instance Semigroup e => Applicative (Errors e) where pure x = Errors [] (Just x) Errors e1 f <*> Errors e2 x = Errors (e1 <> e2) (f <*> x)
2. 四种期望行为场景示例
-- 场景1:所有步骤成功,返回空错误列表和成功值 allSuccess :: Errors String Int allSuccess = do a <- pure 10 b <- pure 20 pure (a + b) -- runErrors allSuccess → ([], Just 30) -- 场景2:某步骤出错,但未使用错误值,返回累积错误和成功值 unusedError :: Errors String Int unusedError = do _ <- Errors ["参数格式错误"] Nothing -- 错误值未被使用 b <- pure 50 pure b -- runErrors unusedError → (["参数格式错误"], Just 50) -- 场景3:某步骤出错且使用错误值,返回累积错误和失败结果 usedError :: Errors String Int usedError = do a <- Errors ["数值超出范围"] Nothing -- 错误值被后续计算使用 b <- pure 30 pure (a + b) -- runErrors usedError → (["数值超出范围"], Nothing) -- 场景4:多个步骤出错,返回所有累积错误和失败结果 multipleErrors :: Errors String Int multipleErrors = do a <- Errors ["用户名不能为空"] Nothing b <- Errors ["密码长度不足"] Nothing pure (a + b) -- runErrors multipleErrors → (["用户名不能为空", "密码长度不足"], Nothing)
为什么选择ApplicativeDo?
- 你的
Errors类型的Applicative实例已经实现了错误累积的核心逻辑:<*>会同时计算左右两边的结果,收集所有错误,仅当两边都有有效时值才生成结果。 - ApplicativeDo扩展允许你用熟悉的
do语法编写Applicative风格的代码,无需实现Monad实例(而Monad的>>=语义无法做到非短路的错误累积,强行实现会违反Monad法则或引入运行时风险)。
内容的提问来源于stack exchange,提问作者hyllos
相关产品推荐
相关产品推荐

