parser可以认为是将一段文本转换为ast的工具。
作为前端开发人员,我们的生态圈早已充斥着各种parser工具,通过本篇文章,希望你能学习到parser combinator的实现原理,更加了解函数式编程。在有必要的时候,轻松地通过parser combinator实现自己的parser。本篇文章在实现parser combinator库后,在末尾会通过使用这个库来实现一个json parser。
在开始之前,我们先说一下parser combinator相比parser generator的优势:
- 不用学习一门新的语言,如知名的parser generator: ANTLR,pegjs,nearley等,开发者首先要会BNF,然后还要学习各自的语法。而使用parser combinator,开发者可以使用他们最熟悉的主语言
- 因为parser generator是通过一门语言生成代码(即生成parser),所以不易debugger和类型检查。
- 更强大的表达能力,parser combinator拥有宿主语言的所有表达能力。
我们先写一个简单匹配的parser,直观感受下。需要说明的是,编译原理中的语法分析通常分为LL算法和LR算法,解析组合子本质上使用的就是LL算法,即通过向前看字符,自顶而下的分析,生成语法树。
function text(expected) {
return function textParser(input){
if (input.startsWith(expected)) {
return expected;
} else {
return console.error(`expect ${expected}`);
}
}
}
text这个函数可以构造出匹配字符串的函数,如果匹配成功就返回匹配到的字符,如果失败就打印出失败信息。
这是一个非常简单的parser构造函数,如果我们希望能匹配html,css,javascript中的任意字符应该怎么处理呢?
function oneOf(...ps){
return function (input, state){
let pResult = null
for(let p of ps){
pResult = p(input)
if(pResult) return pResult
continue
}
}
}
const matchLang = oneOf(text("html"), text("css"), text("javascript"))
const matched = matchLang("css") // css
这里我们通过oneOf函数和text函数即可构造出更强大的匹配函数。
我们整个解析组合子库的工作方式就是如上所示,通过一系列构造parser的函数组合,产生更强大的parser。
上面的只是开胃菜,希望大家有一个简单的了解,下面开始我们的构造之旅。
下面的代码为了更加健壮和更好的可读性,都是使用typescript书写,但类型不是很复杂,对typescript不熟悉的同学不用担心
如果你将上面的示例复制粘贴,并运行,你会发现打印出“expect html”这个字符串,这是因为我们的textParser中,如果匹配失败,就很简单地打印出了错误的信息。所以首先我们来定义parser执行成功和失败的函数:
const SUCCESS = Symbol('success')
const MISMATCH = Symbol('mismatch')
function success(value: A, state: ParserState): ParserResult{
return {
type: SUCCESS,
value,
state: state
}
}
function mismatch(state: ParserState): ParserResult{
return {
type: MISMATCH,
state: state,
}
}
上面的state保存了我们parser执行的上下文信息,在我们text函数构造出的函数中,如果匹配成功,我们只是简单的返回,但在实际的场景中,我们的parser执行函数会继续地向前看,所以我们需要保存一些上下文信息,对应的类型如下:
type UserState = object interface ParserState{ position: number; expectedTokens: string[]; // 错误提示时可以用 userState: UserState; } type ParserResult
= | { type: typeof SUCCESS, state: ParserState, value: A } | { type: typeof MISMATCH, state: ParserState }
向前看我们需要移动向前看第一个字符的指针,对应的函数如下:
function advance(state: ParserState, length: number): ParserState{
return length === 0
? state
: {
...state,
position: state.position + length,
expectedTokens: []
}
}
同时,当匹配错误的时候,我们希望能展示期待匹配的字符:
function expect(state: ParserState, expected: string | string[]){
return {
...state,
expectedTokens: state.expectedTokens.concat(expected)
}
}
现在来改造我们的text函数:
function text(expected) {
return function textParser(input, state){
if (input.startsWith(expected)) {
return success(expected, advance(state, expected.length));
} else {
return mismatch(expect(state, expected))
}
}
}
为了使我们的api使用上更加地友好方便,我们再次改造text函数,让text函数返回一个parser对象:
function text(expected: string): Parser {
return new Parser(function textParser(input, state) {
if (input.startsWith(expected, state.position)) {
return success(expected, advance(state, expected.length));
} else {
return mismatch(expect(state, expected, false));
}
});
}
Parser是一个类,它的构造函数接受一个函数,在我们的text函数中就是实际执行匹配的函数,然后Parser有一个parse函数,这是开始parse时执行的函数。
interface ParserFun
{ (input: string, state: ParserState): ParserResult; } const initialState: ParserState = { position: 0, expectedTokens: [], userState: {}, }; class Parser { public _parse: ParserFun; constructor(parse: ParserFun) { this._parse = parse; } parse(input: string, userState = {}) { return this._parse(input, { ...initialState, userState }); } }
现在我们只有text函数能构造出基本的匹配函数,且只能匹配字符,为了扩充我们的能力,我们写一个匹配正则的函数:
function regex(re: RegExp, expected?: string | string[]): Parser{
const anchoredRegex = new RegExp(`^${re.source}`)
return new Parser(function(input: string, state: ParserState){
const m = anchoredRegex.exec(input.slice(state.position))
if(m == null){
return mismatch(expect(state, re.source))
}
const matchedText = m[0]
return success(matchedText, advance(state, matchedText.length))
})
}
在实际匹配的过程中,我们往往需要将匹配到的字符串改为另外一个结构,我们添加map和mapTo函数:
export class Parser
{ public _parse: ParserFn; constructor(parse: ParserFn){ this._parse = parse } parse(input: string, userState = {}){ return this._parse(input, { ...initialState, userState }) } map(fn: (x: A) => B): Parser{ return new Parser((input: string, state: ParserState) => { const pResult = this._parse(input, state) if(pResult.type !== SUCCESS) return pResult return { ...pResult, value: fn(pResult.value) } }) } mapTo(b:B): Parser{ return this.map(_ => b) } }
如果我们想执行多个匹配函数,并处理他们的结果呢?
export function apply(f: (...args: TS) => R, ...ps: ParserMap){
return new Parser(function(input: string, state: ParserState) {
let results: TS = [] as any
for(let p of ps){
let pResult = p._parse(input, state)
if(pResult.type !== SUCCESS){
return pResult
}
results.push(pResult.value)
state = pResult.state
}
return success(f(...results), state)
})
}
看上面的代码可能有些抽象,我们来举一个实际的apply例子,假如我们想匹配一个字符串,但想忽略它后面的空白字符:
function first(a: Parser
, b: Parser): Parser{ return apply((firstArg, secondArg) => firstArg, a, b) } const token = word => first(text(word), regex(/\s*/))
让我们把上面的模式做的更通用一些:
type MaybeParser = string | RegExp | Parser
function first(a: Parser , b: Parser): Parser{ return apply((firstArg, secondArg) => firstArg, a, b) } function second(a: Parser, b: Parser): Parser{ return apply((firstArg, secondArg) => secondArg, a, b) } function liftP(a: MaybeParser): Parser
{ if(typeof a === "string") return text(a) if(a instanceof RegExp) return regex(a) return a } function lexeme(junk: MaybeParser) { const junkP = liftP(junk) return (p: MaybeParser) => first(liftP(p), junkP) } const token = lexeme(/\s*/)
假如我们想匹配布尔值,我们就可以这样用:
const jTrue = token("true").mapTo(true)
const jFalse = token("false").mapTo(false)
const jBoolean = oneOf(jTrue, jFalse)
我们把本篇开头提到的oneOf函数改造:
function oneOf
(...ps: Parser[]): Parser{ return new Parser((input: string, state: ParserState) => { let pResult: ParserResult; let expected = state.expectedTokens for(let p of ps){ pResult = p._parse(input, state) if(pResult.type !== SUCCESS) continue return pResult } return mismatch(expect(state, expected)) }) }
在正则表达式中,*表示匹配0次或多次,在词法分析和语法分析中,这种模式也是很常见的,让我们来实现这种模式。构造这种parser的函数,我们命名为many,这是我们本篇文章中相对比较难的一个函数。
要实现many,我们先分析下many有哪些情况:
- 一个都没匹配
- 匹配一个自己
- 匹配多个自己
通过前面first函数的构造经验,我们很容易想到使用apply函数,进行多个parser的串联匹配,并在其中做递归,但一个关键的点是,某一步匹配失败后,应该返回空数组,我们有下面的代码:
const EMPTYARRAY: any[] = [] class Parser
{ public _parse: ParserFn; constructor(parse: ParserFn){ this._parse = parse } parse(input: string, userState = {}){ return this._parse(input, { ...initialState, userState }) } orElse(p: Parser): Parser{ return oneOf(this, p) } } function pure(value: A): Parser{ return new Parser((input, state) => { return success(value, state) }) } function lazy(getP: () => Parser){ let p: null | Parser = null return new Parser((input, state) => { if(p == null) p = getP() return p._parse(input, state) }) } function many(p: Parser): Parser{ const manyP: Parser = apply((p, list) => [p, ...list], p, lazy(() => manyP).orElse(pure(EMPTYARRAY))) return manyP.orElse(pure(EMPTYARRAY)) }
这里的lazy函数,可以帮助你使用还未声明的变量,当然其实many函数的构造方法有多种,你也可以这样:
const EMPTYARRAY: any[] = [] class Parser
{ public _parse: ParserFn; constructor(parse: ParserFn){ this._parse = parse } parse(input: string, userState = {}){ return this._parse(input, { ...initialState, userState }) } orElse(p: Parser): Parser{ return oneOf(this, p) } chain(fn: (x: A) => Parser): Parser{ return new Parser((input: string, state: ParserState) => { const pResult = this._parse(input, state) if(pResult.type !== SUCCESS) return pResult const p2 = fn(pResult.value) return p2._parse(input, pResult.state) }) } } function pure(value: A): Parser{ return new Parser((input, state) => { return success(value, state) }) } function many(p: Parser): Parser{ const manyP: Parser = p.chain(x => oneOf(manyP, pure([])).map(xs => { return [x].concat(xs) })).orElse(pure(EMPTYARRAY)) return manyP }
这里的代码主要是为了展示chain这个函数,chain这个函数很有用,它接受一个函数,函数接收当前parser执行之后的结果,产生第二个parser,然后执行第二个parser的parse,通过chain,我们可以动态地决定下一步的匹配方式。
其实通过上面的学习,可以发现我们的组合子库的框架已经搭建完成,如果想扩充它的能力,我们只需要通过扩充基础的函数,或者将函数间进行组合,就能产生更强大的parser。
现在让我们以json parser为例,进行一次实战吧。
首先我们来实现json的基本类型,根据标准https://www.json.org/json-en....,有如下代码:
function unquote(s: string) {
return s.substring(1, s.length - 1);
}
// 此处为了方便阅读学习,并没有完全按照标准去写匹配正则
const jNumber = token(/-?\d+(\.\d+)?/).map(x => +x)
const jString = token(/"(?:[^"|\"|\b|\f|\r|\n|\t])*"/).map(unquote)
const jTrue = token("true").mapTo(true)
const jFalse = token("false").mapTo(false)
const jBoolean = oneOf(jTrue, jFalse)
const jNull = token("null").mapTo(null)
然后我们来定义一个普通的json数据结构:
// 与上面的many函数一样,为了使用还未声明的变量,我们使用lazy函数
const jValue: Parser = lazy(() => oneOf(jNumber, jString, jBoolean, jNull, jObject, jArray))
最后让我们实现object类型和array类型:
class Parser {
public _parse: ParserFn;
constructor(parse: ParserFn){
this._parse = parse
}
parse(input: string, userState = {}){
return this._parse(input, { ...initialState, userState })
}
sepBy(parser: Parser): Parser{
const suffixes = many(second(parser, this))
return oneOf(apply((x, xs) => [x, ...xs], this, suffixes), pure(EMPTYARRAY))
}
skip(junk: Parser): Parser{
return first(this, junk)
}
... // 其他函数
}
const pair = apply((key, value) => [key, value], first(jString, token(":")) , jValue)
const comma = token(",")
const pairs = pair.sepBy(comma).map(pairs2Object)
const jObject = apply((leftBracket, obj, rightBracket) => obj, token('{'), pairs, token('}'))
const jArray = token("[").chain(() => jValue.sepBy(comma)).skip(token("]"))
json parser已经组合成功了,我们怎么使用它们呢?我们定义一个函数接收parser,执行这个parser的parse,如果成功就返回结果,如果失败就打印出错误信息:
export function parse
(parser: Parser, input: string){ const res = parser.skip(eof).parse(input) if(res.type === SUCCESS) return res.value if(res.type === MISMATCH) { return console.error(`position ${res.state.position}, expect ${res.state.expectedTokens}`) } }
其中skip(eof)是为了保证我们的parser匹配到了最后一个字符,如果匹配到了最后一个字符,还没匹配完成,就返回mismatch
class Parser
{ skip(junk: Parser): Parser{ return first(this, junk) } // ...其他函数 } const eof = new Parser(function eofParser(input: string, state: ParserState){ if(input.length > state.position){ return mismatch(expect(state, "EOF")) } return success(null, state) })
从上面的代码可以看出,当我们有了一个解析组合子库后,实现一个json parser是很容易的。而且json parser中使用的很多函数,在匹配其他语言的时候,我们可以直接复用。
为了便于学习和消化,我们总结下上面出现的函数类型,上面大部分函数都是parser的构造器,对于这种函数,我们称为Constructor(与我们的构造函数不同)。而对于将一个Constrctor转换为另外一种Constructor的函数,我们称为Combinator。
我们最基本的匹配Constructor就是text函数和regex函数,在这两个函数的基础上通过各种combinator(oneOf,map,apply)等,使我们的parser更加的强大,同时这些单个函数也易于测试。
现在我们的解析组合子库,已经可以支持json parser,但它还有一些问题:
1、它的性能不好,如果读过我的另一篇文章回溯法和记忆法,你会发现我们现在的解析组合子库使用的就是回溯法,很容易做一些重复的计算。
2、我们的错误信息,显然是不够友好的。在生产环境中,当我们匹配失败时,我们应显示期待的字符串,实际的字符串,匹配到的位置。
而这两点就作为进一步实践,留给读者去完成了。
参考: