lead

It’s been a while since I’ve written anything, and my interest in functional programming continues to grow despite my daily coding cycle. This article mainly introduces a very interesting and powerful function, it has high order properties, and its main function is used to implement the callback mechanism, so IN the title I call the high order callback function; Later in the article, I will demonstrate its practicability in combination with actual project practice. The code for this article is written by Swift, but the idea of functional programming is common in any programming language, so you can try implementing this function later in a language that supports functional programming.

A preliminary study

About the callback

I’ve given this higher-order callback an alias — Action. As the name suggests, this function is built on an event-driven basis and acts as a central guide to the event’s execution -> completion of the callback.

Pictured above, a complete correction process is mainly composed of two characters to participate in, is a Caller (Caller), another is the Callee (Caller), first of all, the Caller for execution by the Caller requests, some initial data is transmitted to the Caller, and by the Caller accordingly after receipt of the request processing operation, When the operation is complete, the called sends the result of the operation back to the caller through the completion callback.

The advantage of the Action

Callbacks are common in everyday development, but typically we build a complete callback process that places the execution of the request and completion of the callback in a different place, for example: By adding a target to UIButton, when the button is pressed, the target method will be executed, and you might make an asynchronous business logic request to UIViewController or ViewModel, and when the business logic is done, You can re-render your buttons by adding agents through proxy design mode or by using closures to call back the processing results. Thus, both the request execution of the callback and the completion of the callback will be scattered around.

In event-driven strategy, I compare taboo is: when the business logic is more and more complex, the event may be because too much and not a good solution to manage the relationship between them, and vertical and horizontal weaving, flying everywhere, in the maintenance or iteration you may need to take a longer time to comb the relationship between the event and logic. In the case of callbacks, if you have a large number of callbacks in your logic, and the execution request and completion callback for each callback are scattered around, this can make your code much less maintainable.

The Action function is a good helper for managing and guiding callbacks. The blue box shown in the figure above is the Action, which covers the execution of the callback request and the completion of the callback, so as to achieve unified management of events during the callback process. We can use actions in logic with a lot of callback procedures to improve the maintainability of our code.

Basic implementation

Let’s implement an Action, which is just a function of a specific type:

typealias Action<I.O> = (I, @escaping (O) -> ()) -> ()
Copy the code

Action function accepts two parameters, the first parameter is the caller requests by the caller to perform operations when the initial value of the incoming type I use generic parameters, the second parameter type for a function, can escape this function is called after the execution of the callback function, the parameters of the function using the generic parameter O, don’t return a value, Action itself is a function that returns no value.

The basic use

Suppose you are building the logic for a user login operation. You need to encapsulate the Network request in a Model named Network. By passing in a structure to the Model with login information, it will get the Network response for the login result for you.

First of all, we should formulate the structure of login information and network response:

struct LoginInfo {
    let userName: String
    let password: String
}

struct NetworkResponse {
    let message: String
}
Copy the code

Since the login information is the initial value of the callback process and the network response is the result value, the type of Action we should create should be:

typealias LoginAction = Action<LoginInfo.NetworkResponse>
Copy the code

Thus, we can build our Network Model:

final class Network {
    // Singleton mode
    static let shared = Network(a)private init() {}let loginAction: LoginAction = { input, callback in
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            if input.userName == "Tangent" && input.password == "123" {
                callback(NetworkResponse(message: "Successful landing."))}else {
                callback(NetworkResponse(message: "Login failed")}}}}Copy the code

In the implementation of Network Action above, I used the GCD delay method to simulate the asynchrony of Network requests. As can be seen, we treat the Action function as a first-class citizen in the Network and let it directly exist as an instance constant. Through the input parameter, We can get the login information passed in by the caller, and when the network request completes, we send the result back through callback.

So we can use the Network we just built like this:

let info = LoginInfo(userName: "Tangent", password: "123")
Network.shared.loginAction(info) { response in
    print(response.message)
}
Copy the code

The advanced

This shows you the basics of Action. In fact, Action is much more powerful than that! Let’s talk about the advanced use of Action.

combination

Before we get to Action composition, let’s take a look at a relatively simple concept — function composition:

If you have A function f of type A -> B, and A function G of type B -> C, and the value A is of type A, then you can write C = g(f(A)), and the value C is of type C. Therefore, we can define the operator., whose function is to combine functions together to form a new function, such as h = g. f, such as h(a) == g(f(a)), which is called function combination: to combine two or more functions that are connected in terms of parameters and return types to form a new function. We implement the operator with a function. Features:

func compose<A, B, C>(_ l: @escaping (A) -> B._ r: @escaping (B) - >C) - > (A) - >C {
    return { v in r(l(v)) }
}
Copy the code

We can compose two or more actions that have a connection between the initial value type and the callback result type into a new Action. For this purpose, we can define the Action composition function 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)
            }
        }
    }
}
Copy the code

The composition function is not difficult to implement. It is simply a recombination of the original two actions to call back.

Action

