函數語言程式設計 - 玩轉高階回撥函式

TangentW發表於2018-03-21

已經有一段時間沒有寫過東西了,雖每天都迴圈渡著鹹魚般的編碼生活,但我對函數語言程式設計的興趣依舊高漲不退。這篇文章主要介紹的是一個非常有趣且實力強勁的函式,它有著高階的特性,且它主要的作用就是用來實現回撥機制,所以在標題中我稱之為高階回撥函式;在文章的後面我會結合專案實戰來演示它的實用性。本文程式碼由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比較多,往同事們見諒??

相關文章