引
已经有一段时间没有写过东西了,虽每天都循环渡着咸鱼般的编码生活,但我对函数式编程的兴趣依旧高涨不退。这篇文章主要介绍的是一个非常有趣且实力强劲的函数,它有着高阶的特性,且它主要的作用就是用来实现回调机制,所以在标题中我称之为高阶回调函数
;在文章的后面我会结合项目实战来演示它的实用性。本文代码由Swift
编写,但是函数式编程的思想无论在哪种编程语言上都是相通的,所以后面你也可以使用一门支持函数式编程的语言来尝试实现一下这个函数。
初探
关于回调
我为这个高阶回调函数
取了一个别名 —— Action
。由名字可知,这个函数是基于事件驱动来构建的,它能在事件执行 -> 完成回调
这一过程中能起着中枢引导的作用。
如上图所示,一个完整的回调过程主要由两个角色参与,一个是Caller(调用者)
,另外一个则是Callee(被调用者)
,首先,调用者向被调用者发起执行的请求,一些初始的数据将被传输到被调用者身上,被调用者收到请求后进行相应的操作处理,待操作结束后,被调用者则将操作的结果通过完成回调回传给调用者。
Action的优势
回调在日常的开发中随处可见,但是,通常来说我们构建一个完整的回调过程会将执行请求和完成回调置于不同的地方,打个比方:我们通过为UIButton添加target,当按钮被按下时,target对应的方法将被执行,此时你可能要往UIViewController或者ViewModel发起一个异步业务逻辑处理的请求,当业务逻辑处理完毕后,你能通过代理设计模式添加代理或者使用闭包来将处理结果回调回来,进而重新渲染你的按钮。这样,回调的请求执行和完成回调都将被分散到各处。
在事件驱动的策略中,我比较忌讳的一点是:当业务逻辑越来越复杂,事件可能会因为过多且没有一个好的方案来管理它们之间的关系,从而纵横穿插、到处乱飞,在维护或迭代中你可能需要花较长的时间来梳理好事件的关系和逻辑。在回调过程上,如果逻辑中存在大量的回调过程,每个回调过程的执行请求和完成回调都分散四周,就会出现上面所提及的情况,这会让代码的可维护性大大降低。
Action函数则是一个管理和引导回调的好助手。上图所示的蓝色框就是Action,它涵盖了回调过程中的执行请求以及完成回调,做到了回调过程中事件的统一管理。我们能在含有大量回调过程的逻辑中使用Action来提高我们代码的可维护性。
基本实现
下面来实现Action,Action只是一个具有特定类型的函数:
typealias Action = (I, @escaping (O) -> ()) -> ()复制代码
Action函数接受两个参数,第一个参数是调用者
请求被调用者
执行操作时所传入的初始值,类型使用泛型参数I
,第二个参数类型为一个可逃逸的函数,这个函数就是被调用者
执行操作完毕后的回调函数,函数的参数使用的是泛型参数O
,不返回值,Action自身也是一个不返回值的函数。
基本使用
假定你现在正在构建一个用户登陆操作的逻辑,你需要将网络请求封装在一个名为Network
的Model中,通过对这个Model传入带登陆信息的结构体它就能为你获取到登陆结果的网络响应,我们将使用Action一步一步实现此功能。
首先,我们先拟定好登陆信息以及网络响应的结构体:
struct LoginInfo { let userName: String let password: String}struct NetworkResponse { let message: String}复制代码
因为登陆信息是回调过程的初始值,网络响应是结果值,所以我们应该创建的Action的类型应该是:
typealias LoginAction = Action复制代码
由此,我们就可以构建我们的Network
Model了:
final class Network { // 单例模式 static let shared = Network() private init() { } let loginAction: LoginAction = { input, callback in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { if input.userName == "Tangent" && input.password == "123" { callback(NetworkResponse(message: "登陆成功")) } else { callback(NetworkResponse(message: "登陆失败")) } } }}复制代码
在上面Network
Action的实现中我使用了GCD
的延期方法来模拟网络请求的异步性,可以看到,我们把Action这个函数当成是Network
中的一等公民,让它直接作为一个实例常量而存在,通过input
参数,我们能获取到调用者传入的登录信息,当网络请求完成后,我们则通过callback
把结果回传出去。
于是,我们就能这样来使用刚刚构建好的Network
:
let info = LoginInfo(userName: "Tangent", password: "123")Network.shared.loginAction(info) { response in print(response.message)}复制代码
进阶
上面展示了Action的基本使用方法,事实上,Action的威力不仅仅如此!下面就来说说Action的进阶使用。
组合
在讲到Action的组合之前,我们先来看一个比较简单的概念 —— 函数组合
:
假设有函数f
,类型是A -> B
,有函数g
,类型是B -> C
,现有值a是属于类型A,于是你就能够写出式子: c = g(f(a))
,得到的值c
它的类型就是C
。由此我们可以定义操作符.
,它的作用就是将函数组合在一起,形成新的函数,如: h = g . f
,满足 h(a) == g(f(a))
,这样就叫做函数的组合:将两个或多个在参数和返回类型上有接连关系的函数组合在一起,形成新的函数。我们用一个函数来实现运算符.
的功能:
func compose (_ l: @escaping (A) -> B, _ r: @escaping (B) -> C) -> (A) -> C { return { v in r(l(v)) }}复制代码
Action的组合原理与此相同,我们可以将两个或多个在初始值类型和回调结果类型有接连关系的Action组合成一个新的Action,为此可定义Action组合函数compose
,函数实现为:
func compose (_ l: @escaping Action , _ r: @escaping Action ) -> Action { return { input, callback in l(input) { resultA in r(resultA) { resultB in callback(resultB) } } }}复制代码
组合函数的实现并不难,它其实就是对原有的两个Action进行回调的重组。
如上图所示,就像上面所说到的函数组合,Action<A, C>
其实是将Action<A, B>
和Action<B, C>
两个的执行请求和完成回调有序地叠加在一次,它与函数组合的区别是:函数组合的调用是实时同步的,而Action组合的调用则是可适配非实时的异步情况。
为了方便,我们为Action的组合函数compose
定义运算符:
precedencegroup Compose { associativity: left higherThan: DefaultPrecedence}infix operator >- : Composefunc >- (lhs: @escaping Action , rhs: @escaping Action ) -> Action { return compose(lhs, rhs)}复制代码
现在就来展示Action组合的强大威力: 回归到之前所说的Network
Model,假设这个Model对网络发起的请求成功后响应的数据是一串JSON字符串而不是一个解析好的NetworkResponse
,你就需要在这时对JSON进行解析转换,为此你需要编写一个专门用于JSON解析的解析器Parser
,并为了提高性能把解析过程放到异步中:
final class Network { static let shared = Network() private init() { } typealias LoginAction = Actionlet loginAction: Action = { info, callback in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { let data: String if info.userName == "Tan" && info.password == "123" { data = "{\"message\": \"登录成功!\"}" } else { data = "{\"message\": \"登录失败!\"}" } callback(data) } }}final class Parser { static let shared = Parser() private init() { } typealias JSONAction = Action let jsonAction: JSONAction = { json, callback in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { guard let jsonData = json.data(using: .utf8), let dic = (try? JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)) as? [String: Any], let message = dic["message"] as? String else { callback(NetworkResponse(message: "JSON数据解析错误!")); return } callback(NetworkResponse(message: message)) } }}复制代码
利用Action组合
,你就能够把网络请求 -> 数据异步解析
整个回调过程串联起来:
let finalAction = Network.shared.loginAction >- Parser.shared.jsonActionlet loginInfo = LoginInfo(userName: "Tangent", password: "123")finalAction(loginInfo) { response in print(response.message)}复制代码
试想一下,后面业务逻辑可能增加了数据库或其他Model的异步操作,你也能够很方便地为这个Action组合
进行扩展:
let finalAction = Network.shared.loginAction >- Parser.shared.jsonAction >- Database.shared.saveAction >- OtherModel.shared.otherAction >- ...复制代码
请求与回调分离
Action
可以将回调过程的执行请求和完成回调统一起来管理,但是,在日常的项目开发中,往往它们是处于互相分离的状况,举个例子:页面中有一个按钮,你希望的是当你点击这个按钮的时候向远程服务器拉取数据,最后展示在界面上。在这个过程中,按钮的点击事件就是回调的执行请求,而数据拉取完后显示在界面上就是完成回调,有可能你想要展示的地方并不是这个按钮,可能是一个Label,这样就出现了执行请求和完成回调分离的情况。
为了能让Action做到请求和回调的分离,我们可以定义一个函数:
func exec (_ l: @escaping Action , _ r: @escaping (B) -> ()) -> (A) -> () { return { input in l(input, r) }}复制代码
exec
函数的参数列表中,左边接受一个需要分离的Action,右边则是回调函数,exec
返回值也是一个函数,这个函数就是用来发送执行请求事件的。
下面我也为exec
函数定义了一个运算符,并对前面的compose
运算符进行稍微修改,让它的优先级比exec
运算符高:
precedencegroup Compose { associativity: left higherThan: Exec}precedencegroup Exec { associativity: left higherThan: DefaultPrecedence}infix operator >- : Composeinfix operator <- : Execfunc <- (lhs: @escaping Action , rhs: @escaping (B) -> ()) -> (A) -> () { return exec(lhs, rhs)}复制代码
接下来我结合Action组合
来展示一下Action请求与回调分离
的用法:
// 组合Action以及监听回调let request = Network.shared.loginAction >- Parser.shared.jsonAction <- { response in print(response.message) }// 发送回调执行请求let loginInfo = LoginInfo(userName: "Tangent", password: "123")request(loginInfo)复制代码
你甚至可以将Action分离封装到苹果Cocoa框架中,比如下面我创建了UIControl
的扩展,让其兼容Action:
private var _controlTargetPoolKey: UInt8 = 32extension UIControl { func bind(events: UIControlEvents, for executable: @escaping (()) -> ()) { let target = _EventTarget { executable(()) } addTarget(target, action: _EventTarget.actionSelector, for: events) var pool = _targetsPool pool[events.rawValue] = target _targetsPool = pool } private var _targetsPool: [UInt: _EventTarget] { get { let create = { () -> [UInt: _EventTarget] in let new = [UInt: _EventTarget]() objc_setAssociatedObject(self, &_controlTargetPoolKey, new, .OBJC_ASSOCIATION_RETAIN) return new } return objc_getAssociatedObject(self, &_controlTargetPoolKey) as? [UInt: _EventTarget] ?? create() } set { objc_setAssociatedObject(self, &_controlTargetPoolKey, newValue, .OBJC_ASSOCIATION_RETAIN) } } private final class _EventTarget: NSObject { static let actionSelector = #selector(_EventTarget._action) private let _callback: () -> () init(_ callback: @escaping () -> ()) { _callback = callback super.init() } @objc fileprivate func _action() { _callback() } }}复制代码
上面的代码主要的角色为bind
函数,它接受一个UIControlEvents
和一个回调函数,回调函数的参数是一个空元组。当UIControl接收到用户触发的特定事件时,回调函数将会被执行。
下面我将构建一个UIViewController
,并结合Action组合
、Action执行与回调分离
、UIControl的Action扩展
这几种特性,向大家展示Action
在日常项目中的实战性:
final class ViewController: UIViewController { private lazy var _userNameTF: UITextField = { let tf = UITextField() return tf }() private lazy var _passwordTF: UITextField = { let tf = UITextField() return tf }() private lazy var _button: UIButton = { let button = UIButton() button.setTitle("Login", for: .normal) return button }() private lazy var _tipLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 20) label.textColor = .black return label }()}extension ViewController { override func viewDidLoad() { super.viewDidLoad() view.addSubview(_userNameTF) view.addSubview(_passwordTF) view.addSubview(_button) view.addSubview(_tipLabel) _setupAction() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // TODO: Layout views... }}private extension ViewController { var _fetchLoginInfo: Action<(), LoginInfo> { return { [weak self] _, ok in guard let userName = self?._userNameTF.text, let password = self?._passwordTF.text else { return } let loginInfo = LoginInfo(userName: userName, password: password) ok(loginInfo) } } var _render: (NetworkResponse) -> () { return { [weak self] response in self?._tipLabel.text = response.message } } func _setupAction() { let loginRequest = _fetchLoginInfo >- Network.shared.loginAction >- Parser.shared.jsonAction <- _render _button.bind(events: .touchUpInside, for: loginRequest) }}复制代码
Action
统一管理了项目中的各种回调过程,让事件分布更加清晰。
Promise ?
写过前端的小伙伴们可能会发现Action
思想跟前端的一个组件Promise
非常相似。哈,事实上,我们可以用Action
轻易地构建一个我们Swift平台上的Promise
!
我们要做的,只需要将Action
封装在一个Promise
类中~
class Promise { private let _action: Action init(action: @escaping Action ) { _action = action } func then(_ action: @escaping Action ) -> Promise { return Promise (action: _action >- action) } func exec(input: I, callback: @escaping (O) -> ()) { _action(input, callback) }}复制代码
只需要上面几行的代码,我们就能够基于Action
来实现自己的Promise
。Promise
的核心方法是then
,我们可以基于Action组合
函数compose
来实现这个then
函数。下来我们来使用一下:
Promise{ input, callback in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { callback(input + " Two") }}.then { input, callback in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { callback(input + " Three") }}.then { input, callback in DispatchQueue.main.asyncAfter(deadline: .now() + 4) { callback(input + " Four") }}.exec(input: "One") { result in print(result)}// 输出: One Two Three Four复制代码
终
这篇文章的代码我就不放上Github了,想要的同学们可以私聊我~ 哎呀,昨天因为写这篇文章写到深夜两三点,若今天工作中我敲的bug比较多,往同事们见谅??