函数式编程进阶 - 实现 Parser Combinator [Swift描述]

·  阅读 2158
函数式编程进阶 - 实现 Parser Combinator [Swift描述]

前言

在函数式编程的世界里,抽象组合往往密不可分:多个细粒度抽象通过特定的组合则形成更高粒度的抽象,而后高粒度的抽象又可以被再次组合、不断递进,一步一步地抬升代码抽象的高度。我在工程开发中所感受到的函数式编程的魅力,也正是体现在它强大的抽象能力上。

Parser(解析器)能分析输入,产生结果。如正则表达式引擎可以解析匹配输入的字符串、JSONSerialization可帮助 iOS/Mac 开发者将 JSON 解析成 Objective-C 中的 Dictionary。 Parser CombinatorParser的一种实现方式,其基于函数式编程思想,姿态十分优雅。使用它,我们可以非常方便地编写解析逻辑,代码简洁且易于理解。Parser Combinator在设计上充分体现了组合的思想,通过运用其多种Combinator(组合子),我们可以将若干的细粒度子Parser组合在一起以得到更粗粒度的Parser,而后,组合可以继续,抽象度也逐步提升。

在这篇文章中,我将使用 Swift 语言,逐步构建出一套轻量的Parser Combinator库,然后以此来编写一款简单的解析器。借此文章,我希望读者能和我一起深刻地感受函数式编程的魅力,并且加深对其相关概念的认识,方便日后能在项目工程上写出更优雅、抽象的函数式代码。

文章涉及到部分函数式编程的概念,如果读者对这部分不太熟悉,可翻阅我在之前写过的相关文章:

函数式编程 - Swift中的Functor(函子)、Monad(单子)、Applicative

函数式编程 - 将Monad(单子)融入Swift

函数式编程 - 酷炫Applicative(应用函子)

本文实现的Parser Combinator库代码已在 Github 上开源,以供大家参考:TangentW/Parsec

因本人技术水平有限,若文章存在谬误,还望大家指正。

模型

在实现Parser Combinator前,我们先来看看它的基本抽象模型。

基础模型

解析过程会消费输入,产生结果,这里的输入则为字符串。如下图所示,每一小格代表输入串中的一个元素,类型则为字符。Parser会消费若干元素进行解析处理,而后产生相应的结果;除此之外,Parser还会输出一个额外的状态:解析后所剩余的输入串,以供接下来的Parser继续解析处理。

Model

根据以上的描述,我们可使用 Swift 来表示Parser的类型:

typealias Parser<Value> = (String) -> Result<(Value, String), Error>
复制代码

Parser在这里被定义成了函数类型,因其解析后输出的结果不定,这里使用了Value泛型来指代结果的类型。函数的输入参数类型为String,返回的是Result。若解析成功,Result则装载了一个二元组,里面的值分别代表了解析的结果以及剩余的输入串;当解析失败时,错误信息也将通过Result装载返回。

举个例子,假设现在有一个能将字符串中最长数字前缀解析出来的Parser:

typealias Parser = (String) -> Result<(Int, String), Error>
复制代码

当输入字符串"123abc"时,Parser输出的则是.success((123, "abc"))。这里最长数字前缀被解析出来并转成了Int类型,连同解析后所剩余的输入串一起作为结果返回。

优化模型

以上描述的模型遵循了函数式编程典型的数据不可变+纯函数特性,也就是说这里没有可变的变量,且函数对于相同的输入仅有唯一的输出。但是这样对于 Swift 来说未免太苛刻了:有时候“可变”能够带来更多便利,另一方面,如果按照上面模型要求,Parser在解析完后还需要返回剩下的输入串,那么每次解析都会有String的实例被构建,这样显然对于性能来说不是一个好做法。所以,在实际使用 Swift 来实现Parser时,我们要对模型进行"Swift 特色"的优化:

typealias Parser<Value> = (Context) -> Result<String, Error>
复制代码

这个模型将不再遵循数据不可变+纯函数特性,不过非常适用于 Swift。在这里,Context是可变的,它将记录目前输入串所解析到的位置,以取代旧模型中直接返回剩余输入串的做法。

接下来的章节将会详细介绍模型中Context的概念以及Parser Combinator具体的实现。

实现

下面我们就来使用 Swift 实现这套轻量的Parser Combinator库。

Context

