函数式编程 - 玩转高阶回调函数

已经有一段时间没有写过东西了,虽每天都循环渡着咸鱼般的编码生活,但我对函数式编程的兴趣依旧高涨不退。这篇文章主要介绍的是一个非常有趣且实力强劲的函数,它有着高阶的特性,且它主要的作用就是用来实现回调机制,所以在标题中我称之为高阶回调函数;在文章的后面我会结合项目实战来演示它的实用性。本文代码由Swift编写,但是函数式编程的思想无论在哪种编程语言上都是相通的,所以后面你也可以使用一门支持函数式编程的语言来尝试实现一下这个函数。

初探

关于回调

我为这个高阶回调函数取了一个别名 —— Action。由名字可知,这个函数是基于事件驱动来构建的,它能在事件执行 -> 完成回调这一过程中能起着中枢引导的作用。

Callback

如上图所示,一个完整的回调过程主要由两个角色参与,一个是Caller(调用者),另外一个则是Callee(被调用者),首先,调用者向被调用者发起执行的请求,一些初始的数据将被传输到被调用者身上,被调用者收到请求后进行相应的操作处理,待操作结束后,被调用者则将操作的结果通过完成回调回传给调用者。

Action的优势

回调在日常的开发中随处可见,但是,通常来说我们构建一个完整的回调过程会将执行请求和完成回调置于不同的地方,打个比方:我们通过为UIButton添加target,当按钮被按下时,target对应的方法将被执行,此时你可能要往UIViewController或者ViewModel发起一个异步业务逻辑处理的请求,当业务逻辑处理完毕后,你能通过代理设计模式添加代理或者使用闭包来将处理结果回调回来,进而重新渲染你的按钮。这样,回调的请求执行和完成回调都将被分散到各处。

在事件驱动的策略中,我比较忌讳的一点是:当业务逻辑越来越复杂,事件可能会因为过多且没有一个好的方案来管理它们之间的关系,从而纵横穿插、到处乱飞,在维护或迭代中你可能需要花较长的时间来梳理好事件的关系和逻辑。在回调过程上,如果逻辑中存在大量的回调过程,每个回调过程的执行请求和完成回调都分散四周,就会出现上面所提及的情况,这会让代码的可维护性大大降低。

Action函数则是一个管理和引导回调的好助手。上图所示的蓝色框就是Action,它涵盖了回调过程中的执行请求以及完成回调,做到了回调过程中事件的统一管理。我们能在含有大量回调过程的逻辑中使用Action来提高我们代码的可维护性。

基本实现

下面来实现Action,Action只是一个具有特定类型的函数:

typealias Action<I, O> = (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<LoginInfo, NetworkResponse>

由此,我们就可以构建我们的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<A, B, C>(_ l: @escaping (A) -> B, _ r: @escaping (B) -> C) -> (A) -> C {
    return { v in r(l(v)) }
}

Action的组合原理与此相同,我们可以将两个或多个在初始值类型和回调结果类型有接连关系的Action组合成一个新的Action,为此可定义Action组合函数compose,函数实现为:

func compose<A, B, C>(_ l: @escaping Action<A, B>, _ r: @escaping Action<B, C>) -> Action<A, C> {
    return { input, callback in
        l(input) { resultA in
            r(resultA) { resultB in
                callback(resultB)
            }
        }
    }
}

组合函数的实现并不难,它其实就是对原有的两个Action进行回调的重组。

Action组合

如上图所示,就像上面所说到的函数组合,Action<A, C>其实是将Action<A, B>Action<B, C>两个的执行请求和完成回调有序地叠加在一次,它与函数组合的区别是:函数组合的调用是实时同步的,而Action组合的调用则是可适配非实时的异步情况。

为了方便,我们为Action的组合函数compose定义运算符:

precedencegroup Compose {
    associativity: left
    higherThan: DefaultPrecedence
}

infix operator >- : Compose

func >- <A, B, C>(lhs: @escaping Action<A, B>, rhs: @escaping Action<B, C>) -> Action<A, C> {
    return compose(lhs, rhs)
}

现在就来展示Action组合的强大威力:
回归到之前所说的Network Model,假设这个Model对网络发起的请求成功后响应的数据是一串JSON字符串而不是一个解析好的NetworkResponse,你就需要在这时对JSON进行解析转换,为此你需要编写一个专门用于JSON解析的解析器Parser,并为了提高性能把解析过程放到异步中:

final class Network {
    static let shared = Network()
    private init() { }
    
    typealias LoginAction = Action<LoginInfo, NetworkResponse>

    let loginAction: Action<LoginInfo, String> = { 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<String, NetworkResponse>
    
    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.jsonAction
let 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<A, B>(_ l: @escaping Action<A, B>, _ 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 >- : Compose
infix operator <- : Exec

func <- <A, B>(lhs: @escaping Action<A, B>, 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 = 32
extension 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<I, O> {
    private let _action: Action<I, O>
    init(action: @escaping Action<I, O>) {
        _action = action
    }
    
    func then<T>(_ action: @escaping Action<O, T>) -> Promise<I, T> {
        return Promise<I, T>(action: _action >- action)
    }
    
    func exec(input: I, callback: @escaping (O) -> ()) {
        _action(input, callback)
    }
}

只需要上面几行的代码,我们就能够基于Action来实现自己的PromisePromise的核心方法是then,我们可以基于Action组合函数compose来实现这个then函数。下来我们来使用一下:

Promise<String, String> { 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比较多,往同事们见谅😙😜

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容