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 Swift
written, 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 O
and 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 Network
Model. 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 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: " "))
}
}
}
}
In Network
the implementation of Action above , I used the GCD
deferred method to simulate the asynchrony of network requests. As you can see, we regard the Action function as Network
a first-class citizen, and let it exist directly as an instance constant. Through input
parameters, We can get the login information passed in by the caller, and when the network request is completed, we pass callback
the 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 c
is C
. From this we can define the operator .
, its role is to combine functions together to form a new function, such as:, h = g . f
satisfies 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 compose
define 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 Network
Model 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
Action
The 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)
}
}
exec
In 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 exec
return value is also a function. This function is used to send execution request events.
Below I also exec
define an operator for the function, and compose
slightly modify the previous operator to make it have 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)
}
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 UIControl
the 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 bind
function, which accepts one UIControlEvents
and 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 Action
real 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)
}
}
Action
Unified 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 Action
thinking Promise
is very similar to a component of the front-end . Ha, in fact, we can Action
easily build one on our Swift platform Promise
!
What we need to do is to Action
encapsulate it 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)
}
}
With just a few lines of code above, we can Action
implement our own based on it Promise
. Promise
The core approach is then
that we can based on Action
functions compose
to achieve this then
function. 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