FParsec 库

728 篇文章 1 订阅
86 篇文章 0 订阅

FParsec 库

 

FParsec 是一个开源的组合库,用于解析文本。可以从 http://www.quanttec.com/fparsec/ 下载,由 StephanTolksdorf 实现,是基于流行的Haskell 解析库 Parsec,http://www.haskell.org/haskellwiki/Parsec。这 是一个组合库的示例,其思想与我们在前一章所讨论的相似,解析是通过组合大量的基本解析器逐步建立起来的,基本解析器是在FParsec 库国定义的,通过也是在FParsec 库中定义的函数和运算符进行组合。

如果你习惯于阅读巴氏范式(BNF)语法和使用正则表达式,那么,就可能会发现使用fslex.exe 和 fsyacc.exe 比使用 FParsec 要直观一点点。尽管如此,FParsec 仍然是相当这容易使用的,当你的大脑中有也几个关键概念之后。它最大的好处是,它只需要这个库,不需要代码生成和另外的工具,这样,解析器的定义趋向于更短小,更少地配置棘手的开发环境;另外,FParsec 的设计已经考虑到性能问题,通常生成的解析器非常快,且有很好的错误提示。

要演示 FParsec 的使用,我们还是看一下已经用fslex 和 fsyacc 实现的算术语言。但现在,我们先看一个简单的示例。

fparsec 提供了大量预先定义好的解析器,因此,最简单的用法就是使用这些解析器。下面的示例展示了使用 pfloat 解析器,它解析浮点数,返回字符串。

 

open FParsec

let pi = CharParsers.run CharParsers.pfloat"3.1416"

printfn "Result: %A" pi

 

示例的运行结果如下:

 

Result: Success: 3.1416

 

[

FParsec 的版本现在是 1.0.1。

安装 FParsec,可以通过程序包管理器控制台,执行命令:

install-package FParsec

需要引用 FPaesecSC.dll 和 FParsec.dll

如果觉得 pi 解析的是字符串到字符串,可以这样改写一下:

let pi = CharParsers.run CharParsers.pfloat"31416e-4"

输出结果不变。

]

 

尽管在这个示例中只有三行代码,但仍有一些重要的东西需要注意。我们已经提到过解析器,pfloat,它是许多预定义的解析器中的一个,可以作为积木来使用。表 13-2 汇总了一些重要的解析器;函数 run 运行解析字符串,还有其他许多函数可以用来运行解析器,针对其他的输入类型,表 13-3 列出了所有与输入类型一致的应该运行的函数;最后,注意输出的结果是“Success: 3.1416”,这是因为返回的不仅是一个浮点数,返回的结果实际上是差别联合,它既包含了表示解析的成功与失败,也包含了解析器的位置与解析器的结果,在这里是浮点数。在下一个示例中会看到更详细的结果。

表 13-2 有用的预定义解析器

函数

描述

upper

匹配一个大写字母

lower

匹配一个小写字母

digit

匹配一个 0 到 9 之间的数字

hex

匹配一个 0 到 F 之间的十六进制数字,包括大写和小写字母

spaces

匹配 0 或多个空白,包括空格、tab 和换行

spaces1

匹配一个或多个空白,包括空格、tab 和换行

pfloat

匹配浮点数

pint32

匹配 32 位整数

 

值得注意的是,表 13-2 中的字母是指字母的 UTF-16表示,包括许多生意和非拉丁字符。

输入un  点 e 有也几个关键概念之后,pressionParsere, 0,lb.Lexeme.Length)

表 13-3 在不同的文本上运行的解析器函数

 

函数

描述

run

最简单的在字符串上运行解析器的方法

runParser

运行包含在 FParsec.CharStream 类中的文本上的解析器,这个类由 FParsec 在内部使用,表示被解析的文本

runParserOnString

运行字符串解析器

runParserOnSubstring

运行部分字符串解析器,由开始和结束的索引指定

runParserOnStream

运行 System.IO.Stream 解析器

runParserOnSubstream

运行部分 FParsec.CharStream 解析器,由开始和结束的索引指定

runParserOnFile

运行文件解析器

 

