链接:https://juejin.cn/post/6910585840255107079
前言
在函数式编程的世界里,抽象
与组合
往往密不可分:多个细粒度抽象通过特定的组合则形成更高粒度的抽象,而后高粒度的抽象又可以被再次组合、不断递进,一步一步地抬升代码抽象的高度。我在工程开发中所感受到的函数式编程的魅力,也正是体现在它强大的抽象能力上。
Parser(解析器)
能分析输入,产生结果。如正则表达式引擎可以解析匹配输入的字符串、JSONSerialization
可帮助 iOS/Mac 开发者将 JSON 解析成 Objective-C 中的 Dictionary。 Parser Combinator
是Parser
的一种实现方式,其基于函数式编程思想,姿态十分优雅。使用它,我们可以非常方便地编写解析逻辑,代码简洁且易于理解。Parser Combinator
在设计上充分体现了组合
的思想,通过运用其多种Combinator(组合子)
,我们可以将若干的细粒度子Parser
组合在一起以得到更粗粒度的Parser
,而后,组合可以继续,抽象度也逐步提升。
在这篇文章中,我将使用 Swift 语言,逐步构建出一套轻量的Parser Combinator
库,然后以此来编写一款简单的解析器。借此文章,我希望读者能和我一起深刻地感受函数式编程的魅力,并且加深对其相关概念的认识,方便日后能在项目工程上写出更优雅、抽象的函数式代码。
因本人技术水平有限,若文章存在谬误,还望大家指正。
模型
在实现Parser Combinator
前,我们先来看看它的基本抽象模型。
基础模型
解析过程会消费输入,产生结果,这里的输入则为字符串。如下图所示,每一小格代表输入串中的一个元素,类型则为字符。Parser
会消费若干元素进行解析处理,而后产生相应的结果;除此之外,Parser
还会输出一个额外的状态:解析后所剩余的输入串,以供接下来的Parser
继续解析处理。
根据以上的描述,我们可使用 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
库。
最近我加了一个iOS新裙,有想交流的可以了解一下,:891 / 488 / 181 里面还分享BAT,阿里面试题、面试经验,讨论技术,裙里资料直接下载就行, 大家一起交流学习!想要学习的直接来,不想的…那就算了吧,反正白嫖的你都不要。
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
记录了输入串、输入串