使用函数式swift解决回调地狱

在抓学校的教务信息的时候使用了太多的嵌套回调, 于是找了一些解决回调地狱的方法, 参照付若愚的做法, 使用函数式swift将回调封类似于promisekit的做法进行封装. 无奈水平太菜, 看了好久才看明白点东西, 不总结下来怕以后还会忘orz.


全程以iOS中使用URLSession类的网络请求为例, url瞎写的, 能看懂就行XD

因为涉及到网络请求, 最好不使用playground进行测试, 会出现莫名其妙的问题, 同时因为涉及到http请求, 会遇到ATS问题, 参考这篇文章修改一下info.plist即可.

目录

回调地狱

一般网络请求时使用闭包来进行异步回调:

 1
 2
 3
 4
 5
 6
let url = URL(string: "http://shino.space")!
URLSession.shared.dataTask(with: url) { data?, response?, error? in
    /* 
    do something
    */
}.resume()

当连续进行多个请求, 而且后一个请求需要上一个请求的结果时, 我们的问题就来了. 因为异步回调不在一个线程中运行, 所以无法像单线程编程中通过代码的前后顺序确定执行的先后顺序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 单线程
print("firtst")
print("second") // 该行一定在上一行执行后执行
/*
first
second
*/

// 多线程
let url = URL(string: "http://shino.space")!
URLSession.shared.dataTask(with: url) { _, _, _ in
    print("first")
}.resume()
URLSession.shared.dataTask(with: url) { _, _, _ in
    print("second")
}.resume() // 无法保证该闭包一定在上一行执行后执行
/*
结果无法确定
*/

可以定义全局变量, 通过修改全局变量的顺序确定执行顺序, 不过更常见的是将闭包嵌套起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let url = URL(string: "http://shino.space")!
URLSession.shared.dataTask(with: url) { data?, response?, error? in
    /*
    do something first
    */
    URLSession.shared.dataTask(with: url) { data?, response?, error? in
        /*
        then, do other thing
        */
    }
}

不过这样的写法看上去很难懂, 嵌套层数多起来后, 修改和调试就更加困难了, 这种多层嵌套回调就是传说中的回调地狱(callback hell).

目标

我们今天的目标就是将上面这种嵌套的回调闭包变为链式调用的形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let url = URL(string: "http://shino.space")!
URLSession.shared.dataTask(with: url).flatMap {
    print("first")
    return URLSession.shared.dataTask(with: url)
}.flatMap {
    print("second")
    return URLSession.shared.dataTask(with: url)
}.execute {
    print("finally")
}
/*
first
second
finally
*/

现在看不太没关系……可以看出之前的嵌套定义变成了链式的函数调用, 在能够解决调用执行顺序的前提下又保证了代码的可读性.

回调函数的类型

在封装之前, 先看一下回调函数的类型, 最简单的异步调用函数就是只接受一个回调函数作为参数, 返回值为空, 它的作用是在后台线程完成执行后, 回到主线程调用回调函数;而对于简单的回调函数, 接受异步调用请求到的数据作为参数, 而返回值一般为空.

确定回调函数的类型后, 就可以将其封装成一个异步调用的结构体Async中了, 先假设回调函数只接受一个String类型参数, 我们慢慢扩大它的适用范围:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Async {
    let trunk: (@escaping(String) -> Void) -> Void

    init(trunk: @excaping(@escaping(String) -> Void) -> Void) {
        self.trunk = trunk
    }

    func execute(callback: (String) -> Void) {
        self.trunk(callback)
    }
}

trunk就是异步回调的本体, 可以用一个异步回调进行初始化, 通过execute方法传入一个回调函数, 开始执行异步回调. 对于@escaping关键字, 在定义闭包的作用域内没有执行的闭包, 被称为逃逸闭包, swift默认的闭包为非逸闭包, 在使用逃逸闭包时要使用@escaping关键字.

闭包

在进行对Async结构体的扩充之前, 先来复习一下闭包, 要不然下面的代码可能会很难懂. 闭包和匿名函数经常被弄混, 尤其在swift中它们的定义方法都是一样的. 匿名函数就是没有名称的函数, 很好理解;而闭包中还捕获了定义闭包的作用域中的一些变量. 简单点理解, 执行结果不随定义作用域改变而改变的就是函数, 而执行结果随定义作用域改变而改变的就是闭包. 为了说明方便, 下面就都叫作闭包吧……在swift中, 一个标准闭包的定义是这样的:

 1
 2
 3
 4
{ (str: String) -> String in
    print(str)
    return str
}

这就是一个接受一个String类型参数, 返回值也是String类型的一个闭包, 因为swift中的类型推断机制, 可以省略很多东西, 但是为了方便理解下面的内容, 暂时使用这种复杂的写法. 一定要记住上面的过程是在定义闭包而不是在执行, 要执行一个闭包需要这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//赋值给一个变量执行

