轻松无痛实现异步操作串行
提起异步操作的序列执行,指的是有一系列的异步操作(比如网络请求)的执行有前后的依赖关系,前一个请求执行完毕后,才能执行下一个请求。
异步操作的定义
我们定义一般异步操作都是如下形式:
1
2
3
4
5
|
func asyncOperation(complete : ()-> Void){
//..do something
complete()
}
|
常规的异步操作都会接受一个闭包作为参数,用于操作执行完毕后的回调。
那异步操作的序列化会有什么问题呢? 看如下的伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
func asyncOperation(complete : ()-> Void){
//..do something
print("fst executed")
complete()
}
func asyncOperation1(complete : ()-> Void){
//..do something
print("snd executed")
complete()
}
func asyncOperation2(complete : ()-> Void){
//..do something
print("third executed")
complete()
}
|
我们定义了三个操作asyncOperation
,asyncOperation1
和 asyncOperation2
,现在我们想序列执行三个操作,然后在执行完后输出 all executed
。 按照常规,我们就写下了如下的代码:
1
2
3
4
5
6
7
|
asyncOperation {
asyncOperation1({
asyncOperation2({
print("all executed")
})
})
}
|
可以看到,明明才三层,代码似乎就有点复杂了,而我们真正关心的代码却只有 print("all executed")
这一行。但为了遵从前后依赖的时许关系,我们不得不小心的处理回调,以防搞错层级。如果层级多了就有可能像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
asyncOperation {
asyncOperation1({
asyncOperation2({
asyncOperation3{
asyncOperation4{
asyncOperation5{
print("all executed")
}
}
}
})
})
}
|
这就是传说中的callback hell
, 而且这还只是最clean的情况,实际情况中还会耦合很多的逻辑代码,更加无法维护。
用reduce来实现异步操作的串行
那是否有解决办法呢? 答案是有的。很多FRP的框架都提供了类似的实现,有兴趣的读者可以自行查看Promise、 ReactiveCocoa 和 RxSwift中的实现。
然后正如本节的标题所说,Swift提供了两个函数式的特性:
- 函数是一等公民(可以像变量一样传来传去,可以做函数参数、返回值
- 高阶函数,比如
map
和reduce
接下来我们就用这两个特性,实现一个更加优雅的方式来做异步操作串行。
1. 定义类型
为了方便书写,我们先定义一下异步操作的类型:
1
|
typealias AsyncFunc = (()->Void) -> Void
|
AsyncFunc 代表了一个函数类型,这样的函数有一个闭包参数(其实就是上面 asyncOperation 的类型)
2. 从串行两个操作开始
我们先化简问题,假设我们只需要串行两个异步操作呢? 有没有办法把两个异步操作串行成一个异步操作呢? 想到这里,我们可以YY出这样一个函数:
1
2
3
|
func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
}
|
concat函数,顾名思义,是连接的意思。指的是将两个异步操作:left
和right
串行起来,并返回一个新的异步操作。
那现在,我们来思考如何实现concat
函数,既然返回的是AsyncFunc
也就是一个函数,那我们可以先YY出这样的结构:
1
2
3
4
5
|
func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
return { complete in
}
}
|
仔细回忆 AsyncFunc
的类型: (()->Void) -> Void
,所以闭包参数complete
就对应前面的参数。
架子已经写好了,我们来思考要实现如何实现最终返回这个函数。根据concat的定义我们可以知道,我们最终返回的是一个 接受一个闭包作为参数, 先执行left,成功后执行right,成功后再执行传入的闭包。
你看,这样一分析,逻辑就非常清晰了,闭包参数就是complete
. 我们抽丝剥茧,找到了问题的本质,于是很容易可以写出:
1
2
3
4
5
6
7
8
9
|
func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
return { complete in
left{
right{
complete()
}
}
}
}
|
核心逻辑和我们最原始的版本其实并没有区别,区别就是不论再多个串行,我们都不需要写更多的嵌套了。
基于最开始的例子,我们测试一下:
1
2
3
4
5
|
let concatedFunction = concat(asyncOperation,
right: asyncOperation1)
concatedFunction {
print("all executed")
}
|
至此,我们以及成功的实现了把两个异步操作合并成一个串行的异步操作。
3. 定义一个运算符
让我们回过头去,再审视一个我们concact的签名:
1
|
func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
|
我们忘记什么函数,什么闭包,什么异步。就来看签名:他接收两个相同类型的参数,最后返回一个结果,结果的类型和参数一致。
像什么?像雾像雨又像风? 还是像加法像减法又像乘法?总之我们可以把他看做是某种运算,具备如下性质:
1
|
a -> b -> c = concact(a,b) -> c = concat( concat(a,b) , c) (-> 代表异步地串行执行)
|
既然是运算,我们干脆给他定义个运算符,修改我们的concat
函数如下所示, + 代表这是一种用来表示结合的运算,>代表他有前后的依赖关系,不满足交换律。+>
就是我们自己定义的异步串行运算符。
1
2
3
4
5
6
7
8
9
10
|
infix operator +> {associativity left precedence 150}
func +> (left:AsyncFunc,right:AsyncFunc) -> AsyncFunc{
return { complete in
left{
right{
complete()
}
}
}
}
|
这样,我们最开始的,五个异步操作串行执行的代码就可以改为这样:
1
2
3
4
5
6
7
8
9
|
let concatedFunction = asyncOperation +>
asyncOperation1 +>
asyncOperation2 +>
asyncOperation3 +>
asyncOperation4 +>
asyncOperation5
concatedFunction {
print("all executed")
}
|
我们先把五个操作串行成一个,然后执行它。
4. 串行任意多个异步操作
那你会说,如果我们有更多的异步操作呢?比如我们有一组异步操作:[AsyncFunc]
, 难道只能展开来一个个用 +>
来合并吗?
其实,现在我们有了串行运算符,那就很容易想到我们可以拿我们刚才实现的+>
运算符来reduce一组异步操作。继续用刚才的例子,我们先写下如下代码:
1
2
|
let reducedFunction = [asyncOperation,asyncOperation1,asyncOperation2].
reduce(【初始值】, combine: +>)
|
我们把刚才定义的三个异步函数扔到列表里,然后用我们的串行运算符+>
来reduce他,combine 其实就是+>
,但此时似乎又面临另外一个问题,【初始值】填什么?
每次思考reduce
的初始值都是一个哲学问题,大多数情况下我们不希望他参与运算,但又不得不让他参与运算(因为combine是个二元函数),所以我们希望reduce的初始值(记为initial
)具备如下性质:
combine(initial,x) = x
这种性质,大家应该能联想到一个类似的东西叫 CGAffineTransformIdentity
,往深了讲,这其实是一个代数问题,不过这里暂时不讨论。
在本例,我们的initial
可以定义为:
1
|
let identityFunc:AsyncFunc = {f in f()}
|
它是这样的一个函数,接受闭包作为参数,然后什么都不做,马上调用闭包。这里大家简单感受一下。_(:зゝ∠)
于是,我们完整的reduce版本可以定义为:
1
2
3
4
5
6
7
8
|
let identityFunc:AsyncFunc = {f in f()}
let reducedFunction = [asyncOperation,asyncOperation1,asyncOperation2,asyncOperation3,asyncOperation4,asyncOperation5].
reduce(identityFunc, combine: +>)
reducedFunction {
print("all executed")
}
|
首先定义了identityFunc
作为初始值,然后把我们开头定义的几个异步操作reduce成一个:reducedFunction
,然后调用了它,可以观察输出结果,和我们最开始写的嵌套版本是一样的。
引申的话题
带参数的串行
真实世界里,当我们需要串行异步操作的时候,一般后一个操作都需要前一个操作的执行结果。比如我们可能需要先请求新闻的列表,拿到新闻的id之后,再请求新闻的一些具体的信息,前后操作有数据上的依赖关系。(当然一般不这么搞,这里只是举个例子)
抽象的来看,我们要处理一组串行的操作,为了方便处理,我们希望函数的签名是一样的,偷懒的做法可以这样:
1
|
typealias AsyncFunc = (info : AnyObject,complete:(AnyObject)->Void) -> Void
|
定义闭包的类型为AnyObject->Void
,同时异步函数也接受一个AnyObject
的参数,这样在各个异步函数中通过把参数cast成字典,提取信息,操作完毕后把结果的值传到回调的闭包中。具体实现见一下节
如果嫌AnyObject
太丑的话也可以针对串行操作的场景设计一个protocol,然后用protocol作为参数的类型来传递信息。
错误处理
我们最终将一组异步操作,reduce成了一个异步操作,那如果中间某个操作出错了,我们该怎么知道呢? 其中一种实现,可以是:
1
|
typealias AsyncFunc = (info : AnyObject,complete:(AnyObject?,NSError?)->Void) -> Void
|
对比之前带参数的例子,唯一的区别就是在闭包的参数里加了一个NSError?,以及把AnyObject改成了optional,因为这里的AnyObject代表的是结果,如果失败了,结果自然就是nil.
于是,我们的核心,串行运算符可以变成这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func +>(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
return { info , complete in
left(info: info){ result,error in
guard error == nil else{
complete(nil,error)
return
}
right(info: info){result,error in
complete(result,error)
}
}
}
}
|
逻辑也是很直接的,我们首先尝试执行left,在left的回调中查看error是否是nil,如果不是,说明有错误,则立刻执行complete,并且带上这个error。否则再执行right,并将right的结果调用complete。然后在用+>
连接了一组异步操作的时候,一旦有错,这个逻辑就可以让错误一步步传播到最顶层,避免执行了冗余的代码。
一个稍微异步一点的例子
随便建一个single view application,在viewcontroller.swift
的顶部(swift 要求 operator 定义在 file scope,所以不能写在类里),添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
typealias AsyncFunc = (info : AnyObject,complete:(AnyObject?,NSError?)->Void) -> Void
infix operator +> {}
func +> (left:AsyncFunc,right:AsyncFunc) -> AsyncFunc{
return { info , complete in
left(info: info){ result,error in
guard error == nil else{
complete(nil,error)
return
}
right(info: info){result,error in
complete(result,error)
}
}
}
}
|
然后修改viewDidLoad为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let identity:AsyncFunc = {info,complete in complete(nil,nil)}
func dispatchSecond(afterSecond : Int, block:dispatch_block_t){
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(afterSecond) * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue(), block)
}
let async1: AsyncFunc = { info, complete in
dispatchSecond(2, block: {
print("oh, im first one")
complete(nil, nil)
})
}
let async2: AsyncFunc = { info, complete in
dispatchSecond(2, block: {
print("oh, im second one")
complete(nil, nil)
})
}
let async3: AsyncFunc = { info, complete in
dispatchSecond(2, block: {
print("shit, im third one")
complete(nil, nil)
})
}
let async4: AsyncFunc = { info, complete in
dispatchSecond(2, block: {
print("fuck, im fourth one")
complete(nil, nil)
})
}
let asyncDaddy = [async1,async2,async3,async4].reduce(identity, combine: +>)
asyncDaddy(info: 0) { (o, e) in
print("okay, im deadly a last one")
}
}
|
运行程序后,会每两秒有一个输出。:)
本文旨在抛砖引玉,其实swift的functional特性已经非常丰富,稍微探索一下是可以做出很多fancy的应用出来的。
在函数式编程的世界里,我们定义的
identity
加上+>
就是一种monoid,常见的monoid还有:加法: identity 就是 0 , +> 就对应 +
乘法:identity 就是 1 , +> 就对应 *一点有趣的思考: 刚才我们已经解释了,我们的
+>
运算符是不支持交换律的,因为是串行。那它是否支持结合律呢? 比如:(a +> b) +> c
是否等于a +> (b +> c)
?