如何解决FParsec解析器中可变变量条件判断失效问题?
我在为处理括号内逗号分隔序列(如(a,b,c))的FParsec解析器添加错误恢复机制时遇到了问题:输入()符合语法规则,但(a,b,)不符合(序列未正确结束)。我已经实现了第二种场景的错误恢复及诊断,但错误恢复机制在第一种场景下产生了误报。
为区分这两种情况,我计划在解析右括号)时检查序列是否为空,于是尝试添加可变计数器,在解析序列时递增计数。但出现异常现象:计数器递增生效,但判断其值是否为0的逻辑未按预期工作。
我编写了简化程序复现该问题:
open FParsec type MyClass() = let mutable contextCounter = 0 member this.Increment() = contextCounter <- contextCounter + 1 member this.Reset() = contextCounter <- 0 member this.GetCounter() = contextCounter member this.Print(s:string) = printf "\ncurrent value is %i, %s after checking if equals 0" contextCounter s let x = pchar 'x' let y = pchar 'y' let z = pchar 'z' let c = skipChar ',' let ad = MyClass() let choicexyz (ad:MyClass) = if ad.GetCounter()=0 then choice [x;y;z] >>= fun result -> ad.Print("in if block") ad.Increment() preturn result else z >>= fun result -> ad.Print("in else block") ad.Increment() preturn result let parserSepBy (ad:MyClass) = ad.Reset() sepBy (choicexyz ad) c let inputSepBy = "x,x,y,y,z,z" let resultSepBy = run (parserSepBy ad) inputSepBy printf "\n%O" resultSepBy
程序运行输出:
current value is 0, in if block after checking if equals 0 current value is 1, in if block after checking if equals 0 current value is 2, in if block after checking if equals 0 current value is 3, in if block after checking if equals 0 current value is 4, in if block after checking if equals 0 current value is 5, in if block after checking if equals 0 Success: ['x'; 'x'; 'y'; 'y'; 'z'; 'z']
请问为何计数器递增生效,但判断其值是否为0的逻辑未触发else分支?
问题根源
FParsec的解析器是惰性构造的:你调用parserSepBy ad时,choicexyz ad会被立即求值一次,此时ad.GetCounter()返回0,所以choicexyz直接固定为choice [x;y;z] >>= ...这个分支的解析器。后续所有解析步骤都会重复使用这个已经确定的解析器,不会再重新判断ad.GetCounter()的实时值。
简单说,if ad.GetCounter()=0只在解析器构造阶段执行了一次,而非每次解析元素时都执行。
解决方法
要让判断逻辑在每次解析元素时都执行,需要把条件判断放到解析器的执行流程里,用>>=包裹逻辑,而非在解析器构造阶段做判断。修改后的choicexyz如下:
let choicexyz (ad:MyClass) = // 先触发解析流程,再实时检查计数器值 preturn () >>= fun _ -> if ad.GetCounter()=0 then choice [x;y;z] >>= fun result -> ad.Print("in if block") ad.Increment() preturn result else z >>= fun result -> ad.Print("in else block") ad.Increment() preturn result
这样修改后,每次解析元素时都会先执行preturn () >>= ...,此时才会读取ad.GetCounter()的当前值,从而正确触发else分支。
额外建议
在FParsec中使用可变状态需要谨慎,解析器的复用或回溯可能引发状态不一致问题。针对你原本的需求(判断括号内序列是否为空),完全可以不用可变计数器,直接利用sepBy的返回值判断结果列表是否为空,更符合函数式编程风格:
let bracketedSeq = skipChar '(' >>= fun _ -> sepBy elementParser (skipChar ',') >>= fun elements -> skipChar ')' >>= fun _ -> if List.isEmpty elements then // 处理空序列逻辑 preturn elements else // 处理非空序列逻辑 preturn elements
内容的提问来源于stack exchange,提问作者bookofproofs

