关于(Mega)Parsec中错误通过延续累积/传播机制的技术问询
作为一个经常和Parsec打交道的Haskell开发者,我来拆解一下ParsecT作为MonadPlus实例时,mplus操作是怎么通过延续(continuations)来处理错误累积和传播的——这也是Parsec错误消息比很多简陋解析器友好的关键原因。
首先得明确mplus m n的核心语义:先尝试用解析器m处理输入字符串s,如果m可恢复地失败(比如预期某个Token没找到,但还能回溯尝试其他解析路径),就再试n;如果m是致命失败(比如输入提前结束,根本没法继续解析),直接返回错误,不试n。而延续在这里的作用,就是把“成功/失败后的处理逻辑”作为参数传递,让错误信息能在解析路径之间传递和合并。
先理清楚四个延续函数的角色
在Parsec的内部实现中,每个解析器运行时都会接收四个延续回调:
cok:成功延续——解析成功时触发,接收解析结果、剩余输入、当前解析状态,返回最终的解析输出cerr:可恢复失败延续——解析失败但可以回溯时触发,接收当前的错误信息、剩余输入、状态,返回最终输出eok:用于处理解析成功但带有预期提示的场景(比如可选解析成功时),不过在mplus里核心用到的是前三个加eerreerr:致命失败延续——解析遇到无法恢复的错误(比如输入突然结束)时触发,接收致命错误信息,直接终止解析
mplus的具体执行逻辑
当调用mplus m n处理输入s时,它会先启动m解析s,并给m传入一套定制的延续:
如果
m解析成功(触发cok):
直接调用原本的cok返回结果,完全忽略n——这符合MonadPlus的“左选择”语义,只要左边成功就用左边的结果。如果
m触发cerr(可恢复失败):
这时候不会直接返回错误,而是先把m产生的错误信息暂存,然后启动n解析同一个输入s,同时给n传入另一套延续:- 若
n成功触发cok:直接用n的结果,相当于m失败后 fallback 到n成功的情况 - 若
n也触发cerr:把m的错误和n的错误合并(比如合并预期的Token列表、保留最具体的错误位置等),然后调用原本的cerr返回合并后的错误——这就是错误累积的核心!用户最终会看到所有尝试过的解析路径的错误提示,比如“预期a或b” - 若
n触发eerr:直接返回n的致命错误,因为这种错误已经没法继续回溯了
- 若
如果
m触发eerr(致命失败):
直接调用原本的eerr返回错误,不尝试n——因为致命错误意味着输入已经处于无法继续解析的状态,试n也没用。
错误累积的关键:延续传递错误上下文
这里的核心思路是用延续把错误信息从失败的解析器传递到后续的解析器,而不是在每个解析器失败时直接抛出错误。当多个解析器依次失败时,每个解析器的错误都会被收集起来,最后合并成一个更友好的错误消息——这也是为什么Parsec的错误消息能准确告诉你“我尝试了这些可能性,但都失败了”,而不是只告诉你最后一个尝试的解析器失败了。
举个实际的例子:假设你用string "foo" <|> string "bar"解析输入"baz",第一个解析器string "foo"会在第一个字符失败,错误是“预期'f'”;第二个解析器string "bar"也会在第一个字符失败,错误是“预期'b'”。mplus会把这两个错误合并,最终返回的错误消息是“预期'f'或'b'”——这就是延续传递错误并累积的结果。
内容的提问来源于stack exchange,提问作者fmg