在一开始提到的“基础模型”中,Parser作为函数类型,参数就是输入串String,当其解析完成,除了会返回结果值,还会带上剩余的输入串。而在“优化模型”中,解析函数输入参数为Context,解析完成后只有结果值返回。能够这样优化的原因是我们不必每次解析后都返回剩余的输入串,只需要将输入串当前所解析到的位置做个记录,而这里Context就负责了这项工作,所以它是可变的

public final class Context {
    
    public typealias Stream = String
    public typealias Element = Stream.Element
    public typealias Index = Stream.Index
    
    public let stream: Stream
    
    public init(stream: Stream) {
        self.stream = stream
        _cursor = stream.startIndex
    }
    
    private var _cursor: Index
}
复制代码

以上代码,Context记录了输入串stream以及当前输入串所解析到的位置_cursor(私有,所以用下划线命名)。位置的类型为字符串索引Index,初始值为字符串的开头位置startIndex

Context还对外提供了消费方法:

// MARK: - Iterator

extension Context: IteratorProtocol {
    
    public func next() -> Element? {
        let range = stream.startIndex..<stream.endIndex
        guard range.contains(_cursor) else {
            return nil
        }
        defer {
            stream.formIndex(after: &_cursor)
        }
        return stream[_cursor]
    }
}
复制代码

这里我们让Context实现了IteratorProtocol,每次调用next()Context将通过步进_cursor从而消费并返回输入串中的一个元素(字符),当输入串在这之前已完全被消费完,这里则返回nil

Error

错误处理在解析中是十分必要的,当解析失败时,我们能通过错误信息清楚地了解失败原因。为此我们需要定义好错误的类型:

public struct Error: Swift.Error {
    
    public let stream: Context.Stream
    public let position: Context.Index
    public let message: String

    public init(stream: Context.Stream, position: Context.Index, message: String) {
        self.stream = stream
        self.position = position
        self.message = message
    }
}
复制代码

Error记录了输入串、输入串当前所解析到的位置以及以字符串类型表示的失败信息。当解析失败时,我们能通过Parser返回的Result获取到Error的实例,从而了解到失败的原因,以及当前解析所在的位置。

为了方便抛出错误,我们可以扩展Context,增加错误抛出的相关代码:

// MARK: - Error

public extension Context {
    
    func `throw`<T>(_ errorMessage: String) -> Result<T, Error> {
        .failure(error(with: errorMessage))
    }
    
    func error(with message: Stream) -> Error {
        .init(stream: stream, position: _cursor, message: message)
    }
}
复制代码

我们通过Context内部的数据构建了Error,这样Context就可以非常方便地向外抛出错误。

Parser

我们在前面模型章节所提到的Parser其实是函数类型:

typealias Parser<Value> = (Context) -> Result<String, Error>
复制代码

但是为了方便后续Combinator的扩展以及增强代码的可读性,我们将Parser定义为struct类型,并把函数直接包装在它体内:

public struct Parser<Value> {

    public typealias Result = Swift.Result<Value, Error>

    public let parse: (Context) -> Result
    
    public init(_ parse: @escaping (Context) -> Result) {
        self.parse = parse
    }
}
复制代码

我们接下来为Parser扩展一个便捷解析方法:

public extension Parser {
    
    func parse(_ stream: Context.Stream) -> Result {
        parse(.init(stream: stream))
    }
}
复制代码

方法parse虽然名字跟被包装在Parser里的函数一样,但是入参有所不同:这里入参直接是输入串,方法内部帮我们构造好了Context,并调用了被包装在Parser中的函数。使用此方法,我们可以直接输入字符串以得到解析结果。

初尝 Parser

OK,目前我们基本上已经定义好了Parser Combinator库核心的几个数据结构,接下来先试玩一下:

public let element = Parser<Context.Element> {
    if let element = $0.next() {
        return .success(element)
    } else {
        return $0.throw("unexpected end of stream")
    }
}

public let endOfStream = Parser<()> {
    if $0.next() == nil {
        return .success(())
    } else {
        return $0.throw("expecting end of stream")
    }
}
复制代码

以上我们定义了两个Parser

  • element:在解析时通过Context消费一个输入串元素(字符),如果输入串在之前已经被消费完了,则返回错误。
  • endOfStream:跟element一样会通过Context消费输入串,但是它预期的是输入串在之前已经被消费完毕,否则返回错误。

我们输入字符串,试运行一下这两个Parser:

let inputOne = "hello world!"
let inputTwo = ""

