如何让嵌套的Megaparsec解析器按预期触发失败?
问题解答
1. fail调用未终止整个解析的原因
这个问题是MP.many组合子的默认行为和Megaparsec的错误类型设计共同导致的:
- Megaparsec的错误分为两类:消耗输入的致命失败和未消耗输入的普通失败。
fail调用产生的是未消耗输入的普通失败,这类失败会被<|>、many这类具备容错能力的组合子默认捕获。 MP.many p的执行逻辑是:反复运行解析器p,一旦p返回未消耗输入的失败就直接停止,返回之前已经解析到的所有结果,不会把内部的普通失败向上抛出为全局错误。
在你测试的ABC&D场景中,解析流程是这样的:
- 解析到
&时,regular分支的MP.satisfy isAdmissibleChar不匹配,返回未消耗输入的失败 - 接着执行
<|>右侧的fail分支,仍然返回未消耗输入的普通失败 - 整个
escaped <|> regular解析器返回普通失败,触发MP.many停止执行,直接返回已经解析到的ABC,不会抛出全局错误。
2. 实现方案优化
你的整体解析思路是合理的,只是没有明确区分「合法终止条件(未转义的控制字符)」和「非法输入(不可接受字符)」,这是这类终止型字符串解析的常见误区。
这类场景的规范实现可以用MP.manyTill组合子,明确指定停止条件为未转义的控制终止符,同时把不可接受字符的错误设为消耗性的致命错误,避免被组合子吞掉,修改后的参考代码如下:
import qualified Data.Text as T import qualified Text.Megaparsec as MP import qualified Text.Megaparsec.Char as MC import Data.List.NonEmpty as NE pComponent :: Parser (Maybe Text) pComponent = do t <- MP.manyTill (escaped <|> regular) stopCondition pure $ if null t then Nothing else Just (T.pack t) where -- 合法停止条件:前瞻到未转义的控制字符,或者到达输入末尾 stopCondition = MP.lookAhead (MP.satisfy isControlChar) <|> MP.eof regular = do c <- MP.anySingle case () of _ | isAdmissibleChar c -> pure c _ | isControlChar c -> -- 控制字符属于终止条件,返回未消耗失败触发manyTill停止 MP.unexpected (MP.Label $ MP.fromList "control character") _ -> -- 不可接受字符抛出致命错误,不会被组合子吞掉 MP.failure (Just $ MP.Tokens $ NE.singleton c) mempty escaped = do _ <- MC.char escChar MP.satisfy isControlChar <?> "escaped control character"
这个实现的优势在于:
- 停止条件显式声明,逻辑更清晰易维护
- 不可接受字符会直接抛出全局错误,符合需求预期
- 转义逻辑和原逻辑保持一致,仅允许转义控制字符
内容的提问来源于stack exchange,提问作者Ulrich Schuster
相关产品推荐
相关产品推荐

