相信每个Scala的爱好者都有读过《Programming in Scala》这本书,书中有一个章节「Combinator Parsing」介绍了一个很有趣的Scala库 – Parser Combinator(https://github.com/scala/scala-parser-combinators)。单从「Parser Combinator」字面并不太好理解它是什么,可以用来做什么。这篇文章将带着大家一起通过实例来了解Parser Combinator。
通常我们会有这样一种需求,将输入语言转换成计算机程序能够识别和处理的数据结构,也就是实现一个词法分析器。通过Parser Combinator,我们可以比较方便且快速地实现一个词法分析器。首先,需要把Parser Combinator加入到我们的项目当中。以SBT项目为例,只需要在build.sbt里加入如下一行便可以将这个库引入到项目中:
词法分析的主要目的是把一个长字符串转换为单词的序列,而每个单词就是被解析出来的可以由计算机去理解的处理单元,一般被称为token。假设现在我们想对“1+2*3-4/5”这个的算术表达式进行解析。直观上,我们可以轻松看出每个数是一个token,由加、减、乘、除的运算符连接起来,还知道要先计算乘除运算,再计算加减运算。但是计算机应该如何理解它呢?下面开始实现我们的词法分析器。
定义一下可能解析出来的token:加、减、乘、除、和十进制数。
然后进行解析。把这些token从输入字符串中识别出来,需要使用到Parser(解析器)。Parser Combinator提供了很多预定义的Parser,比如RegexParsers提供了很多使用正则表达式来识别和解析的Parser,而JavaTokenParsers继承自RegexParsers,并且提供了一些Java语法的Parser,等等。我们先定义解析十进制数的Parser "factor":
为了使用JavaTokenParsers提供的Parser,我们定义一个ArithParsers并继承了JavaTokenParsers。decimalNumber是JavaTokenParsers提供的一个Parser,定义如下:
decimalNumber使用正则表达式识别十进制数,如果解析成功,则返回一个基于String的Parser。在factor的定义当中,decimalNumbert调用了"^^"函数,而“{ v => DecimalVal(v.toDouble) }”是这个函数的参数。可以理解factor是这么一个Parser,在decimalNumber匹配成功的情况下,将返回的Parser[String]转换为Parser[Token]。
接下来进行运算符的解析。众所周知,乘除的优先级高于加减,因此在十进制数被解析出来以后,我们优先定义解析乘除运算的Parser “term":
这里调用了chainl1这个预定义的函数(定义如下):Parser p是用来解析元素,Parser q是用来解析分割元素的token,然后产生一个左结合的函数将两个元素合并成一个。
所以,factor就是这里的p,而q是运算符解析,定义如下:
可以理解为,如果“*”匹配成功,就将它分割两边的token合成一个“Mul" token;如果“/”匹配成功,就将它分割两边的token合成一个“Div” token。“|”也是一个函数调用,表示“或”,也就是匹配“*”或者“/”。
我们可以用同样的方法来定义解析加减运算符的Parser “expr”:
此外,算术表达式可以通过括号来改变运算的优先顺序。我们只需要将Parser “factor”改进一下,做一个“递归”:
factor中新加的部分"(" ~> expr <~ ")"用来匹配左右括号,“expr”是上面定义的加减运算符Parser。也就是说括号里可以是另一个复杂的算术表达式。
到这里,我们词法分析器就定义好了,汇总一下:
写一个测试程序:
输出结果为:
这样几行代码就实现了一个针对算术表达式的词法分析器。我们还能进行很多的扩展,大家有兴趣可以尝试一下:
- 对比较运算符AND,OR,NOT进行解析
- 对函数调用进行解析,比如1 + SUM(1, 2) + 3
- 对对象进行识别,比如[column 1],[column 2]
另外,对于词法分析的结果,我们还可以继续用Parser Combinator进行语法分析,只是分析的不是最初的输入字符串,而是词法分析产生的token。
最后,我们理解一下文章开头留下的问题,Parser Combinator是什么,用来做什么?它是利用原始简单的Parser配合组合操作运算符,从而构建出复杂的Parser来满足解析的需求。