let inputOneResultOfElement = element.parse(inputOne)
let inputTwoResultOfElement = element.parse(inputTwo)

let inputOneResultOfEndOfStream = endOfStream.parse(inputOne)
let inputTwoResultOfEndOfStream = endOfStream.parse(inputTwo)
复制代码

输出:

inputOneResultOfElement: success("h")

inputTwoResultOfElement: failure(Error(stream: "", position: Swift.String.Index(_rawBits: 1), message: "unexpected end of stream"))

inputOneResultOfEndOfStream: failure(Error(stream: "hello world!", position: Swift.String.Index(_rawBits: 65793), message: "expecting end of stream"))

inputTwoResultOfEndOfStream: success()
复制代码

不错,这两个Parser符合预期地解析了输入。

Combinator

前言章节提到:Parser Combinator在设计上充分体现了组合的思想。借助Combinator(组合子)Parser被不断进行组合,抽象度也逐步提升。为此,我们需要定义各种Combinator

Monad

在这里,我们用静态方法just指代Monadreturn函数,用flatMap指代bind (>>=)函数:

public extension Parser {
    
    static func just(_ value: Value) -> Parser<Value> {
        .init { _ in .success(value) }
    }
    
    func flatMap<O>(_ transform: @escaping (Value) -> Parser<O>) -> Parser<O> {
        .init {
            switch self.parse($0) {
            case .success(let value):
                return transform(value).parse($0)
            case .failure(let error):
                return .failure(error)
            }
        }
    }
}
复制代码
  • just方法返回的Parser,其解析逻辑不消费任何输入串,只会将just的输入参数直接包装到Result中,作为解析结果返回。

  • flatMap里,我们会先运行当前Parser的解析逻辑,若解析成功,解析的结果将作为参数传入flatMap的闭包参数transform中;闭包调用将返回一个新的Parser,而后这个新Parser的解析逻辑将会继续运行;若当前Parser解析失败,错误信息将直接从flatMap返回。

justflatMap主要是针对解析成功后的结果进行映射,对于解析失败的情况,我们也能写出相对应的Combinator:

public extension Parser {
    
    static func error<T>(_ error: Error) -> Parser<T> {
        .init { _ in .failure(error) }
    }
    
    static func `throw`(_ errorMessage: String) -> Parser<Value> {
        .init { $0.throw(errorMessage) }
    }
    
    func flatMapError(_ transform: @escaping (Error) -> Parser<Value>) -> Parser<Value> {
        .init {
            switch self.parse($0) {
            case .success(let value):
                return .success(value)
            case .failure(let error):
                return transform(error).parse($0)
            }
        }
    }
}
复制代码

这里代码与上面的思想相同,就不再展开细说了。


Functor

完成Monad后,我们就可以很方便地实现Functor了,这里的核心就是map方法:

public extension Parser {
    
    func map<O>(_ transform: @escaping (Value) -> O) -> Parser<O> {
        flatMap {
            .just(transform($0))
        }
    }
}
复制代码

借助MonadflatMapjust方法,map就能如此简单地实现了。

map方法做的事情是对当前Parser解析后的结果做映射变换,就类似于 Swift 中对数组进行map操作:let arr = [1,2,3,4].map { $0 + 1 }

flatMap类似,map亦可编写针对错误情况的版本:

public extension Parser {
    
    func mapError(_ transform: @escaping (Error) -> Error) -> Parser<Value> {
        flatMapError {
            .error(transform($0))
        }
    }
}
复制代码

利用MonadFunctor的特性,我们可以结合之前提到的element写出一个简单的用于消费两个字符的Parser:

let twoElements = element.flatMap { first in
    element.map { second in
        String([first, second])
    }
}
let result = twoElements.parse("hello")
复制代码

以上代码 result 值为:result: success("he")

这里出现的两个element分别解析出了结果中对应的两个字符,map闭包则对解析得到的字符数组映射成了String


过滤

Swift 数组具有filter方法,可根据谓词过滤掉不想要的元素。Parser也可以做到类似的效果,我们可以直接为Parser定义一个同名的filter方法:

public extension Parser {
    
    func filter(_ label: String, predicate: @escaping (Value) -> Bool) -> Parser<Value> {
        flatMap {
            predicate($0) ? .just($0) : .throw(label)
        }
    }
}
复制代码

filter接受两个参数,第一个以字符串为类型,表示谓词判断失败时,返回的错误中所携带的提示信息;第二个则为谓词闭包,通过返回Bool以表示结果是否符合预期。

