Functional programming-play with high-level callback functions

Functional programming-play with high-level callback functions

lead

I haven't written anything for a while. Although I cycle through my life of coding every day, my interest in functional programming is still rising. This article mainly introduces a very interesting and powerful function, which has high-level features, and its main function is to implement the callback mechanism, so I call it in the title ; I will later in the article Combine the actual combat of the project to demonstrate its practicality. The code in this article is Swiftwritten, but the idea of functional programming is the same no matter which programming language is used, so you can also use a language that supports functional programming to try to implement this function later.

Preliminary

About callbacks

I took an alias for this- Action. From the name, this function is based on event-driven construction, and it -> can play a central guiding role in this process.

As shown in the figure above, a complete callback process is mainly participated by two roles, one is Caller( )and the other is Callee( ). 1. the caller initiates an execution request to the callee, and some initial data will be transmitted to the callee. After the callee receives the request, the corresponding operation processing is performed. After the operation is completed, the callee returns the result of the operation to the caller through the completion callback.

Advantages of Action

Callbacks can be seen everywhere in daily development, but generally speaking, when we build a complete callback process, we put the execution request and the completion callback in different places. For example: we add a target to the UIButton, when the button is pressed When the target method is executed, you may need to initiate an asynchronous business logic processing request to UIViewController or ViewModel . When the business logic is processed, you can add a proxy through the proxy design pattern or use a closure to process it. The result is called back, and your button is rendered again. In this way, the execution of the callback request and the completion callback will be scattered everywhere.

In the event-driven strategy, I am more taboo: when the business logic becomes more and more complex, there may be too many events and there is no good plan to manage the relationship between them, so as to intersperse and fly everywhere. In maintenance or iteration, you may need to spend a longer time to sort out the relationship and logic of events. In the callback process, if there are a large number of callback processes in the logic, and the execution request and completion callback of each callback process are scattered around, the situation mentioned above will occur, which will greatly reduce the maintainability of the code.

The Action function is a good assistant for managing and guiding callbacks. The blue box shown in the figure above is the Action, which covers the execution request and the completion of the callback during the callback process, and achieves the unified management of events during the callback process. We can use Action to improve the maintainability of our code in logic that contains a large number of callback procedures.

Basic realization

Let's implement Action. Action is just a function of a specific type:

typealias Action<I, O> = (I, @escaping (O) -> ()) -> ()
 

The Action function accepts two parameters. The first parameter is the initial value passed in when the operation is requested . The type uses generic parameters I. The second parameter type is an escapeable function. This function is the callback after the operation is completed. Function, the parameters of the function use generic parameters Oand do not return a value. Action itself is also a function that does not return a value.

Basic use

Assuming that you are now building a user login operation logic, you need to encapsulate the network request in a named NetworkModel. By passing in a structure with login information to this Model, it can get the network response of the login result for you , We will use Action to implement this function step by step.

First of all, we first draw up the structure of the login information and network response:

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

struct NetworkResponse {
    let message: String
}
 

Because 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>
 

From this, we can build our NetworkModel:

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

In Networkthe implementation of Action above , I used the GCDdeferred method to simulate the asynchrony of network requests. As you can see, we regard the Action function as Networka first-class citizen, and let it exist directly as an instance constant. Through inputparameters, We can get the login information passed in by the caller, and when the network request is completed, we pass callbackthe result back.

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

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

Advanced

The above shows the basic usage of Action. In fact, the power of Action is more than that! Let's talk about the advanced use of Action.

combination

Before talking about the combination of Action, let's first look at a relatively simple concept -- :

Suppose there is a function f, the type is A -> B, there is a function g, and the type is B -> C, the existing value a belongs to the type A, so you can write the formula:, the type of the c = g(f(a))obtained value cis C. From this we can define the operator ., its role is to combine functions together to form a new function, such as:, h = g . fsatisfies h(a) == g(f(a)), this is called the combination of functions: two or more of the parameters and return types have a concatenation relationship The functions of are combined to form a new function. We use a function to implement the function of the operator .:

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

The principle of action combination is the same. We can combine two or more actions that have a contiguous relationship between the initial value type and the callback result type into a new Action. For this purpose, the Action combination function can be defined compose, and the function is implemented as:

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

The realization of the combined function is not difficult, it is actually a callback reorganization of the original two Actions.

As shown above, as a function of a combination of the above mentioned, Action<A, C>in fact, is Action<A, B>and Action<B, C>two of execution and completion callback requests in an orderly superposed, it is a function of the difference between a combination of: calling the function of a combination of real-time synchronization is , And the call of the Action combination is adaptable to non-real-time asynchronous situations.

For convenience, we composedefine operators for the combined function of 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)
}
 

Now let s show the powerful power of Action combination: Returning to the NetworkModel mentioned before , assuming that the response data of this Model after a successful request to the network is a string of JSON strings instead of a parsed one NetworkResponse, you need to To parse and convert JSON, you need to write a parser specifically for JSON parsing Parser, and put the parsing process in asynchronous in order to improve performance:

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

By using Action , you can connect the -> entire callback process in series:

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

Imagine that the following business logic may add asynchronous operations of the database or other Models, and you can easily Action extend this:

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

Separate request and callback

ActionThe execution request and completion callback of the callback process can be managed together. However, in daily project development, they are often separated from each other. For example: there is a button on the page, what you want is when you click this Pull the data from the remote server when the button is pressed, and finally display it on the interface. In this process, the click event of the button is the execution request of the callback, and the completion of the callback is displayed on the interface after the data is pulled. It may be that the place you want to display is not this button, it may be a Label, and then it appears The situation where the execution request and the completion callback are separated.

In order to allow Action to achieve the separation of request and 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)
    }
}
 

execIn the parameter list of the function, the left side accepts an Action that needs to be separated, and the right side is a callback function, and the execreturn value is also a function. This function is used to send execution request events.

Below I also execdefine an operator for the function, and composeslightly modify the previous operator to make it have a higher priority than the execoperator:

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

Next, I combine Action to show Action the usage:

// Action 
let request = Network.shared.loginAction
    >- Parser.shared.jsonAction
    <- { response in
        print(response.message)
    }

// 
let loginInfo = LoginInfo(userName: "Tangent", password: "123")
request(loginInfo)
 

You can even encapsulate the Action separately into the Apple Cocoa framework, such as UIControlthe extension I created below to make it compatible with 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()
        }
    }
}
 

The main role of the above code is a bindfunction, which accepts one UIControlEventsand a callback function, and the parameter of the callback function is an empty tuple. When UIControl receives a specific event triggered by the user, the callback function will be executed.

I'll build a following UIViewController, and in conjunction with Action , Action , UIControl Action these types of properties, to show Actionreal sex in daily project:

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

ActionUnified management of various callback processes in the project, making the distribution of events clearer.

Promise?

Friends who have written about the front-end may find that their Actionthinking Promiseis very similar to a component of the front-end . Ha, in fact, we can Actioneasily build one on our Swift platform Promise!

What we need to do is to Actionencapsulate it in a Promiseclass~

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

With just a few lines of code above, we can Actionimplement our own based on it Promise. PromiseThe core approach is thenthat we can based on Action functions composeto achieve this thenfunction. 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)
}

// : One Two 3.Four
 

end

I won t put the code of this article on Github anymore. Students who want can chat with me privately~ Oh, yesterday I wrote this article until two or three o clock late at night. If there are more bugs in my work today, go to Colleagues forgive me