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

链接:https://juejin.cn/post/6910585840255107079

前言

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

Parser(解析器)能分析输入,产生结果。如正则表达式引擎可以解析匹配输入的字符串、JSONSerialization可帮助 iOS/Mac 开发者将 JSON 解析成 Objective-C 中的 Dictionary。 Parser CombinatorParser的一种实现方式,其基于函数式编程思想,姿态十分优雅。使用它,我们可以非常方便地编写解析逻辑,代码简洁且易于理解。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记录了输入串、输入串

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值