, Action

and Action

are ordered to execute the request and complete the callback, as shown above. Function combinations are called synchronously in real time, while Action combinations are called asynchronously in non-real time.
,>
,>
,>

For convenience, we define the operator for the compose function for the Action:

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)
}
Copy the code

Now to demonstrate the power of the Action combo: Going back to the Network Model, suppose that the Model responds to a successful request from the Network with a STRING of JSON strings instead of a parsed NetworkResponse, you need to parse and transform the JSON at this point. To do this, you need to write a Parser specifically for JSON parsing and put it in asynchron for better performance:

final class Network {
    static let shared = Network(a)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\": \" Login successful! \ "}"
            } else {
                data = "{\"message\": \" Login failed! \ "}"
            }
            callback(data)
        }
    }
}

final class Parser {
    static let shared = Parser(a)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 data parsing error!")); return }
            callback(NetworkResponse(message: message))
        }
    }
}
Copy the code

Using the Action combination, you can concatenate the entire callback process of network request -> data asynchronous parsing:

let finalAction = Network.shared.loginAction >- Parser.shared.jsonAction
let loginInfo = LoginInfo(userName: "Tangent", password: "123")
finalAction(loginInfo) { response in
    print(response.message)
}
Copy the code

Imagine that later business logic might add asynchronous operations to the database or other models, and you could easily extend this Action combination:

let finalAction = Network.shared.loginAction >- Parser.shared.jsonAction >- Database.shared.saveAction >- OtherModel.shared.otherAction >- ...
Copy the code

The request is separated from the callback

Action callback can be the execution of the request and complete the callback unified management, but in the daily project development, they are often separated from each other, for example: the page there is a button, is it you want when you click this button to pull the data to the remote server, the final show on the interface. In this process, the click event of the button is the execution request of the callback, and the display of the data after pulling is the completion of the callback. It may be that the place you want to display is not the button, it may be a Label, so that the separation of the execution request and the completion of the callback occurs.

To enable Action to separate the request from the callback, we can define a function:

func exec<A, B>(_ l: @escaping Action<A, B>, _ r: @escaping (B)- > () - > (A) - > () {return { input in
        l(input, r)
    }
}
Copy the code

The exec function argument list takes an Action that needs to be separated on the left and a callback on the right. The exec return value is also a function that sends the execution request event.

Here I also define an operator for the exec function and modify the compose operator slightly to give it a higher priority than the exec operator:

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)
}
Copy the code

Next, I use the Action group to show the separation of Action requests and callbacks:

// Combine actions and listen for callbacks
let request = Network.shared.loginAction
    >- Parser.shared.jsonAction
    <- { response in
        print(response.message)
    }

// Send a callback execution request
let loginInfo = LoginInfo(userName: "Tangent", password: "123")
request(loginInfo)
Copy the code

You can even separate and encapsulate actions into the Apple Cocoa framework. For example, here I created an extension to UIControl to make it Action compatible:

private var _controlTargetPoolKey: UInt8 = 32
extension UIControl {
    func bind(events: UIControlEvents, for executable: @escaping ((a)) - > ()) {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(a) {
            _callback()
        }
    }
}
Copy the code

The main role of the above code is the bind function, which takes a UIControlEvents and a callback function whose argument is an empty tuple. The callback function is executed when UIControl receives a specific event triggered by the user.

Now I’m going to build a UIViewController and combine it with Action composition, Action execution and callback separation, and UIControl Action extensions to show you how Action can be used in everyday projects:

final class ViewController: UIViewController {
    private lazy var _userNameTF: UITextField = {
        let tf = UITextField(a)return tf
    }()
    
    private lazy var _passwordTF: UITextField = {
        let tf = UITextField(a)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(a) {
        super.viewDidLoad()
        view.addSubview(_userNameTF)
        view.addSubview(_passwordTF)
        view.addSubview(_button)
        view.addSubview(_tipLabel)
        _setupAction()
    }
    
    override func viewDidLayoutSubviews(a) {
        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.textelse { 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(a) {
        let loginRequest = _fetchLoginInfo
            >- Network.shared.loginAction
            >- Parser.shared.jsonAction
            <- _render
        _button.bind(events: .touchUpInside, for: loginRequest)
    }
}
Copy the code

Action manages the various callback processes in a project to make the distribution of events clearer.

Promise ?

Those of you who have written front ends may have noticed that the Action idea is very similar to one of the components on the front end, Promise. Ha, in fact, we can easily build promises on our Swift platform using Action!

All we need to do is wrap the Action in a Promise class

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)
    }
}
Copy the code

With just a few lines of code above, we can implement our Promise based on Action. The core method of promises is then, which we can implement based on the Action composition function compose. Let’s use it:

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)
}

// Output: One Two Three Four
Copy the code

The final

I will not put the code of this article on Github, want to the students can private chat about me ~ oh, yesterday because of writing this article to two or three in the middle of the night, if today’s work I knock the bug more, forgive my colleagues 😙😜