let foo = { (str: String) -> String in
    print(str)
    return str
}
foo("print string")
/*
print string
*/

//定义之后直接执行
{ (str: String) -> String in
    print(str)
    return str
}("print string")
/*
print string
*/

如果一个函数接受一个闭包作为参数, 而且闭包参数是这个函数的后一个参数, 那么为了阅读方便, 闭包可以写成尾随闭包的型式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 定义一个参数为闭包的函数
func foo(closure: (String) -> Void)) {
    closure("print string")
}

// 传入一个闭包, 执行这个函数
foo { (str: String) in
    print(str)
}
/*
print str
*/

了解这些基本的闭包知识后, 就可以啃下面的重点了.

将嵌套化为链式

仔细看一下上面定义的Async结构体, 可以发现在初始化的过程中并没有确定回调函数是什么, 也就是说, 定义一个Async只是确定了在后台线程中会干什么, 而回到主线程后, 也就是回调函数的部分并没有确定下来, 只有在执行execute方法传入一个回调函数后才会执行这个Async中封装和异步回调. 要想让多个异步回调按顺序串联起来, 我们需要Async有这样一个方法:接受一个闭包作为参数, 返回一个新的Async, 这个闭包接受一个String类型的参数, 对应着上一个Async传给其回调函数的String, 在闭包中对这个String进行处理后生成一个新的Async, 作为这个闭包的返回值;而这个方法的返回值是为了进行下一步的链式调用.

只是看上面的文字说明肯定会懵逼的, 边上代码边解释吧. 首先, 函数定义是这样的:

 1
func flatMap(transform: @escaping(String) -> Async) -> Async

为什么叫faltMap一会再解释, 参数就是一个将String转换成Async的一个闭包, 对应着第一次回调后对加载调结果String的操作, 闭包的返回值是Async, 就说明着第二faltMap次回调还没有确定回调是什么, 也就是说还需要传入一个回调函数才能执行第二次回调, 这也就是flatMap函数的返回值还是Async的原因, 因为最后要执行execute方法执行最后一个Async.

 1
 2
 3
 4
 5
 6
 7
 8
func flatMap(transform: @escaping(String) -> Async) -> Async {
    return Async(trunk: @escaping{ (nextCallback: @escaping(String) -> Void) -> Void in
        self.execute(callback: {(result: String) -> Void in
            let nextAsync = transform(result)
            nextAsync.execute(callback: nextCallback)
        })
    })
}

我发现无论研究多长间, 再看一眼这个实现还是头疼orz. 写的时候避免了尾随闭包和类型推断, 对于这种复杂的闭包应该可以看得更方便一些吧. 再次提示一下:**只要最后一个链节没的执行execute方法, 那么整个链式调用就没有执行, 这一切都是在进行定义而已. **下面一点一点地分析, 因为返回值是Async所以直接return一个Async, 这个AsyncflatMap方法完成后供下一次调用的, 所以其回调函数命名为nextCallback.

闭包transform执行需要的String类型参数是第一次异步调用的结果, 所以首先要进行一次execute, 即self.execute(), nextAsync即为闭包transformAsync类型返回值. 到此第一次回调完成.

接着要进行第二次回调, 即nextAsync.execute(). 因为我们只知道第二次回调前要对第一次回调的结果做什么(就是闭包transform里所定义的), 而不知道第三次回调前要对第二次回调的结果做什么(因为还没有对第二次回调执行flatMap方法), 所以nextAsync.execute()的参数是flatMap返回值的Async的回调函数nextCallback.

这样定义Async的主要目标就达成了, 一个Async实例封装着一个异步调用, 如果调用execute方法, 传入一个回调函数, 完成异步回调;如果调用flatMap方法, 传入一个(String) -> Async的闭包, 完成第一次回调, 使用闭包的返回值Async等待着调用下一个execute或者flatMap方法进行下一次回调.

先来一波精简

加上尾随闭包和类型推断, 如果上面的都能理解的话, 下面的代码也没什么问题.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
extension Async {
    func flatMap(transform: @escaping(String) -> Async) -> Async {
        return Async { nextCallback in
            self.execute { result in
                let nextAsync = transform(result)
                nextAsync.execute(callback: nextCallback)
            }
        }
    }
}

泛型

下面要把Async变成泛型的结构体, 以适应各种类型参数异步调用, 现在的Async变成了这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Async<T> {
    let trunk: (@escaping(T) -> Void) -> Void
    
    init(trunk: @escaping(@escaping(T) -> Void) -> Void) {
        self.trunk = trunk
    }
    
    func execute(callback: @escaping(T) -> Void) {
        self.trunk(callback)
    }
}

extension Async {
    func flatMap<U>(transform: @escaping(T) -> Async<U>) -> Async<U> {
        return Async<U> { nextCallback in
            self.execute { result in
                let nextAsync = transform(result)
                nextAsync.execute(callback: nextCallback)
            }
        }
    }
}