利用Monad的特性我们可以很方便地实现filter:通过在flatMap中调用谓词闭包以得知结果是否符合预期,如果符合,结果会直接返回,否则将抛出错误。

我们可以基于filterParser进行扩展,以此实现两个便携Combinator

public extension Parser {

    func equal(to value: Value) -> Parser<Value> where Value: Equatable {
        filter("expecting \(value)") { $0 == value }
    }

    func notEqual(to value: Value) -> Parser<Value> where Value: Equatable {
        filter("unexpected \(value)") { $0 != value }
    }
}
复制代码

以上两个Combinator作用于结果值遵循了Equatable协议Parser上,期望结果值等于/不等预期值。

下面就来试试:

let str = "hello"
let resultOne = element.equal(to: "h").parse(str)
let resultTwo = element.notEqual(to: "h").parse(str)
复制代码

结果值分别为:

resultOne: success("h")

resultTwo: failure(Error(stream: "hello", position: Swift.String.Index(_rawBits: 65793), message: "unexpected h"))
复制代码

因为element.equal(to: Character)element.notEqual(to: Character)这两种形式太常见了,我们可以直接将它们封装成函数:

public func char(_ char: Character) -> Parser<Character> {
    element.equal(to: char)
}

public func notChar(_ char: Character) -> Parser<Character> {
    element.notEqual(to: char)
}
复制代码

之前的代码就可以改写为:

let str = "hello"
let resultOne = char("h").parse(str)
let resultTwo = notChar("h").parse(str)
复制代码

有时候我们也希望对接下来的输入串进行字符串匹配,基于上面的char可以很容易地进行扩展:

public func string(_ string: String) -> Parser<String> {
    .init {
        for element in string {
            if case .failure(let error) = char(element).parse($0) {
                return .failure(error)
            }
        }
        return .success(string)
    }
}
复制代码

string进行尝试:

let parser = string("Hello")
let resultOne = parser.parse("Helao")
复制代码

得到的结果:

resultOne: failure(Error(stream: "Helao", position: Swift.String.Index(_rawBits: 262401), message: "expecting l"))

resultTwo: success("Hello")
复制代码

要左不要右、要右不要左

很多时候我们希望Parser能成功解析输入,但它的输出结果我们并不关心,从而选择忽略。如解析字符串字面量"Hello World",我们要的内容只是双引号"对里面的字符串,但是解析成对出现的双引号也是必不可少的。

这里我们可以利用MonadFunctor的特性来实现要左不要右、要右不要左Combinator

public extension Parser {
    
    func usePrevious<O>(_ next: Parser<O>) -> Parser<Value> {
        flatMap { previous in next.map { _ in previous } }
    }
    
    func useNext<O>(_ next: Parser<O>) -> Parser<O> {
        flatMap { _ in next }
    }
    
    func between<L, R>(_ left: Parser<L>, _ right: Parser<R>) -> Parser<Value> {
        left.useNext(self).usePrevious(right)
    }
}
复制代码

对于Parserleftright,有:

  • left.usePrevious(right):输入串会依次经过leftright的解析逻辑,但最后会忽略right的解析结果,只返回left的结果。
  • left.useNext(right):输入串会依次经过leftright的解析逻辑,但最后会忽略left的解析结果,只返回right的结果。
  • aParser.bwtween(left, right)则会依次经过leftaParserright的解析逻辑,但最后会忽略leftright的解析结果,只返回aParser的结果。

现在我们就可以编写一个用于解析以成对单引号包裹的字符字面量Parser了:

public let charLiteral = element.between(char("'"), char("'"))

let str = "'A'"
let result = charLiteral.parse(str)
复制代码

解析后得到的result值为success("A")


选择

有时候Parser在解析失败后,我们希望能有另一个Parser接替工作重新解析。就像 Swift 的??运算符一样,当左侧值非空,运算符值直接返回左侧值,否则将返回右侧值。以上场景正对应了函数式编程概念:Alternative

在为Parser实现Alternative前,我们先来扩展一下Error

public extension Error {
    
    func merge(with another: Error) -> Error {
        position > another.position ? self : another
    }
}
复制代码

这里为Error添加的方法merge用于合并错误,它会从两个Error中选取位置最远的那个然后返回。