解析时,最重要的是知道这个操作是否成功,FParsec 是通过差别联合实现的。这个类型在解析成功时,可以得到访问结果,如果不成功,会得到错误消息。在两种情况下,都还会返回有关解析器位置的信息。下面的示例显示如何使用 ParserResult 类型。

 

open FParsec

 

let parseAndPrint input =

  letresult = CharParsers.run CharParsers.pfloat input

 match result with

  |CharParsers.Success (result, _, _) ->

   printfn "result: %A" result

  |CharParsers.Failure (_, errorDetails, _) ->

   printfn "Error details: %A" errorDetails

 

parseAndPrint "3.1416"

parseAndPrint "    3.1416"

parseAndPrint "Not a number"

 

这个示例的运行结果如下;

 

result: 3.1416, pos: (Ln: 1, Col: 7)

Message: Error in Ln: 1 Col: 1

 3.1416

^

Expecting: floating-point number

Error details: Error in Ln: 1 Col: 1

Expecting: floating-point number

Message: Error in Ln: 1 Col: 1

Not a number

^

Expecting: floating-point number

Error details: Error in Ln: 1 Col: 1

Expecting: floating-point number

 

从示例的输出可以看出,第一次调用函数parseAndPrint,解析器成功地成功地解析了输入,但是,另外两次都失败了。当然,我们可能并不会希望浮点数的解析器能够解析出"Not a number" 这样的输入,而更可能希望它能解析出"    3.1416",这样简单的带有前缀为空白的数字。解析器 pfloat 不能处理空白,但是,我们可创建一个解析器,把处理空白的解析器与 pfloat 组合起来。

 

open FParsec

open FParsec.Primitives

 

let wsfloat = CharParsers.spaces >>.CharParsers.pfloat

let pi = CharParsers.run wsfloat "3.1416"

printfn "Result: %A" pi

 

示例的运行结果如下:

 

Result: Success: 3.1416

 

在这个示例中,我们使用 >>. 运算符,这是 FParsec 自定义的运算符,把两个解析器组合在一起。这个运算符表示组合两个解析器,并只返回第二个解析器的结果。在这个上下文中,组合查找匹配第一个解析器的文本,接着,再查找匹配第二个解析器的文本。还有一个相似的运算符 .>>,它也组合两个解析器,只保持第一个解析器的输入。这些运算符中的点可以认为是解析器将要返回的结果。例如,如果想忽略后面的空格,而不是前面的空格,就应该使用 .>> 运算符。

因为能够组合解析器,并忽略其结果,在某些上下文中是有用的,能够组合解析器的结果以及解析器本身,是非常重要的。运算符 >>= 就能这样做,如同 >>. 和 .>> 运算符一样,在 >>= 运算符的右边接收一个解析器,而右边接收一个函数,前面解析器的结果传递给它,结果返回的还是解析器。下面的示例演示了这个运算符的常规用法。

 

open FParsec

open FParsec.Primitives

 

let simpleAdd = CharParsers.pfloat>>= fun x ->

            CharParsers.spaces >>= fun () ->

            CharParsers.pfloat >>= fun y ->

            preturn (x + y)

 

let pi2 = CharParsers.run simpleAdd"3.1416 3.1416"

printfn "Result: %A" pi2

 

示例的运行结果如下:

 

Result: Success: 6.2832

 

这里,我们组合两个 pfloat 解析器,用一个空白解析器分隔开,每个解析器的结果经过 lambda 函数,作为参数传递给下一个解析器。在这里,我们把它们称作 x 和 y,希望能把 x 和 y 加起来,或者执行某些其他运算,然后,返回结果,但是,这个 lambda 函数的结果必须是解析器类型。在这里,x 和加y lkfhgo,ak ftj rvtfadwb的义Parsec      访问结果,如果失败float "3.y 都是浮点类型,为了避免类型方面的问题,可以使用 preturn 函数,在前面的示例中我们使用的,这个函数可以取任意值作为参数,然后,创建一个返回这个值的解析器,因此,可以把两个浮点数加起来,并使用 preturn 创建一个问题返回这个值的解析器。

