FParsec:回溯`sepBy`

时间:2017-08-12 19:05:26

标签: f# parser-combinators fparsec

考虑以下玩具语法和解析器:

(* in EBNF:
  ap = "a", { "ba" }
  bp = ap, "bc"
*)
let ap = sepBy1 (pstring "a") (pstring "b")
let bp = ap .>> (pstring "bc")
let test = run bp "abababc"

我得到以下输出:

Error in Ln: 1 Col: 7
abababc
      ^
Expecting: 'a'

显然,sepBy1会看到最后一个b,并希望它会导致另一个a,当它找不到时会失败。是否有sepBy1的变体可以回溯b并使此解析成功?我有什么理由不能使用它吗?

3 个答案:

答案 0 :(得分:3)

我一直在查看FParsec source for the sepBy family of functions,看起来没有办法(目前)准确地执行您要求的任何sepBy函数。但是,如果分隔符解析器很简单,您可能能够很好地接近它。

要近似您要查找的内容,您可以尝试使用sepEndBy,或者更有可能使用sepEndBy1。它的用途是使用类似Python语法的列表,其中最后一项允许使用尾随逗号:[ 1, 2, 3, ]。使用sepEndBy1将使用最终分隔符,因为这是您想要的预期用途。但是,在您的用例中,您必须解决sepEndBy1使用最终分隔符的事实。如,

// This parser will be either a separator *or* a prefix of the next item
let sharedParser = pstring "b"

let ap = sepEndBy1 (pstring "a") sharedParser
let restOfBp = pstring "c"
let bp = restOfBp <|> (pipe2 sharedParser restOfBp (fun b c -> b + c))

这里,pipe2中的合并功能是简单的字符串连接,但在实际使用场景中可能会有所不同。关键的想法是, if ,您可以编写一个简单的函数,将b解析器与c解析器组合在一起,生成{{ 1}}结果, 如果 bc解析器不是太复杂,那么这将适合您。如果c非常复杂但c很简单,那么只需颠倒b两边的顺序,以便首先测试<|>b仅在c成功或失败后进行测试;这样你就不必像我刚刚发布的示例代码那样两次应用b解析器。我按照我的方式写了它,因为它更容易理解。

如果c 为您工作,因为您现实生活中的sepEndBy1b解析器太昂贵,无法解析两次,那么我能想到的唯一解决方案就是要求将这个新的c变体作为新功能添加到FParsec中。我通过FParsec代码看出,有可能重写实现以选择性地回溯到最终分隔符之前,但它需要一些重要的测试和边缘情况的考虑因此它不会是一个五分钟的修复。但是,如果sepBy解决方法对您不起作用,请继续submit that feature request

答案 1 :(得分:3)

这是实现sepBy1

这种变体的一种方法
let backtrackingSepBy1 p sep = pipe2 p (many (sep >>? p)) (fun hd tl -> hd::tl)

避免在解析器语法中进行回溯通常会使解析器更快,更便携,更易于调试。因此,如果你有机会通过重组语法来避免回溯(没有太多复杂化),我建议你这样做。

答案 2 :(得分:1)

我不确定返回的值是否重要,但如果没有,那么最简单的方法就是更密切地转录语法:

let ap = pstring "a" >>. many (pstring "ba")
let bp = ap .>> pstring "bc"

请注意,pstring "ba"不会导致您遇到同样的问题,因为pstring不能只消耗其部分参数;要么消耗完整的"ba",要么在没有消费的情况下失败,因此bp可以成功查看b(如果有)。

另一种可能性,如果这确实是完整的语法(即ap未在其他地方的其他制作中使用),则将其更改为以下EBNF,这是等效的:

ap = "ab", { "ab" }
cp = ap, "c"

这使得使用FParsec更容易实现:

let ap = many1 (pstring "ab")
let cp = ap .>> pstring "c"