为什么要增加这样一个方法?考虑到存在场景:我们希望当aParser解析失败后让bParser重新尝试,但aParserbParser最终都解析失败了,那么此时就会有两个Error产生,但是最终究竟选取哪个Error返回呢?merge就是帮我们做这个决策:选取解析位置最远的那个Error,这样能使最终得到的错误信息更加准确易懂。

接下来我们为Parser实现Alternative,其实就只是一个函数:or

// MARK: - Alternative

public extension Parser {
    
    func or(_ another: Parser<Value>) -> Parser<Value> {
        flatMapError { error in
            another.mapError(error.merge)
        }
    }
}
复制代码

若当前Parser解析失败,通过or参数传入的Parser将会重新尝试。若两者解析都失败了,解析过程分别产生的错误Error将被合并。


回溯

等等,上面关于选择的实现,是不是有点问题呀?

let str = "Hello"
let parser = char("W").or(char("H"))
let result = parser.parse(str)
复制代码

这段代码result的值不应该是成功的.success("H")吗,为什么最后得到的却是失败:failure(Error(stream: "Hello World", position: Swift.String.Index(_rawBits: 131329), message: "expecting H"))

Rewind

如上图所示,白色的三角形代表char("W")这个Parser即将解析到的位置,在此位置上元素是字符H,这并不满足要求,所以这个Parser解析错误。因为or的效果,传入的char("H")将会继续解析,但是因为之前已经消费掉了输入串中的一个元素,接下来即将要解析的位置则位于红色三角形处,这里的元素已经是字符e了,所以最终解析还是失败了。

要解决这个问题,我们就要进行回溯操作:当char("W")解析失败后,让char("H")要解析的位置还是处于白色三角形处。

要实现回溯,我们需要在解析前先记录输入串当前的解析位置,当需要回溯时,就以这个记录重置解析位置。为了代码优雅,我们可以为Context扩展以下方法:

public extension Context {
    
    func `do`<T>(_ work: (Context) -> Result<T, Error>) -> Result<T, Error> {
        let rewind = { [_cursor] in self._cursor = _cursor }
        let result = work(self)
        if case .failure = result { rewind() }
        return result
    }
}
复制代码

方法do接受一个nonescaping的闭包参数,这个闭包的参数为Context本身,返回值是一个带有泛型的Result,闭包会在do方法内部被调用。work闭包调用前,do会捕获当前Context的解析位置,构造出用于回滚的闭包。当闭包调用后,其结果值将会原封不动通过do方法返回,不过当结果表示的是失败时,回滚闭包将在方法返回前被调用,Context的解析位置将会被重置。

现在,我们可以稍微修改一下Parser的初始化方法,让所有Parser的解析逻辑都运行在Contextdo方法中:

public struct Parser<Value> {

    public typealias Result = Swift.Result<Value, Error>

    public let parse: (Context) -> Result
    
    public init(_ parse: @escaping (Context) -> Result) {
        self.parse = { $0.do(parse) } // ⚠️ here
    }
}
复制代码

OK,到此,带有回溯效果的Parser已实现完成。我们再来试一试前面的代码:

let str = "Hello"
let parser = char("W").or(char("H"))
let result = parser.parse(str)
复制代码

测试结果为success("H"),符合预期!


Many & Some

有时候我们希望多次运行某个Parser的解析逻辑,并把多次的结果收集起来。如多次调用element解析得到一个由多个字符组成的字符串。为此,我们可以构造manysome这两个Combinator

我们先来看somesomeParser须成功解析至少一次或多次。假设没有一次解析成功,则返回错误,而解析如果还未失败,那么将会一直运行下去,并用数组将之前的解析结果收集起来。

public extension Parser {
    
    var some: Parser<[Value]> {
        flatMap { first in
            .init {
                var result = [first]
                while case .success(let value) = self.parse($0) {
                    result.append(value)
                }
                return .success(result)
            }
        }
    }
}
复制代码

many要求则比some宽松:Parser允许成功解析零次或多次。若Parser第一次解析就失败了,结果将返回一个空数组:

public extension Parser {

    var many: Parser<[Value]> {
        some.or(.just([]))
    }
}
复制代码

manyor连接some和一个只返回空数组的Parser,所以当some解析失败时,空数组将作为结果返回。

接下来我们就可以实现一个用于解析以成对双引号包裹的字符串字面量Parser了:

public let stringLiteral = notChar("\"").many
    .map { String($0) }
    .between(char("\""), char("\""))