原来的回调函数的(String) -> Void改为了(T) -> Void. 在flatMap方法中, 返回值的类型可能和原Async不同, 所以使用U型类, transform参数自然就是T类型变为Async<U>类型的闭包.

为什么要命名为flatMap

这就涉及到一些函数式编程的知识了, 一开始看的时候, 函子、单子这些概念看得我一脸懵逼, 但看明白之后, 发现这么做还是有必要的. 在Aditya Bhargava的文章里有很容易理解的解释. 文章中还有一些关于适用函子的解释, 这里不多提, 只谈Async涉及的函子和单子, 其他的可以参考上面的文章.

这是一个简单的

而将一个转换为另一个函数就是这样的

有的时候, 我们会将进行封装, 将其称为数据类型

而将一个转换为另一个的函数无法在封装后的值上起作用

这时我们就需要一种方法, 可以将原来只能作用在值上的函数作用在封装后的数据类型上, 它的实现应该是这样的:

我们把能实现这种方法的数据类型称为函子(functor), 这种方法通常命名为map. 对应着我们的Async<T>, 它是一个将异步回调封装后的数据类型, 如果把它变为函子, 那么应该实现这种方法:接受一个函数(闭包)作为参数, 这个函数是将一个值(T类型)娈为另一个值(U类型), 这个map方法的返回值还是一个封装后的值(Async<U>).

 1
 2
 3
extension Async {
    func map<U>(transform: @escaping(T) -> U) -> Async<U>
}

有的函数是将值转换为数据类型, 可以想像成一个管子, 输入是一个值, 输出是一个数据类型:

而这样的函数也不能直接运用在数据类型上, 因为输入变成了数据类型, 而不是裸露的值:

同样, 我们还需要一种方法, 将上面提到的函数能够运用到数据类型上, 这种方法通常称为flatMap, 实现了这种方法的数据类型被称为单子(monad). 对应着我们的Async, 要想将其变为单子, 就应该实现flatMap方法. 等等, 是不是发现方法名有点眼熟, 再看一下我们之前实现的flatMap

 1
 2
 3
extension Async {
    func flatMap<U>(transform: @escaping(T) -> Async<U>) -> Async<U>
}

参数transform就是一个输入为值T, 输出为封装Async<U>的函数, 它应用在数据类型Async上, 返回值还是一个封装的数据类型Async<U>.

可以看出, 在函数式编程的世界中, 函数就像一个个管子, 值或者数据类型在管子中流动, 管子之间可以互相连接, 实现着不同的功能. 函子和单子和存在使得函数之间可以任意连接. 在Async中实现函子和单子, 规范了方法命名, 可以让人一下子就看出来这是个单子, 也就明白了可以使用函数式编程的方式使用Async.

实现了map方法, 现在我们的Async就完成了, 其中的unit方法是用来将值T封装成数据类型Async<T>的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension Async {
    static func unit(x: T) -> Async<T> {
        return Async { (callback: (T) -> Void) -> Void in
            callback(x)
        }
    }

    func map<U>(transform: @escaping(T) -> U) -> Async<U> {
        return self.flatMap { (result: T) -> Async<U> in
            Async<U>.unit(x: transform(result))
        }
    }

    // map方法可以简化一下, 但我觉得会变得更难懂orz
    /*
    func map<U>(transform: @escaping(T) -> U) -> Async<U> {
        return self.flatMap {
            .unit(x: transform($0))
        }
    }
    */
}

封装URLSession

轮子造好了, 现在要使用它实现最开始提到的需求, 可以发现, URLSessiondataTask方法和我们封着的异步调用还是有点区别的, 它的回调函数接受三个参数, 而且还要调用resume方法才会开始进行网络请求, 所以得先对其进行扩展:

 1
 2
 3
 4
 5
 6
 7
 8
 9
extension URLSession {
    func dataTask(with url: URL) -> Async<Data?> {
        return Async { (callback: @escaping (Data?) -> Void) -> Void in
            URLSession.shared.dataTask(with: url) { (data: Data?, _, _) -> Void in
                callback(data)
            }.resume()
        }
    }
}

dataTask方法进行重载, 让其返回Async, 方便链式调用, 接受一个URL类型参数, 可以进行GET操作. 在返回的Async中, 调用一次dataTask方法和resume方法, 使得网络请求可以发出.

之后我们的问题就解决了, 再看一下最开始的代码就可以看懂了吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let url = URL(string: "http://shino.space")!
URLSession.shared.dataTask(with: url).flatMap {
    print("first")
    return URLSession.shared.dataTask(with: url)
}.flatMap {
    print("second")
    return URLSession.shared.dataTask(with: url)
}.execute {
    print("finally")
}
/*
first
second
finally
*/

对于POST请求和其它的异步调用的封装都差不了多少, 就不一一列举了.


emmmm, 上次更新服务器系统时忘了备份数据库, 再加上评论比较少, 暂时关闭评论功能