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
表达式并将 outerP
和 outerSource
(以及 innerP
和 innerSource
)转换为函数参数,希望你能对结果很满意。
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
请注意,我对运算符 |>>=
的实现丢弃了 'UserState
和 Position
(Success
的最后两个参数)。如果您不关心这些,这个解决方案就足够了。否则,您将需要弄清楚如何将 res
中的那些与 f x
.
返回的那些结合起来
如何编写解析器函数,使它们在不同的源流上执行,而后面的函数依赖于前面的结果? 说下面两个:
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
表达式并将 outerP
和 outerSource
(以及 innerP
和 innerSource
)转换为函数参数,希望你能对结果很满意。
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
请注意,我对运算符 |>>=
的实现丢弃了 'UserState
和 Position
(Success
的最后两个参数)。如果您不关心这些,这个解决方案就足够了。否则,您将需要弄清楚如何将 res
中的那些与 f x
.