函數式編程 - 玩轉高階回調函數

已經有一段時間沒有寫過東西了,雖天天都循環渡着鹹魚般的編碼生活,但我對函數式編程的興趣依舊高漲不退。這篇文章主要介紹的是一個很是有趣且實力強勁的函數,它有着高階的特性,且它主要的做用就是用來實現回調機制,因此在標題中我稱之爲高階回調函數;在文章的後面我會結合項目實戰來演示它的實用性。本文代碼由Swift編寫,可是函數式編程的思想不管在哪一種編程語言上都是相通的,因此後面你也可使用一門支持函數式編程的語言來嘗試實現一下這個函數。前端

初探

關於回調

我爲這個高階回調函數取了一個別名 —— Action。由名字可知,這個函數是基於事件驅動來構建的,它能在事件執行 -> 完成回調這一過程當中能起着中樞引導的做用。數據庫

Callback

如上圖所示,一個完整的回調過程主要由兩個角色參與,一個是Caller(調用者),另一個則是Callee(被調用者),首先,調用者向被調用者發起執行的請求,一些初始的數據將被傳輸到被調用者身上,被調用者收到請求後進行相應的操做處理,待操做結束後,被調用者則將操做的結果經過完成回調回傳給調用者。編程

Action的優點

回調在平常的開發中隨處可見,可是,一般來講咱們構建一個完整的回調過程會將執行請求和完成回調置於不一樣的地方,打個比方:咱們經過爲UIButton添加target,當按鈕被按下時,target對應的方法將被執行,此時你可能要往UIViewController或者ViewModel發起一個異步業務邏輯處理的請求,當業務邏輯處理完畢後,你能經過代理設計模式添加代理或者使用閉包來將處理結果回調回來,進而從新渲染你的按鈕。這樣,回調的請求執行和完成回調都將被分散到各處。json

在事件驅動的策略中,我比較忌諱的一點是:當業務邏輯愈來愈複雜,事件可能會由於過多且沒有一個好的方案來管理它們之間的關係,從而縱橫穿插、處處亂飛,在維護或迭代中你可能須要花較長的時間來梳理好事件的關係和邏輯。在回調過程上,若是邏輯中存在大量的回調過程,每一個回調過程的執行請求和完成回調都分散四周,就會出現上面所說起的狀況,這會讓代碼的可維護性大大下降。設計模式

Action函數則是一個管理和引導回調的好助手。上圖所示的藍色框就是Action,它涵蓋了回調過程當中的執行請求以及完成回調,作到了回調過程當中事件的統一管理。咱們能在含有大量回調過程的邏輯中使用Action來提升咱們代碼的可維護性。api

基本實現

下面來實現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比較多,往同事們見諒😙😜

相關文章
相關標籤/搜索