Swift函数式编程十一(解析器组合算子)
Posted 酒茶白开水
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 恢复为解析前的值。两种方案中编写参数的方式都很不错。
以上是关于Swift函数式编程十一(解析器组合算子)的主要内容,如果未能解决你的问题,请参考以下文章