preface
Functional responsive programming framework we should also use more, such as ReactiveCocoa, ReactiveX series (RxSwift, RxKotlin, RxJava), the internal implementation of these frameworks are based on the idea of functional programming to build. I still remember the interviewer asked me, “Have you read the source code of ReactiveCocoa? Have you seen the core function bind? Do you know how this function works?” . When answering this question, if the interviewer simply seen RAC source code, can say by his own impressions about process of this method, but only a smattering of these thoughts may also, but if you fully understand the functional programming, familiar with the concept of Monad, can know the bind method is actually part of the concept of Monad, RAC uses Monad to implement its Signal. At this point you are ready to present your performance to the interviewer.
As the title suggests, in this article we will use the idea of functional programming to build a small responsive framework. It has the ability to respond to callbacks and abstracts individual event data into a flow of fluid in a pipeline that can be transformed and then subscribed to.
This is the third article in the functional Programming series. If you’re interested in functional programming, you can read the first two:
- Functional programming – an overview of Functor, Monad, Applicative
- Functional programming – Integrate Monad into Swift
The principle of
What is the nature of functional responsiveness
Here is a conceptual diagram of the flow transformation idea:
In building a daily program logic, we always for some data conversion operations, here we will be the data conversion process abstraction into a wrapped pipe flow data, the data in the form of flow in the pipeline flow, when after the converter, the original data stream will be converted to new data flow, and then continue to flow. For data conversion operation, we will use some functions/methods, pass the operation data as arguments into the function/call the method on the operation object, and get the transformed result. At this point, the entire operation will run synchronously, and the conversion function will receive the old data for conversion, and return the new data upon success. In addition, you can place multiple converters in this pipe, and the data will be converted to the desired value after passing through several converters and flowing out of the pipe.
But, in fact, project will involve many asynchronous in the logic operation, such as some of the more time-consuming operations (database operations, network request), based on the event loop (RunLoop) of event listeners processing sensors to monitor (touch screen monitor, equipment), some of these actions will create a new thread in the background, When the processing is completed, the data will be fed back to the main thread, and some will get the events to be processed from the event queue for each cycle in the whole running cycle and distribute them to the corresponding Handler. All of these operations have something in common: the data is returned through a Callback.
Functional responsiveness is the problem solved by applying the idea of flow transformation to Callback.
Not long ago, I had the honor to attend the 2017 Swift Conference in China, where the author of RxSwift was invited to give a speech. In his speech, he explained the essence of RxSwift:
RxSwift just a callback! (RxSwift is a callback.)
One might wonder why callbacks don’t use a simple proxy pattern or a closure instead of building such a complex and heavyweight framework. Because what these functional and responsive frameworks do is combine callbacks with the idea of flow transformation, allowing developers to focus on the data conversion process without having to spend a lot of time on the design of callbacks, making it easy to write simple and elegant callbacks.
core idea
The idea of flow transformation is to abstract a data event as a fluid circulating in a pipeline, using a converter to convert it into a new data event, and with the implementation of a callback, we can say that the pipeline is built on a callback. At this point, we can clarify the relationship between pipes and data: pipes built on callbacks wrap around data. In other words, a call-back pipe acts as a Context that surrounds basic data values, and it has the ability to trigger events and listen for callbacks that we don’t need to focus on, we just want to focus on the transformation of the data.
Looking at the description of functional responsiveness above, you may have noticed that this is a good match for a very important concept in functional programming, which is Monad. Yes, the core of functional responsiveness is actually built on Monad, so to implement functional responsiveness, we have to build a Monad, let’s call it reactive Monad.
RACSignal is a Monad class that implements the bind and return functions in Monad. The bind method in ReactiveCocoa is not a completely standard Monad bind function. It has a different parameter type and a RACSignalBindBlock on the outside. That should be the flattenMap method in RACSignal, which is also based on a bind wrapper. So by implementing a responsive Monad, you can get a flattenMap method for free.
Because Monad is definitely a Functor, when you implement a responsive Monad, you can easily implement the map method of the corresponding Functor. Yes, the map method is not unique to RACSignal; it also comes from Functor in functional programming.
implementation
As a personal fan of Swift, I will implement a simple functional responsive framework based on the Swift language.
Event
First of all, we will implement Event. For example, in ReactiveCocoa and RxSwift, there are three types of events:
- Next represents a data flow element
- Completed indicates that the data flow has completed
- Error indicates that an error occurred in the data flow
The event I implemented is simpler and has only types next and error:
enum Event<E> {
case next(E)
case error(Error)
}
Copy the code
The generic E in Event represents the type of the data element in it. It is important to note that when the event type is error, the associated error instance has no type restriction. For a simple demonstration, I did not add a generic constraint error instance. You can optimize it a little later if you try to implement it yourself, for example:
enum Event<E, R> where R: Error {
case next(E)
case error(R)
}
Copy the code
Observer
The Observer does two things: send events and listen to events.
// MARK: - Protocol - Observer
protocol ObserverType {
associatedtype E
var action: (Event<E>) -> () { get }
init(_ action: @escaping (Event<E>) -> ())
func send(_ event: Event<E>)
}
extension ObserverType {
func send(_ event: Event<E>) {
action(event)
}
func sendNext(_ value: E) {
send(.next(value))
}
func sendError(_ error: Error) {
send(.error(error))
}
}
// MARK: - Class - Observer
final class Observer<Element>: ObserverType {
typealias E = Element
let action: (Event<E>) -> ()
init(_ action: @escaping (Event<E>) -> ()) {
self.action = action
}
}
Copy the code
With the send method, the Observer can send events, and by implementing a closure and passing it to the Observer constructor, we can listen for events emitted by the Observer.
Signal
Then comes the big story: Signal (a name I borrowed directly from ReactiveCocoa), which is the reactive Monad we mentioned above, the heart of the whole functional reactive thing.
Let’s start with the SignalType protocol:
// MARK: - Protocol - Signal protocol SignalType { associatedtype E func subscribe(_ observer: Observer<E>) } extension SignalType { func subscribe(next: ((E) -> ())? = nil, error: ((Error) -> ())? = nil) { let observer = Observer<E> { event in switch event { case .error(let e): error? (e) case .next(let element): next? (element) } } subscribe(observer) } }Copy the code
The protocol declares a method called SUBSCRIBE (_:) for subscribes to events, which takes an Observer. Based on this method, we can extend subscribe(Next :error) for special event types (next, error).
Here is the implementation of Signal:
// MARK: - Class - Signal final class Signal<Element>: SignalType { typealias E = Element private var value: E? private var observer: Observer<E>? init(value: E) { self.value = value } init(_ creater: (Observer<E>) -> ()) { let observer = Observer(action) creater(observer) } func action(_ event: Event<E>) { observer? .action(event) } static func `return`(_ value: E) -> Signal<E> { return Signal(value: value) } func subscribe(_ observer: Observer<E>) { if let value = value { observer.sendNext(value) } self.observer = observer } static func pipe() -> (Observer<E>, Signal<E>) { var observer: Observer<E>! let signal = Signal<E> { observer = $0 } return (observer, signal) } }Copy the code
We can see that Signal has an internal member attribute observer, to which we assign the arguments we pass when we call the subscribe(_:) method. The other member attribute, value, is used to enable Signal to implement the Monad return function, which, as I explained earlier in the Functional Programming series, wraps a basic piece of data around a Monad context. So in Signal I define the class method return(_:), which internally calls the Signal constructor init(value: E) for value initialization, assigning a basic data to the value member property. In the implementation of subscribe(_:), we first make a non-null judgment on value. If value exists, the incoming observer will send the next event associated with the value. This ensures that the entire Signal conforms to the Monad feature.
Next comes the init(_ creater: (Observer
) -> ()) constructor, which accepts a closure that performs operations on logic or event listeners, such as network requests, event listeners, etc. The closure takes an argument of type Observer, which is used to send the event when the operational processing logic in the closure completes or an event callback is received. Inside the constructor implementation, I first create an Observer instance by passing Signal’s own action(_:) method as an argument to the Observer constructor, where the action(_:) method does the following: Instructs the member attribute Observer to forward the event parameters it receives. The neat design here is that we do the processing logic or event listening in the constructor closure type parameter creater. If we get the result, we send the event using the Observer parameter in the closure. The event is passed to the subscribers who subscribed to the Signal, triggering the relevant callback.
One might wonder here: why do you need two observers to pass events? The creater closure can be called while the subscribe(_:) method is called, passing in the received subscriber. In fact, I do this to ensure that creater calls are synchronized with init(_ creater: (Observer
) -> ()) because I provide the pipe method in Signal.
The pipe method returns a binary, the first item being an Observer, which we can use to send events, and the second item being Signal, which we can subscribe to events, which is like the Subject in RxSwift, except here I have separated the event sender from the subscriber. Here’s one caveat:
As mentioned above, for the Observer obtained using the pipe function, the internal action member attributes come from the action(_:) method of Signal, which references the member attributes in Signal. If the Observer does not release the Signal, the Signal will be retained.
Signal implements Monad’s bind method:
// MARK: - Monad - Signal extension Signal { func bind<O>(_ f: @escaping (E) -> Signal<O>) -> Signal<O> { return Signal<O> { [weak self] observer in self? .subscribe(next: { element in f(element).subscribe(observer) }, error: { error in observer.sendError(error) }) } } func flatMap<O>(_ f: @escaping (E) -> Signal<O>) -> Signal<O> { return bind(f) } func map<O>(_ f: @escaping (E) -> O) -> Signal<O> { return bind { element in return Signal<O>.return(f(element)) } } }Copy the code
The bind method takes as an argument a function of type (E) -> Signal
. The E generic type is the type in the old Signal element, and O is the type in the new Signal element. This bind method does the same thing that a flattenMap in ReactiveCocoa or a flatMap in RxSwift does, so in my implementation of the flatMap method below I simply call bind directly. Many people call this process dimensionality reduction.
In our implementation of the bind method, we return a new Signal. To construct this Signal, we subscribe to the old Signal in the creater closure using the initialization method init(_ creater: (Observer
) -> ()). If the Observer of the old Signal emits an error event, the error instance associated with the error event is extracted, wrapped in the Observer passed as a parameter in the Creater closure, and then passed. If the Observer of the old Signal sends a Next event, the data elements associated with next are extracted, a middle-tier Signal is obtained by calling the function passed in by BIND, and the event is passed to the new Signal by subscribing to the middle-tier Signal.
In the Creater closure I used the [weak self] capture list to re-reference the old Signal to prevent cyclic references. Why would cyclic references happen here? As mentioned above, the Observer refers to the Signal, and in the Creater closure the old Signal refers to the Observer of the new Signal, thus it can be implied that the old Signal refers to the new Signal, which can cause circular references if not carefully.
The bind method in Monad handles the context automatically. In Signal, bind takes care of the subscription, transfer, and delivery of events for ourselves, and we only need to focus on pure data conversion.
The map method is implemented quite simply by calling bind internally and wrapping the final data into the Signal context with a return, which I won’t go into here.
That completes our implementation of responsive Monad!
The above is a very simple implementation of functional responsiveness. The purpose is to briefly introduce how to use functional programming ideas to accomplish reactive operations. There is no consideration of cross-thread scheduling.
Let’s test it out.
Simple to use
Build Signal with the Creater closure
let mSignal: Signal<Int> = Signal { observer in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
observer.sendNext(1)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
observer.sendNext(2)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
observer.sendNext(3)
}
}
mSignal.map { $0 + 1 }.map { $0 * 3 }.map { "The number is \($0)" }.subscribe(next: { numString in
print(numString)
})
Copy the code
Output:
The number is 6
The number is 9
The number is 12
Copy the code
Build Signal with PIPE
let (mObserver, mSignal) = Signal<Int>.pipe()
mSignal.map { $0 * 3 }.map { $0 + 1 }.map { "The value is \($0)" }.subscribe(next: { value in
print(value)
})
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
mObserver.sendNext(3)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
mObserver.sendNext(2)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
mObserver.sendNext(1)
}
Copy the code
Output:
The value is 10
The value is 7
The value is 4
Copy the code
extension
Next, we extend the functional responsiveness we just implemented by associating some of the classes we usually use.
UIControl
Listen for UIControl trigger events, traditionally by calling the addTarget(_:, Action :, for:) method, passing in the target and a callback Selector. Many people are tired of this approach, thinking that every time you listen for an event, you need to define an event handler, which is cumbersome, and wish you could just call the event through the closure.
Here’s a simple encapsulation to meet this requirement:
final class ControlTarget: NSObject {
private let _callback: (UIControl) -> ()
init(control: UIControl, events: UIControlEvents, callback: @escaping (UIControl) -> ()) {
_callback = callback
super.init()
control.addTarget(self, action: #selector(ControlTarget._handle(control:)), for: events)
}
@objc private func _handle(control: UIControl) {
_callback(control)
}
}
fileprivate var targetsKey: UInt8 = 23
extension UIControl {
func on(events: UIControlEvents, callback: @escaping (UIControl) -> ()) {
var targets = objc_getAssociatedObject(self, &targetsKey) as? [UInt: ControlTarget] ?? [:]
targets[events.rawValue] = ControlTarget(control: self, events: events, callback: callback)
objc_setAssociatedObject(self, &targetsKey, targets, .OBJC_ASSOCIATION_RETAIN)
}
}
Copy the code
Here I indirectly use the ControlTarget object to pass the UIControl event trigger to the closure and associate the object so that UIControl keeps a reference to the ControlTarget to prevent it from being released automatically. After the simple encapsulation above, we can use closures to listen for UIControl event callbacks in a very elegant way:
button.on(events: .touchUpInside) { button in
print("\(button) - TouchUpInside")
}
button.on(events: .touchUpOutside) { button in
print("\(button) - TouchUpOutside")
}
Copy the code
From this, we can simply extend our functional response based on the encapsulation above:
extension UIControl { func trigger(events: UIControlEvents) -> Signal<UIControl> { return Signal { [weak self] observer in self? .on(events: events, callback: { control in observer.sendNext(control) }) } } var tap: Signal<()> { return trigger(events: .touchUpInside).map { _ in () } } }Copy the code
The trigger(events:) method passes in an event type to be listened for and returns a Signal that emits an event when the corresponding event is triggered. Tap returns a Signal for the TouchUpInside event.
Simple and elegant to use like RxSwift or ReactiveCocoa:
button.tap.map { _ in "Tap~" }.subscribe(next: { message in
print(message)
})
Copy the code
The reference relation of the whole process above is: UIControl -> ControlTarget -> _callback -> Observer -> Signal; UIControl -> ControlTarget -> _callback -> Signal; Can work continuously throughout the RunLoop,
NotificationCenter
The function response ADAPTS to the control center in the same way as the UIControl extension, using an intermediate layer NotificationObserver for event passing and forwarding:
final class NotificationObserver: NSObject { private unowned let _center: NotificationCenter private let _callback: (Notification) -> () init(center: NotificationCenter, name: Notification.Name, object: Any? , callback: @escaping (Notification) -> ()) { _center = center _callback = callback super.init() center.addObserver(self, selector: #selector(NotificationObserver._handle(notification:)), name: name, object: object) } @objc private func _handle(notification: Notification) { _callback(notification) } deinit { _center.removeObserver(self) } } fileprivate var observersKey: UInt = 78 extension NotificationCenter { func callback(_ name: Notification.Name, object: Any? , callback: @escaping (Notification) -> ()) { var observers = objc_getAssociatedObject(self, &observersKey) as? [String: NotificationObserver] ?? [:] observers[name.rawValue] = NotificationObserver(center: self, name: name, object: object, callback: callback) objc_setAssociatedObject(self, &observersKey, observers, .OBJC_ASSOCIATION_RETAIN) } func listen(_ name: Notification.Name, object: Any?) Return Signal {[weak self] observer in self? .callback(name, object: object, callback: { notification in observer.sendNext(notification) }) } } }Copy the code
Therefore, we can monitor the changes of UITextFiled text based on the above responsive expansion of NotificationCenter:
extension UITextField { var listen: Signal<String? > { return NotificationCenter.default.listen(.UITextFieldTextDidChange, object: Self).map {$0. Object as? UITextField}. Map {$0?.text}} \($0 ?? "")" }.subscribe(next: { print($0) })Copy the code
Method call listener/proxy call listener
We sometimes want to listen for calls to specified methods on an object for section-oriented programming or burying points, and when functional responsiveness is introduced, we want it to act as a proxy, listening for calls to proxy methods. We can do this by extending functional responsiveness to support the above requirements. However, this is not easy to do. There are many Runtime features involved, such as method swap, method dynamic dispatch, ISA swap, etc. It may take a lot of effort and time to implement it. Due to my limited ability and time, I did not write the corresponding code. If you are interested, you can have a try. And if I made relevant efforts in the later period, I will also publish them.
Why there is no Disposable
If we get in touch with RxSwift and Reactivesswift, we will find that every time we subscribe to an Observable or Signal, we will get an instance returned by the subscription method for recycling resources, such as Disposable in RxSwift. We can call its Dispose method at a certain moment or put it into a DisposeBag to make the resource fully recycled at the end.
Going back to the reactive framework we implemented above, we need to pay close attention to the survival and release of resources because the implementation of this framework is very simple and does not return an instance that is specifically provided for us to release resources after subscribing. Here’s an example:
Above, we apply functional responsiveness to UIControl through an intermediate ControlTarget. In order to keep the ControlTarget instance alive, so that it does not automatically release, we wrap it with a collection. And set this collection as the associated object of the target UIControl. At this point we can think of the mid-level ControlTarget as a resource in the event flow pipeline whose destruction is determined by the target UIControl.
For RxSwift, it extends UIControl in the same way as we wrote, through a middle layer, but for the middle layer resource preservation and destruction, it takes a different approach, we can look at the source code of RxSwift (for simplicity, delete some of the extranet code) :
class RxTarget {
private var retainSelf: RxTarget?
init() {
self.retainSelf = self
}
func dispose() {
self.retainSelf = nil
}
}
Copy the code
This type of keepalive is clever in that it makes use of its own circular reference to keep alive, and when dispose method is called, it undoes the circular reference to itself and destroys itself.
Through the comparison of two examples above, we can know, for our own responsive implementation framework, we need to put some effort to keep alive and release resources, and the like RxSwift, it provides a unified way of resource management, more clear and elegant, by contrast, we are interested in can be implemented this way.
A link to the
Github – ReactiveObjc
Github – ReactiveCocoa
Github – RxSwift
This article is purely personal. If you find any errors in the article, feel free to post them in the comments section.