用 Swift 写个`函数式`的解释器(2)

639 阅读7分钟

上一篇文章里,我们已经完成了 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

有多强大?可以下载完整项目, 先查看一下都是被怎么用的。


想看更多内容? 可以关注我的知乎