复制代码
  • notChar("\"").many构造了一个用于解析零个或多个不为双引号"的字符
  • 上面得到的结果类型是字符数组[Character],这里使用了map将结果转换成了String类型
  • between(char("\""), char("\""))则期望Parser结果位于双引号对之间

测试一下:

let str = #""Hello World""#
let result = stringLiteral.parse(str)
复制代码

得到的结果为:success("Hello World")

运算符

为了提升代码的简洁度和可读性,我们可以为一些Combinator拟定运算符:

precedencegroup AlternativePrecedence {
  associativity: left
}

precedencegroup FunctorPrecedence {
  associativity: left
  higherThan: AlternativePrecedence
}

infix operator <|> : AlternativePrecedence
infix operator *> : FunctorPrecedence
infix operator <* : FunctorPrecedence

public func <* <L, R>(lhs: Parser<L>, rhs: Parser<R>) -> Parser<L> {
    lhs.usePrevious(rhs)
}

public func *> <L, R>(lhs: Parser<L>, rhs: Parser<R>) -> Parser<R> {
    lhs.useNext(rhs)
}

public func <|> <T>(lhs: Parser<T>, rhs: Parser<T>) -> Parser<T> {
    lhs.or(rhs)
}
复制代码

至此,我们已经将整套Parser Combinator库实现完了。其实这里还漏缺了许多有趣且有用的Combinator,但考虑到文章篇幅问题就不在此一一详述了,感兴趣的读者可以尝试自己去摸索实现。Swift 针对字符和字符串也提供了很多 API,利用这些 API 我们也可以构造出很多新奇好玩的Combinator

接下来我们试着运用它来编写一个小小解析器。

小试牛刀

利用上面已经实现好的Parser Combinator库,我们试着去编写一款针对字符串键值对的解析器,规则如下:

键和值都是以双引号对包裹的字符串字面量,中间以箭头分割。形如:"key" => "value",其中箭头左右两边都允许有若干空格。通过换行,我们可以拟定多个键值对:

"name" => "Tangent"
"city" => "深圳"
"introduction" => "iOS开发者"
复制代码

从简单到复杂、从细粒度到粗粒度,接下来我们一步一步实现这款解析器:

首先为“空格”和“换行”构建Parser,方便后面使用:

public let space = element.filter("expecting whitespace") { $0 == " " }
    .map { _ in }

public let newline = element.filter("expecting newline") { $0.isNewline }
    .map { _ in }
复制代码

对于字符串字面量的解析,前面的章节已经实现了ParserstringLiteral,用于提取字符串字面量双引号对之间的内容。

接着是箭头的解析:

let arrow = string("=>").between(space.many, space.many)
复制代码

因为规则允许箭头左右两边有任意数量空格,所以这里使用了between(space.many, space.many)

下面构建键值对的Parser

typealias Entry = (key: String, value: String)

let entry: Parser<Entry> = stringLiteral.flatMap { key in
    (arrow *> stringLiteral).map { value in
        (key, value)
    }
}
复制代码

我们首先声明了键值对的类型为一个二元组(key: String, value: String),接着通过flatMapuseNextmap这三个CombinatorstringLiteral > arrow > stringLiteral三个Parser依次进行解析匹配,并把左右两个stringLiteral的值提取出来构建键值对Entry

最后,我们需要构建能够解析多个键值对的Parser

let entries = (entry <* (newline <|> endOfStream)).some
复制代码

因为规则要求每个键值对解析完后,紧接着要么是换行、要么是输入串的结尾(newline or endOfStream),所以这里使用了entry <* (newline <|> endOfStream)的组合,接着通过some来让解析匹配一次或多次。

以上,这个键值对小型解析器就完成了,我们来测试一下:

let string = """
"name" => "Tangent"
"city" => "深圳"
"introduction" => "iOS开发者"
"""
let result = entries.parse(string)
复制代码

结果输出:

success([(key: "name", value: "Tangent"), (key: "city", value: "深圳"), (key: "introduction", value: "iOS开发者")])
复制代码

总结

这篇文章带大家构建了一套轻量的Parser Combinator库,并以此实现了一款简单的解析器。目的是希望能让大家更加深刻地感受到函数式编程的魅力,并加深对函数式编程相关概念的认识。如果后续有时间,我可能也会考虑出一篇用Parser Combinator编写JSON解析器的文章。

如有问题或指正,欢迎评论,谢谢!

参考

haskell/parsec

用Haskell构建Parser Combinator(一)

分类:
iOS
标签: