IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Swift函数式编程十一(解析器组合算子) -> 正文阅读

[移动开发]Swift函数式编程十一(解析器组合算子)

代码地址

解析类似1+2*3这样的数学表达式。

解析器类型

通常一个解析器会接受一个字符串,解析成功返回一些值和剩下的字符串,解析失败则什么也不返回。这个过程总结为这样一个函数类型:

typealias Parser<Result> = (String) -> (Result, String)?

将Parser类型定义为一个结构体而不是简单的类型别名,这样能将组合算子编写为Parser内的方法而不是一些无主的函数,代码也会更加易读:

struct Parser<Result> {
    typealias Stream = String
    let parse: (Stream) -> (Result, Stream)?
}

实现第一个解析器,一个能够解析出匹配条件字符的解析器:

func character(matching condition: @escaping (Character) -> Bool) -> Parser<Character> {
    return Parser { input in
        guard let char = input.first, condition(char) else { return nil }
        return (char, String(input.dropFirst()))
    }
}

测试解析器,从字符串中解析数字“1”:

let one = character { $0 == "1" }
if let v = one.parse("123") { print(v) }
/*输出:
 ("1", "23")
 */

为了方便使用解析器解析结果,为Parser添加一个run方法:

extension Parser {
    func run(_ string: String) -> (Result, String)? {
        guard let result = parse(string) else { return nil }
        return result
    }
}

if let v = one.run("123") { print(v) }
/*输出:
 ("1", "23")
 */

想解析的是任意数字而不只是1,那么需要创建一个数字解析器。为了检查某个字符是不是10进制数字需要用到CharacterSet类。为CharacterSet扩展一个contains方法,希望接受一个UnicodeScalar类型而检查类型是Character的值:

extension CharacterSet {
    func contains(_ c: Character) -> Bool {
        let scalars = c.unicodeScalars
        guard scalars.count == 1 else { return false }
        return contains(scalars.first!)
    }
}

有了上述方法,解析数字就方便了:

let digit = character { CharacterSet.decimalDigits.contains($0) }
if let v = digit.run("456") { print(v) }
/*输出:
 ("4", "56")
 */

下面将这些原子化的解析器组合为更强大的解析器。

组合解析器

希望解析的是整数而不是单独的数字,因此需要多次执行解析器digit将解析结果合并为一个整数。

首先,创建一个组合算子many用来多次执行某个解析器,并将解析结果做为一个数组返回:

extension Parser {
    var many: Parser<[Result]> {
        return Parser<[Result]> { input in
            var result: [Result] = []
            var remainder = input
            while let (element, newRemainder) = self.run(remainder) {
                result.append(element)
                remainder = newRemainder
            }
            return (result, remainder)
        }
    }
}

if let v = digit.many.run("123") { print(v) }
/*输出:
 (["1", "2", "3"], "")
 */

接下来的步骤只是将数字字符数组转化为一个整数,因此为Parser定义一个map方法将返回值为某种类型的解析器转换为返回值为另一种类型的解析器:

extension Parser {
    func map<T>(_ transform: @escaping (Result) -> T) -> Parser<T> {
        return Parser<T> { input in
            guard let (result, remainder) = self.run(input) else { return nil }
            return (transform(result), remainder)
        }
    }
}

有了many和map之后,定义整数解析器就非常方便了:

let integer = digit.many.map { Int(String($0)) }
if let v = integer.run("123") { print(v) }
/*输出:
 (Optional(123), "")
 */

如果解析了不止包含数字的字符串,那么从第一个不是数字的字符开始后面的字符串作为剩余部分返回:

if let v = integer.run("123abc") { print(v) }
/*输出:
 (Optional(123), "abc")
 */

解析器digit.many能对空字符串解析成功,如果对Int初始化方法的返回值做强制解包就会导致崩溃。

顺序解析

现在可以重复执行某个解析器,然后将结果进行组合。但是希望能够同时执行多个不同的解析器。出于这个目的,引入顺序组合算子followed(by:),接受另一个解析器作为参数,返回一个全新的解析器。该新解析器会将前两个解析器的结果组合为一个多元组返回:

extension Parser {
    func followed<A>(by other: Parser<A>) -> Parser<(Result, A)> {
        return Parser<(Result, A)> { input in
            guard let (result1, remainder1) = self.run(input) else { return nil }
            guard let (result2, remainder2) = other.run(remainder1) else { return nil }
            return ((result1, result2), remainder2)
        }
    }
}

利用followed(by:)定义一个乘法表达式的解析器:

let multiplication = integer
    .followed(by: character { $0 == "*" })
    .followed(by: integer)
if let v = multiplication.run("2*3") { print(v) }
/*输出:
 (((2, "*"), 3), "")
 */

由于越多次调用 followed(by:) 会导致越多层级的多元组嵌套,解析器的结果会越复杂,于是用前边定义的map将乘法表达式的结果计算出来:

let multiplication2 = multiplication.map { $0.0.0*$0.1 }
if let v = multiplication2.run("2*3") { print(v) }
/*输出:
 (6, "")
 */

改进顺序解析

每次使用 followed(by:)来组合两个解析器,其返回值都是一个多元组。与其包裹一个嵌套的多元组,不如向每个解析器的结果传入一个可执行的函数来避免这个问题。

有两种方法表示一个拥有多个参数的函数:既可以表示为一次获取所有参数的函数,也可以表示为一次只获取一个参数的柯里化函数。

将之前传入map的函数剥离出来:

func multiply(lhs: (Int, Character), rhs: Int) -> Int {
    return lhs.0*rhs
}

还可以写得更可读一些:

func multiply(_ x: Int, _ op: Character, _ y: Int) -> Int {
    return x*y
}

这个函数的柯里化版本:

func curriedMultiply(_ x: Int) -> (Character) -> (Int) -> Int {
    return { op in
        { y in
            return x*y
        }
    }
}
print(curriedMultiply(2)("*")(3))
/*输出:
 6
 */

每次将一个解析器的解析结果传入该函数,这样可以避免多元组嵌套的问题。

每次写一个柯里化函数比较费劲,可以定义一个curry函数将非柯里化函数转化为柯里化函数:

func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
    return { a in { b in f(a, b) } }
}

如何使用curriedMultiply来简化乘法解析器?第一个整数解析器的解析结果类型与curriedMultiply的第一个参数类型相同。所以使用map对integer的解析结果应用curriedMultiply:

let p1 = integer.map(curriedMultiply)

p1的解析结果类型跟curriedMultiply的返回类型一样为(Character) -> (Int) -> Int。

接下来使用followed(by:) 添加乘法符号解析器:

let p2 = p1.followed(by: character { $0 == "*" })

P2的解析结果类型是一个多元组为((Character) -> (Int) -> Int, Character),继续使用map将多元组的第二个元素作为参数传入第一个元素:

let p3 = p2.map { $0.0($0.1) }

此时p3的解析结果类型为(Int) -> Int,重复这个过程添加最后的整数解析器:

let p4 = p3.followed(by: integer)
let p5 = p4.map { $0.0($0.1) }

现在P5的解析结果类型是Int了:

if let v = p5.run("2*3") { print(v) }
/*输出:
 (6, "")
 */

将乘法解析器编写为一段连贯的代码:

let multiplication3 = integer.map(curriedMultiply)
    .followed(by: character { $0 == "*"} )
    .map { $0.0($0.1) }
    .followed(by: integer)
    .map { $0.0($0.1) }

已经没有嵌套的多元组了,不过代码依旧令人困惑,继续改进代码。

观察发现一个通用模式都是调用 followed(by:)来合并一个解析器然后使用map映射结果,而每次调用map时传入的函数体也是一样的。所以先可以为这种通用模式抽象一个顺序解析运算符<*>:

precedencegroup SequencePrecedence {
    associativity: left
    higherThan: AdditionPrecedence
}
infix operator <*> : SequencePrecedence
func <*><A, B>(lhs: Parser<(A) -> B>, rhs: Parser<A>) -> Parser<B> {
    return lhs.followed(by: rhs).map { $0.0($0.1) }
}

使用这个运算符,解析器代码会更易读:

let multiplication4 = integer.map(curriedMultiply)<*>character { $0 == "*" }<*>integer

不过代码看起来还是有点瑕疵,.map(curriedMultiply)位于第一个解析器和其他解析器之间,如果调换顺序,整句代码更加易读。定义一个运算符<^>,用于将一个前置函数传入解析器的map方法中:

infix operator <^> : SequencePrecedence
func <^><A, B>(lsh: @escaping (A) -> B, rsh: Parser<A>) -> Parser<B> {
    return rsh.map(lsh)
}

于是可以这样编写一个乘法解析器:

let multiplication5 = curriedMultiply<^>integer<*>character { $0 == "*" }<*>integer

一但使用这样的语法,解读解析器的具体功能变得更加容易。

另一种顺序解析

除了定义<*>,通常还会顺便定义另一种顺序解析运算符,同样合并两个解析器,但是会丢弃一个解析器的结果。比如“*”或“+”尾随一个整数这样的算术表达式,这种解析器就会排上用场。在那些情况中,并不关注运算符的解析结果,只要确保被解析的符号是存在的就可以了。

定义*> 运算符,参照<*>运算符:

infix operator *> : SequencePrecedence
func *><A, B>(lsh: Parser<A>, rsh: Parser<B>) -> Parser<B> {
    return curry { _, y in y }<^>lsh<*>rsh
}

let positive = character { $0 == "+" }*>digit
if let v = positive.run("+5") { print(v) }
/*输出:
 ("5", "")
 */

类似定义一个<*运算符,丢弃右侧解析器的结果:

infix operator <* : SequencePrecedence
func <*<A, B>(lsh: Parser<A>, rsh: Parser<B>) -> Parser<A> {
    return curry { x, _ in x }<^>lsh<*>rsh
}

let op = character { $0 == "*" }<*digit
if let v = op.run("*2") { print(v) }
/*输出:
 ("*", "")
 */

选择解析

假如希望解析器解析一个“*”或者一个“+”,处于这个目的,对Parser添加一个组合算子:

extension Parser {
    func or(_ other: Parser<Result>) -> Parser<Result> {
        return Parser { self.run($0) ?? other.run($0) }
    }
}

let star = character { $0 == "*" }
let plus = character { $0 == "+" }
let starOrPlus = star.or(plus)
if let v = starOrPlus.run("+") { print(v) }
/*输出:
 ("+", "")
 */

类似于顺序解析运算符,定义一个<|>运算符:

infix operator <|>
func <|><A>(lsh: Parser<A>, rsh: Parser<A>) -> Parser<A> {
    return Parser { lsh.run($0) ?? rsh.run($0) }
}
if let v = (star<|>plus).run("+") { print(v) }
/*输出:
 ("+", "")
 */

多次解析

前面的整数解析器有一个缺陷,组合算子many会导致解析器integer在解析空字符串时发生奔溃。这是因为many内部实现中即使一开始解析失败也会返回一个空数组作为解析结果。

为了解决这个问题,先单次运行这个解析器,而后再重复多次。使用顺序解析运算符重新定义many1:

extension Parser {
    var many1: Parser<[Result]> {
        return { x in { manyX in [x] + manyX } }<^>self<*>self.many
    }
}
if let v = digit.many1.run("1234") { print(v) }
/*输出:
 (["1", "2", "3", "4"], "")
 */

<^>之前的匿名柯里化函数将self的单个结果拼接到self.many解析返回的数组之前。这种写法可读性并不高,下面使用curry将非柯里化函数转化为柯里化函数:

extension Parser {
    var many2: Parser<[Result]> {
        return curry { [$0] + $1 }<^>self<*>self.many
    }
}
if let v = digit.many2.run("23456") { print(v) }
/*输出:
 (["2", "3", "4", "5", "6"], "")
 */

可选

有时希望特定字符被可选地解析,即无论解析还是不解析都不影响整个解析过程。定义可选组合算子optional:

extension Parser {
    var optional: Parser<Result?> {
        return Parser<Result?> { input in
            guard let result = self.run(input) else { return (nil, input) }
            
            return result
        }
    }
}

解析算术表达式

在使用解析器组合算子来编写算术表达式的解析器之前,需要考虑优先级规则比如,乘法比加法有更高的优先级。

目前除了对类似圆括号这样的语素有明显的缺失以外,还有更严重的问题:比如,一个加法表达式现阶段只能包含一个运算符"+",而可能会希望计算多个被加数的和。要实现这些并不难,但是它会让整个例子更复杂。因此在默许这些限制的情况下实现解析器。

好消息是,在使用解析器组合算子库时,代码会与算术表达式语法非常相似。例如解析表达式2*3+4*6/2-10,只需要将算术表达式按照优先级翻译为组合算子的代码就可以,首先解析乘法:

let mul = curry { return $0*($1 ?? 1) }<^>integer<*>(character{ $0 == "*" }*>integer).optional
if let v = mul.run("4*6")?.0 { print(v) }
/*输出:
 24
 */

