F# 将作用于不同源流的解析器绑定或组合在一起

F# binding or composing together parsers acting on separate source streams

如何编写解析器函数,使它们在不同的源流上执行,而后面的函数依赖于前面的结果? 说下面两个:

let outerP = many (pchar 'a') |>> (fun l -> l.Length)
let innerP x = pstring "something" |>> (fun str -> (str,x))

使用单一来源,绑定运行良好:

let combinedP = outerP >>= innerP
run combinedP "aasomething"

但是作为一个更复杂的项目的一部分,我需要一起解析几个单独的文件,后面的解析器使用前面的输出。 例如:我有

let outerSource = "aaaaa"
let innerSource = "something"

显而易见的解决方案是将文件连接在一起,但可扩展性不是很好,尤其是因为实际上有一个内部源文件列表等...

背景:我是函数式编程的新手,不确定这是否使函数组合走得太远,但似乎它应该是这里的好解决方案,只是在这种情况下我无法弄清楚。 下面是有效但不起作用的解决方案,它导致实际项目中的多层嵌套代码。

独立源文件的作用:

let combinedScript =
    let outerR = run outerP outerSource
    match outerR with
    | Success (outerParam,_,_) ->
        let innerR = run (innerP outerParam) innerSource
        innerR

在真实的代码中,这是一个 4 层深的厄运金字塔,看看它,基本上就是 bind 所做的,只是有一个额外的变化(不同的来源)

你的最后一句话包含了一个很好的功能性方法的线索:“......看着它,它基本上就是 bind 所做的,只是有一个额外的变化(不同的来源)"

让我们通过实现我们自己的类绑定函数将您的 4 级厄运金字塔变成一个漂亮的表达式。我将采用你的 combinedScript 表达式并将 outerPouterSource(以及 innerPinnerSource)转换为函数参数,希望你能对结果很满意。

let combinedScript (outerP, outerSource) (innerP, innerSource) =
    let outerR = run outerP outerSource
    match outerR with
    | Success (outerParam,_,_) ->
        let innerR = run (innerP outerParam) innerSource
        innerR
    | Failure (msg, err, state) ->
        Failure (msg, err, state)

// And we'll define an operator for it
let (>==>) (outerP, outerSource) (innerP, innerSource) =
    combinedScript (outerP, outerSource) (innerP, innerSource)

// Now you can parse your four files like this
let parseResult =
    (parserA, fileA)
    >==> (parserB, fileB)
    >==> (parserC, fileC)
    >==> (parserD, fileD)

函数式编程真正伟大的地方在于,我什至不用考虑就写了上面的代码,因为将厄运金字塔变成平面列表是一个众所周知的秘诀。正如您所说,这基本上就是 "bind" 所做的。我上面所做的就是遵循编写 "bind" 函数的标准方法。如果您还不知道 "bind" 函数的标准配方,https://fsharpforfunandprofit.com/series/map-and-bind-and-apply-oh-my.html 是我找到的最好的解释。如果您像我一样,在某些东西最终 "click" 进入您的大脑之前,您必须阅读它大约四五遍,但是一旦您有了那个 "Ah-HA!" 时刻,您就会获得更深入地了解函数式编程的强大功能,以及它如何让您真正简单地完成真正高级的事情。

P.S。如果该系列文章对于您目前对 FP 的理解来说过于高级,请尝试 https://fsharpforfunandprofit.com/posts/recipe-part2/ and https://fsharpforfunandprofit.com/rop/。其中之一可能是对这些概念的更好介绍,具体取决于您已经了解多少。

首先,为什么您认为您的解决方案不起作用? "Functional" 并不意味着 "beautiful" 或 "elegant"。函数式代码可能和面向对象的代码一样丑陋和复杂。它是一个厄运金字塔这一事实并没有降低它的功能。

其次,bind 所做的并不是 "almost",bind 正在做的 。你在那里有额外价值的事实并没有改变这一点。事实上,如果绑定函数只能使用它们的直接输入,那么 bind 的用处将相当有限。

为了避免厄运金字塔,您可以很好地利用 F# 语法。例如,这有效:

let x =
    20    |> fun a ->
    a + 1 |> fun b ->
    b * 2

// x == 42

这个例子使用了两个嵌套函数,前一个的结果传递给下一个。可以改写为:

let x =  (fun a -> (fun b -> b * 2) (a + 1)) 20

但我利用运算符 |> 和 F# 越位规则使其看起来像 "step by step" 计算。

如果您为 ParseResult<_,_>:

定义一个类似的组合运算符,您可以做类似的事情
// (|>>=) : ParseResult<'a, 'e> -> ('a -> ParseResult<'b, 'e>) -> ParseResult<'b, 'e>
let (|>>=) res f = match res with 
    | Success (x, _, _) -> f x
    | Failure (x, y, z) -> Failure (x, y, z)

// And then use it:
let combinedScript =
   run outerP outerSource |>>= fun outerR ->
   run (innerP outerR) innerSource |>>= fun innerR ->
   run (nextP innerR) nextSource |>>= fun nextR ->
   ... and so on

请注意,我对运算符 |>>= 的实现丢弃了 'UserStatePositionSuccess 的最后两个参数)。如果您不关心这些,这个解决方案就足够了。否则,您将需要弄清楚如何将 res 中的那些与 f x.

返回的那些结合起来