上一篇文章里,我们已经完成了 AST 的创建。接下来我们开始具体的来定义 Parser,并且会给出一个通用的单字符 Parser 的实现。如果有疑问,可以对照着项目代码的实现一起阅读。
定义 Parser 的正确姿势
根据本例中的需求,所谓 Parser,就是完成接受字符串,并解析成 AST 结构的过程。用一个函数来定义它,就是:
Parser : String -> AST
因为解析过程是逐步的解析,并不能一瞬间就完成所有的解析,为了表示逐步的思想,我们得出一个改进版本的 Parser:
Parser : String -> (AST, String)
输入的是一个 String,输出的是一个 tuple,前面是解析出的 AST。后面的 String 代表解析完前面的 AST 后还剩下的 String。
继续细化,既然是逐步解析的,那即便是一个最普通的 AST,比如之前的 Exp.Constant(1234),也是得先解析出一个个的单个数字,再将其组合成一个整数,然后再形成 AST。为了表示更加细化的,比如解析单个数字这样的形式。我们继续改进:
Parser : String -> (a,String)
和上一个版本相比,我们把 AST,换成了a
, a
是泛型变量,代表任意类型。也就是说 Parser 解析的结果可以是character
, string
, Int
, AST
等等。
现在看来,我们已经有了看起来不错的形式。现在问题来了,我们如何来表示解析失败呢?返回某种特殊的 (ERROR, String) ? 还是返回(a,"") ? 为了表示解析失败,我们把结构修改如下:
Parser : String -> [(a,String)]
和上一个版本相比,我们把返回一个 tuple 改为了返回一个 tuple 的 list。这样就可以用空 list []
来表示解析失败。 如果解析成功,就返回一个包含单个 tuple 的 list。 至于为何不用上述两种方法,读者可以带着这个问题看下来的文章。
在 Swift 中定义 Parser
根据我们 Parser 定义的形式,不能想象所谓 Parser 的类型就是一个函数。为了方便书写,我们把它放在结构体中。
函数式编程一个重要的思想方法就是从类型开始思考代码的意义。
struct Parser<a>{
let p : String -> [(a,String)]
}
来审视 Parser 结构:
- 首先需要一个泛型参数
a
,代表这个 Parser 解析的结果的类型。 - 有一个成员变量,
p
其类型是一个函数,该函数接受一个 String 为输入,返回一个[(a,String)]
实现第一个 Parser
我们从最简单的例子,比如要定义一个解析字符"H"的 Parser。什么意思呢? 就是这个 parser 会尝试去字符串里解析一个字符 H 出来(从字符串的开头开始),如果解析成功,则返回[("H",剩下的字符串)], 如果解析失败则返回[].
换句话来说,这个 Parser 会检测字符串首字母是否是 H,如果是则成功,否则就失败。
let par1 = Parser<Character> { x in
guard let head = x.characters.first where head == "H" else{
return []
}
return [("H",String(x.characters.dropFirst()))]
}
要创建一个 Parser,首先就是要用结构体的构造器并指定泛型的类型。 比如Parser<Character>(...)
。 因为结构体只有一个成员且这个成员是一个函数类型,所以...
的部分要传入的就是一个闭包。也就是Parser<Character>({....})
,又因为 Swift 的尾闭包特性,闭包可以从()
移动到外面,所以构造 Parser 的最终形式就是Parser<Character>{x in ...}
, 上面的代码便是这样的结构,最终把创建好的Parser 赋值给了变量 par1
。
现在我们来看,创建这个 Parser 的闭包长什么样。我们都知道这个闭包是String->[(a,String)]
的类型,我们把输入的参数命名为x
,代表要解析的 String,我们通过一个 guard 来看他的首字母是否是 H,如果不是则解析失败,返回[]
,否则就构造[(a,String)]
返回。这里的 a,就是字符"H", String 则是去除首字母剩下的字符串(因为解析成功的话,输入字符串对应的部分要被消费掉)。
测试一下。
print(par1.p("HELLO WORLD"))
print(par1.p("NIHAO"))
输出:
[("H", "ELLO WORLD")]
[]
我们分别用 par1去尝试解析了两个字符串,第一个首字母是 H 所以解析成功,返回了 H 和剩下的”ELLO WORLD“组成的tuple,另一个解析失败了。
Parser 生成器
仔细想一下,我们刚才的 par1,其实并没什么卵用。现在尝试修改一下形式,来让它稍微有点用:
func parserChar(c : Character) -> Parser<Character>{
return Parser { x in
guard let head = x.characters.first where head == c else{
return []
}
return [(c,String(x.characters.dropFirst()))]
}
}
我们定义了一个函数,parserChar
,依然是从他的类型来分析他的工作原理。它接受一个单个字符,然后返回一个 Parser,相当于parserChar
是一个“解析单个字符 Parser ” 的构造器,比如我们调用 parserChar("E")
就能生成一个解析字符E
的 Parser。这看起来似乎就是我们刚才 hardcode 版本实现的 parser 通用多了。
按照惯例,我们还是来测试一下:
let result = parserChar("H").p("HELLO WORLD")
print(result)
let result1 = parserChar("E").p(result[0].1)
print(result1)
输出:
[("H", "ELLO WORLD")]
[("E", "LLO WORLD")]
这里我们把第一个 parser 的结果,喂给了第二个 parser。然后第二个 parser 在此基础上成功解析出了字符“E”。
抽象无止境
还是在单字符 Parser 的前提下。在解析中,我们往往解析的不仅仅是某个特定的字符,而是某一类字符,比如解析所有的数字字符,或者运算符字符。对于这样的需求,我们第一次抽象的结果parserChar
似乎就不够用了。我们来稍微修改一下:
func satisfy(condition : Character -> Bool) -> Parser<Character>{
return Parser { x in
guard let head = x.characters.first where condition(head) else{
return []
}
return [(head,String(x.characters.dropFirst()))]
}
}
为了更通用,我们把函数的名字改为了satisfy
, 其实内部实现都差不多。区别就是现在不再是接受一个单个字符的参数,而是接受一个函数condition, 什么样的函数呢? 还是从他的类型看起:Character->Bool ,代表判断某个字符是否符合条件,符合返回 true,否则返回 false。相当于我们可以通过这个condition 函数,来定义字符的范围。然后在实现的时候,我们不再是用首字母去做某种判等,而是把首字母传给 condition 函数,让这个函数来告诉 Parser 首字母是否符合条件。
比如,假设存在isDigit(Character) -> Bool
这样一个函数,来判断一个字符是否是数字。 我们就可以通过 satisy(isDigit)
来构造一个识别任何数字的 Parser。 让我们来试一下:
首先实现 isDigit
func isDigit(c : Character) -> Bool{
let s = String(c)
return Int(s) != nil
}
然后通过satisfy
构造 Parser
let pNum = satisfy(isDigit)
print(pNum.p("abcde"))
print(pNum.p("1asd"))
print(pNum.p("6asd"))
print(pNum.p("12asd"))
输出:
[]
[("1", "asd")]
[("6", "asd")]
[("1", "2asd")]
isDigit
实现很简单就不讲了,后来我们通过satisfy
构造了一个识别任何数字的 Parser pNum
, 然后分别尝试解析了4个字符串,第1个并不包含数字,所以失败了。后面分别都解析出了不同的数字。至少最后一个例子里后面的2为什么没有一起解析出来,是因为目前还没有实现多字符的逻辑。
整理一下今天的任务
今天说了很多,其实在 Parser 的实现上的进展来看,我们只是增加了如下代码:
struct Parser<a>{
let p : String -> [(a,String)]
}
func isDigit(c : Character) -> Bool{
let s = String(c)
return Int(s) != nil
}
func satisfy(condition : Character -> Bool) -> Parser<Character>{
return Parser { x in
guard let head = x.characters.first where condition(head) else{
return []
}
return [(head,String(x.characters.dropFirst()))]
}
}
PS:
- 函数式编程前期理解起来可能会比较绕,但熟悉了以后很容易就能写出非常简洁的代码。
- satisfy 其实非常强大。可多试试看配合不同的 condition 来产生不同的 Parser
有多强大?可以下载完整项目, 先查看一下都是被怎么用的。
想看更多内容? 可以关注我的知乎