除了能够组合两个解析器的结果之外,通常还可以对解析的结果应用某些转换。在 FParsec 中,使用 |>> 运算符实现,其行为与 F# 中的 |> 会相似。下面的示例演示了这个用法。

 

open FParsec

open FParsec.Primitives

 

let addTwo = CharParsers.pfloat |>>(fun x -> x + 2.0)

let pi2 = CharParsers.run addTwo "3.1416"

printfn "Result: %A" pi2

 

示例的运行结果如下:

 

Result: Success: 5.1416

 

这里,我们使用 |>> 简单地把解析器的结果加上 2,这并不是特别有实际意义的示例,但是,这个运算符有许多用途,我认为最有用处的是把解析器的结果转换成差别联合的一种情况。

到现在为止,我们已经讨论了依次组合解析器,以及组合或转换其结果,另一个常见的任务是利用组合解析器,在两个类型之间做出选择。在下面的示例中,我们创建一个解析器,它既能处理浮点数,也能处理字符串(还能这样说,chains of letters)。我们用 <|> 运算符实现,这个运算符组合了两个解析器,创建了一个新的解析器,它接收一个输入,可能匹配第一个解析器,也可能匹配第二个。它的工作方式有点奇特,返回第一个解析器的结果,作为 consume 输入,如果第一个解析器成功或者失败,且 consume 了输入,那么,就返回第一个解析器的结果;如果没有 consume 输入,那么,就执行第二个解析器,并返回它的结果。,解析器的次序就非常重要了。

 

open FParsec

open FParsec.Primitives

 

type AstFragment =

  |Val of float

  |Ident of string

 

let number = CharParsers.pfloat |>>(fun x -> Val x)

let id =

 CharParsers.many1Satisfy CharParsers.isLetter

 |>> (fun x -> Ident x)

 

let stringOrFloat = id <|> number

 

let num = CharParsers.run stringOrFloat"3.1416"

let ident = CharParsers.run stringOrFloat"anIdent"

 

printfn "Result 'num': %A Result'ident': %A" num ident

 

示例的运行结果如下:

 

Result 'num': Success: Val 3.1416 Result'ident': Success: Ident "anIdent"

 

这个示例的另一个有趣地方是使用了函数many1Satisfy,这个函数能够用判定函数(predicate function)创建解析器,它接收这个函数作为参数。FParsec 库有大量类似的函数,表 13-4 作了汇总了其中的一部分。输入的文本提供给判定函数,如果有一个或多个字符[ charters 是笔误吗?是不是应该为character] 返回真,那么,由many1Satisfy 创建的解析器就成功。判定函数取一个字符作为参数,返回必须为布尔值,表示这个字符是否是这个解析器所接收的。在这里,使用了预定义的判定函数isLetter,只要有任意的字符是字母的,就返回真。这种创建解析器的方法非常强大而方便。我们的示例表明,CharParsers.many1SatisfyL CharParsers.isLetter"identifier" 可以匹配连续的字母串,很容易想到,CharParsers.many1SatisfyL CharParsers.isLower "lower caseidentifier"函数ny1Satisfy.匹配第一既处理浮点数,。器, 将匹配连续的小写字母串,这里提供的字符串是标签,用于生成错误消息。

 

表 13-4 能够创建解析器的判定函数

 

函数

描述

manySatisfy

当有 0 或多个字符使判定函数为真时,创建的解析器成功;输入作为字符串被返回。

skipManySatisfy

当有 0 或多个字符使判定函数为真时,创建的解析器成功;输入被忽略。

many1Satisfy

当有 1 或多个字符使判定函数为真时,创建的解析器成功;输入作为字符串被返回。

skipMany1Satisfy

当有 1 或多个字符使判定函数为真时,创建的解析器成功;输入被忽略。

manyMinMaxSatisfy

可以为判定函数指定字符的最小和最大个数,必须匹配的解析器,才会成功;输入作为字符串被返回。

 

注意,所有这些函数都有一个带大写字母 L 后缀的版本,用于指定标签,能够为用户提供生成出错消息的标签。

现在,为了实现我们的小算术语言所需要的大部分元素都已经有了,另外,抽象语法树的定义保持与以前相同。

 

module Strangelights.ExpressionParser.Ast

 

type Expr =

  |Ident of string

  |Val of System.Double

  |Multi of Expr * Expr

  |Div of Expr * Expr

  |Plus of Expr * Expr

  |Minus of Expr * Expr

 

下面是真正的算术语言定义:

 

open Strangelights.ExpressionParser.Ast

open FParsec

open FParsec.Primitives

open FParsec.OperatorPrecedenceParser

 

// skips any whitespace

let ws = CharParsers.spaces

 

// skips a character possibly postfixedwith whitespace

let ch c = CharParsers.skipChar c >>.ws

 

// parses a floating point number ignoringany postfixed whitespace

let number = CharParsers.pfloat .>>ws |>> (fun x -> Val x)

 

// parses an identifier made up of letters

let id =

  CharParsers.many1SatisfyCharParsers.isLetter

  |>>(fun x -> Ident x)

  .>>ws

 

// create an new operator precedence parser

let opp = newOperatorPrecedenceParser<_,_>()

 

// name the expression parser withinoperator precendence parser

// so it can be used more easily later on

let expr = opp.ExpressionParser

 

// create a parser to parse everythingbetween the operators

let terms =

  Primitives.choice

    [id; number; ch '(' >>. expr .>> ch ')']

opp.TermParser <- terms

 

// add the operators themselves

opp.AddOperator(InfixOp("+", ws,1, Assoc.Left, fun x y -> Plus(x, y)))

opp.AddOperator(InfixOp("-", ws,1, Assoc.Left, fun x y -> Minus(x, y)))

opp.AddOperator(InfixOp("*", ws,2, Assoc.Left, fun x y -> Multi(x, y)))

opp.AddOperator(InfixOp("/", ws,2, Assoc.Left, fun x y -> Div(x, y)))

 

// the complete expression that can beprefixed with whitespace

// and post fixed with an enf of filecharacter

let completeExpression = ws >>. expr.>> CharParsers.eof

 

// define a function for parsing a string

let parse s = CharParsers.runcompleteExpression s

// run some tests and print the results

printfn "%A" (parse "1.0 +2.0 + toto")

printfn "%A" (parse "toto + 1.0* 2.0")

 

// will give an error

printfn "%A" (parse "1.0+")

 

示例的运行结果如下:

 

Success: Plus (Plus (Val 1.0,Val 2.0),Ident"toto")

Success: Plus (Ident "toto",Multi(Val 1.0,Val 2.0))

Success: Plus (Ident "toto",Plus(Val 1.0,Val 2.0))

Failure:

Error in Ln: 1 Col: 6

1.0 +

  ^(endof input)

Expecting: '(' or floating-point number

 

注意,第二个解析树不同于第一个简单的解析树,因为乘法比加法有更高的优先级;还要注意一下,最后输入的是不正确格式,库是如何生成易懂的出错消息的。

通过这个示例介绍的主要的新功能是使用OperatorPrecedenceParser 类,它能很方便地解析表达式,涉及(中缀、前缀、后缀和三元,(infix/prefix/postfix/ternary)运算符,基于运算符的优先级和结合性,使用成员函数 AddOperator 添加感兴起的运算符,连同表示的结合性与优先级的标志一起。OperatorPrecedenceParser 类有一个属性 ExpressionParser,它提供的解析器了运算符的解析,并且它的使用方法与我们在这一节中所遇到的其他解析器一样。

 

 

第十三章小结

 

在这一章,我们学习了解析文本的两种强大的机制,任务乍一看有点琐碎,但能提供有趣的挑战。工具 fslex.exe 和fsyacc.exe 的结合,为那些熟悉正则表达式和巴氏范式语法的人提供了写解析器的有用方法;然而,由于要使用外部工具来生成代码和定制语法,使得在某些情况事情可能变得复杂,这就是组合解析器库 FParsec,我们已经讨论了,能真正取胜的原因之所在,用户创建解析器,除了使用 F# 的内置语法以外,不需要任何其他工具。FParsec 另一个很好的功能是出错消息的质量,要比由fslex.exe 和fsyacc.exe定制,由于要使用外部工具,dtter "identifier"  产生的好很多。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值