在运算符 <^> 之前的部分是一个用于计算结果的函数,它接收两个参数。由于乘法表达式的第二部分在语法中被标记为可选,所以第二个参数是可选的。在运算符 <^> 之后,只需要写出表达式中不同部分的解析器就可以了:一个整数解析器,随后是一个符号*的字符解析器,再之后是另一个整数解析器。由于不需要字符解析器的结果,使用顺序解析运算符 *> 来丢弃该运算符左侧的解析结果。最后使用组合算子 optional 来标记后一个整数解析器是可选的。

接着按照优先级使用同样的方式来定义余下的解析器:

let division = curry { return $0/($1 ?? 1) }<^>mul<*>(character{ $0 == "/" }*>integer).optional
if let v = division.run("4*6/2")?.0 { print(v) }
/*输出:
 12
 */
let addition = curry { return $0+($1 ?? 0) }<^>mul<*>(character{ $0 == "+" }*>division).optional
if let v = addition.run("2*3+4*6/2")?.0 { print(v) }
/*输出:
 18
 */
let minus = curry { $0 - ($1 ?? 0) }<^>addition<*>(character{ $0 == "-" }*>integer).optional
if let v = minus.run("2*3+4*6/2-10")?.0 { print(v) }
/*输出:
 8
 */

也可以将这些代码重构为更精简的代码,比如编写一个函数来生成指定算术运算符的解析器。

更 Swift 化的解析器类型

前面定义的Parser是一种纯函数式的定义方案:函数 parse 没有任何副作用,而解析结果与剩余的输入字符串会作为一个多元组返回。

另一种解决方案,要用到关键字 inout:

struct Parser2<Result> {
    typealias Stream = String
    let pase: (inout Stream) -> Result?
}
func character2(matching condition: @escaping (Character) -> Bool) -> Parser2<Character> {
    return Parser2<Character> { input in
        guard let c = input.first, condition(c) else { return nil }
        input = String(input.dropFirst())
        return c
    }
}
var s = "cd"
let c = character2 { $0 == "c" }
if let v = c.pase(&s) { print(v) }
print(s)
/*输出:
 c
 d
 */

关键字 inout 允许修改输入的字符串,可以只返回解析结果而不再是同时包含了结果和剩余符号的多元组。值得注意的是,inout 的作用效果与 Objective-C 中将对某个值的引用进行传递是不同的。仍旧可以将该参数当做其它简单的值一样进行操作,区别在于,这个值会在函数返回的同时被 复制回去。因此,在使用 inout 时并不会产生危及全局的副作用,因为可变性被严格限制在某 个特定的变量上了。

使用 inout 参数可以简化一些组合算子的实现。比如,组合算子 many、map 现在可以编 写如下:

extension Parser2 {
    var many: Parser2<[Result]> {
        return Parser2<[Result]> { input in
            var result: [Result] = []
            while let element = self.pase(&input) {
                result.append(element)
            }
            
            if result.count > 0 { return result }
            else { return nil }
        }
    }
    func map<T>(transform: @escaping (Result) -> T) -> Parser2<T> {
        return Parser2<T>{ input in
            guard let result = self.pase(&input) else { return nil }
            
            return transform(result)
        }
    }
}
let digit2 = character2 { CharacterSet.decimalDigits.contains($0) }
s = "123"
if let v = digit2.pase(&s) { print(v) }
print(s)
/*输出:
 1
 23
 */
let integer2 = digit2.many.map {
    return Int(String($0))!
}
s = "12345abc"
if let v = integer2.pase(&s) { print(v) }
print(s)
/*输出:
 12345
 abc
 */

与之前的实现相比,鉴于每次调用 self.parse 时可以顺手修改 input,所以不必再去管理每一次解析之后的剩余部分,而像 or 这样的组合算子在实现时会更具技巧性:

extension Parser2 {
    func or(_ other: Parser2<Result>) -> Parser2<Result> {
        return Parser2<Result> { input in
            let original = input
            guard let result = self.pase(&input) else {
                input = original
                return other.pase(&input)
            }
            return result
        }
    }
}

由于在调用 self.parse 时会修改 input,需要先将 input 的值复制到另一个变量中储存起来,以确保在 self.parse 返回 nil 的时候,可以将 input 恢复为解析前的值。两种方案中编写参数的方式都很不错。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-04-04 12:22:54  更:2022-04-04 12:25:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/25 11:37:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码