📜 ⬆️ ⬇️

Reactive programming in Objective-C

Over time, programming languages ​​are constantly changing and evolving due to the emergence of new technologies, modern requirements or a simple desire to refresh the style of writing code. Reactive programming can be implemented using various frameworks such as Reactive Cocoa. It changes the framework of the imperative Objective-C language and this approach to programming has something to offer the standard paradigm. This, of course, attracts the attention of iOS developers.

ReactiveCocoa brings a declarative style to Objective-C. What do we mean by this? The traditional imperative style used by such languages ​​as C, C ++, Objective-C, and Java, etc., can be described as follows: You write directives for a computer program that must be executed in a certain way. In other words, you say "how to do" something. While declarative programming allows you to describe a control flow as a sequence of actions, “what to do,” without defining “how to do”.

ReactiveCocoa

Imperative vs Functional Programming


An imperative programming approach involves a detailed description of each step that a computer must take to complete tasks. In fact, the imperative style is used in native programming languages ​​(or used when writing machine code). This, by the way, is a feature of most programming languages.
')
On the contrary, the functional approach solves problems using a set of functions that must be performed. You define the input parameters for each function, and what each function returns. These two programming approaches are very different.

Here are the main differences of languages:

1. State changes


For pure functional programming, a state change does not exist, since there are no side effects. A side effect implies state changes in addition to the return value due to some external interaction. SP (reference transparency) subexpressions are often defined as “no side effects” and primarily relate to pure functions. The SP does not allow the execution of the function to have external access to the non-constant state of the function, because each subexpression is a function call by definition.

To clarify the essence of the matter, the pure functions have the following attributes:


Despite the fact that the functional approach minimizes side effects, they cannot be completely avoided, since they are an internal part of any development.

On the other hand, functions in imperative programming do not have reference transparency, and this may be the only difference between the declarative approach and the imperative one. Side effects are widely used for state and I / O. Commands in the source language can change state, which results in different values ​​for a single language expression.

What about ReactiveCocoa? This is a functional framework for Objective-C, which is a conceptually imperative language, not including explicitly pure functions. When trying to avoid a change in state, side effects are not limited.

2. First Class Objects


In functional programming, there are objects and functions that are first class objects. What does it mean? This means that functions can be passed as a parameter, assigned to a variable, or returned from a function. Why is this convenient? This allows you to easily manage execution blocks, create and combine functions in various ways without difficulty, such as function pointers (char * (* (** foo [] [8]) ()) []; - have fun!).

Languages ​​that use the imperative approach have their own peculiarities regarding first class expressions. What about Objective-C? It has blocks as closure implementations. Higher order functions (PID) can be modeled by taking blocks as parameters. In this case, the block is a closure, and a higher order function can be created from a specific set of blocks.

However, the process of manipulating FPP in functional languages ​​is faster and requires fewer lines of code.

3. Mainstream Management


Cycles in the imperative style are represented as calls to the recursion function in functional programming. Iteration in functional languages ​​is usually performed through recursion. Why? Probably for the sake of complexity. For Objective-C developers, loops seem to be much more favorable for the programmer. Recursion can cause difficulties, for example, excessive consumption of RAM.

But! We can write a function without using cycles or recursions. For each of the infinitely possible specialized actions that can be applied to each element of the collection, functional programming uses reusable iterative functions, such as “ map ”, “ fold ”, “ filter ”. These functions are useful for reorganizing source code. They reduce duplication and do not require writing a separate function. (read on, we have more information about this!)

4. Order of execution


Declarative expressions only show the logical connections of the arguments of the subexpression function and the constant state relations. So in the absence of side effects, the transition state of each function call occurs independently of the others.

The functional order of execution of imperative expressions depends on a non-constant state. Therefore, the order of execution matters and is implicitly determined by the organization of the source code. In this question, we can point out the difference between the evaluation strategies of both approaches.

Deferred computations or computations, of necessity, in functional programming languages ​​are strategies. In this case, the evaluation of the expression is postponed until its value is necessary; there we avoid duplicate evaluations ourselves. In other words, expressions are evaluated only when evaluating a dependent expression. The order of operations becomes uncertain.

In contrast, vigorous computation in an imperative language means that an expression will be evaluated as soon as it is bound to a variable. This implies dictation of the order of execution. Thus, it is easier to determine when subexpressions (including functions) will be calculated, because subexpressions can have side effects that affect the miscalculation of other expressions.

5. Amount of code


This is important, the functional approach requires writing less code than imperative. This means fewer failures, less code for testing, and a more productive development cycle. Since the system is constantly evolving and growing, it is important.

Main components of ReactiveCocoa


Functional programming works with concepts known as the future (read-only variable presentation) and promise (the read-only future representation of the variable). What is good about them? In imperative programming, you must work with already existing values, which leads to the need to synchronize asynchronous code and other difficulties. But the concepts of futures and promises allow working with values ​​that have not yet been created (the asynchronous code is written in a synchronous way).

Signalal

Signal


Future and promise are presented as signals in reactive programming. RACSignal is the core component of ReactiveCocoa. It gives the opportunity to present a stream of events that will be presented in the future. You subscribe to a signal and get access to events that will occur with time. A signal is a push-driven stream and can be a button click, asynchronous network operations, timers, other UI events, or anything else that changes over time. They can link the results of asynchronous operations and effectively combine multiple event sources.

Sequence


Another type of stream is sequence. Unlike a signal, a sequence is a pull-driven flow. This is a kind of collection that has a similar purpose as NSArray. RACSequence allows certain operations to be performed when you need them, rather than sequentially, as with the NSArray collection. Values ​​in a sequence are evaluated only when this is specified by default. Using only part of the sequence potentially improves performance. RACSequence allows Cocoa collections to be processed in a universal and declarative way. RAC adds the -rac_sequence method to most of the classes in the Cocoa collection so that they can be used as RACSequences .

Team


In response to certain actions, a RACCcommand is created and subscribes to the signal. This applies primarily to UI interactions. The UIKit categories provided by ReactiveCocoa for most UIKit controls give us the correct way to handle UI events. Let's imagine that we have to register a user in response to a button click. In this case, the command can represent a network request. When the process starts, the button changes its state to “inactive” and vice versa. What else? We can transmit an active signal in a team (reachability is a good example). Therefore, if the server is unavailable (which is our “enabled signal”), then the command will be unavailable, and each command of the associated control will reflect this state.

Examples of basic operations


Here are some diagrams of how basic operations work with RACSignals:

Merge / Merge


+ (RACSignal *)merge:(id<NSFastEnumeration>)signals; 

Merge

The result streams have both event streams combined together. Thus, "+ merge" is useful when you do not care about a particular source of events, but would like to process them in one place. In our example, stateLabel.text uses 3 different signals: execution, completion, errors.

 RACCommand *loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { // let's login! }]; RACSignal *executionSignal = [loginCommand.executionSignals map:^id(id value) { return @"Connecting.."; }]; RACSignal *completionSignal = [loginCommand.executionSignals flattenMap:^RACStream *(RACSignal *next) { return [[[next materialize] filter:^BOOL(RACEvent *event) { return event.eventType == RACEventTypeCompleted; }] map:^id(id value) { return @"Done"; }]; }]; RACSignal *errorSignal = [loginCommand.errors map:^id(id value) { return @"Sorry :("; }]; RAC(self.stateLabel, text) = [RACSignal merge:@[executionSignal, completionSignal, errorSignal]]; 

CombineLatest


 + (RACSignal *)combineLatest:(id<NSFastEnumeration>)signals reduce:(id (^)())reduceBlock; 

As a result, the stream contains the last values ​​of the streams being transmitted. If one of the threads does not matter, the result will be empty.

CombineLatest

When can we use it? Let's take our previous example and add more logic to it. It is useful to enable the login button only in the case when the user has entered the correct email and password, right? We can declare this rule as follows:

 ACSignal *enabledSignal = [RACSignal combineLatest:@[self.emailField.rac_textSignal, self.passwordField.rac_textSignal] reduce:^id (NSString *email, NSString *password) { return @([email isValidEmail] && password.length > 3); }]; 

* Now let's change our login command a bit and connect it to the actual loginButton

 RACCommand *loginCommand = [[RACCommand alloc] initWithEnabled:enabledSignal signalBlock:^RACSignal *(id input) { // let's login! }]; [self.loginButton setRac_command:loginCommand]; 

Flattenmap


 - (RACSignal *)flattenMap:(RACStream * (^)(id value))block; 

You create new streams for each value in the original stream using this function (f). The result stream returns new signals based on the values ​​generated in the original streams. Therefore, it can be asynchronous.

Flattenmap

Let's imagine that your authorization request to the system consists of two separate parts: get data from Facebook (ID, etc.) and transfer it to Backend. One of the requirements must be able to cancel the login. Therefore, client code must process the state of the login process in order to be able to cancel it. This gives a lot of generic code, especially if you can log in from multiple places.

How does ReactiveCocoa help you? This could be a login implementation:

 - (RACSignal *)authorizeUsingFacebook { return [[[FBSession rac_openedSession] flattenMap:^RACStream *(FBSession *session) { return [session rac_fetchProfile]; }] flattenMap:^RACStream *(NSDictionary *profile) { return [self authorizeUsingFacebookProfile:profile]; }]; } 

Legend:


+ [FBSession rac_openedSession] - a signal that leads to the opening of the FBSession . If necessary, this can lead to logging into Facebook .

- [FBSession rac_fetchProfile] - a signal that extracts profile data through a session, which is transmitted as self .

The advantage of this approach is that for the user the entire stream is fuzzy, represented by the only signal that can be canceled at any “stage”, be it Facebook login or Backend call.

Filter / Filter


 - (RACSignal *)filter:(BOOL (^)(id value))block; 

As a result, the stream contains the stream values ​​“a”, filtered according to the specified function.

Filter

 RACSequence *sequence = @[@"Some", @"example", @"of", @"sequence"].rac_sequence; RACSequence *filteredSequence = [sequence filter:^BOOL(id value) { return [value hasPrefix:@"seq"]; }]; 

Map


 - (RACSignal *)map:(id (^)(id value))block; 

Unlike FlattenMap, Map runs synchronously. The value of the property “a” passes through the specified function f (x + 1) and returns the displayed initial value.

Map

Suppose you need to enter the model header on the screen, applying some attributes to it. A map comes into play when “Applying certain attributes” is described as a separate function:

 RAC(self.titleLabel, text) = [RACObserve(model, title) map:^id(NSString *modelTitle) { NSDictionary *attributes = @{/*your custom font, paragraph style, etc*/}; return [[NSAttributedString alloc] initWithString:modelTitle attributes:attributes]; }]; 

How it works: combines self.titleLabel.text with the model.title changes, applying custom attributes to it.

Zip


 + (RACSignal *)zip:(id<NSFastEnumeration>)streams reduce:(id (^)())reduceBlock; 

Results stream events are created when each of the threads has generated an equal number of events. It contains values, one from each of the 3 combined threads.

Zip

For some practical examples, zip can be described as dispatch_group_notify For example, you have 3 separate signals and you need to combine their answers at a single point:

 NSArray *signals = @[retrieveFacebookContactsSignal, retrieveAddressBookContactsSignal]; return [RACSignal zip:signals reduce:^id (NSArray *facebookContacts, NSArray *addressBookContacts){ NSArray *mergedContacts = // let's merge them somehow ^_^ return mergedContacts; }]; 

Throttle


 - (RACSignal *)throttle:(NSTimeInterval)interval; 

With the help of a timer set for a certain period of time, the first value of stream “a” is transmitted to the result stream only at the end of the timer. If a new value is produced within a specified time interval, it holds the first value, preventing it from being transmitted to the result stream. Instead, a second value appears in the result stream.

Throttle

Amazing case: we need to perform a search query when the user changes the searchField. Standard task, yes? However, it is not very effective for building and sending a network request with every text change, since a textField can generate many such events per second, and you will come to inefficient use of the network.
The solution here is to add a delay, after which we will actually execute the network request. This is usually accomplished by adding NSTimer. With ReactiveCocoa, this is much easier!

 [[[seachField rac_textSignal] throttle:0.3] subscribeNext:^(NSString *text) { // perform network request }]; 

* An important note here is that all “previous” textFields are changed before the “last” ones are deleted.

Delay / Delay


 - (RACSignal *)delay:(NSTimeInterval)interval; 

The value received in stream “a” is delayed and transmitted to the result stream after a certain time interval.

Delay


As an analogue of - [RACSignal throttle:], delay will only delay sending “next” and “completed” events.

 [[textField.rac_textSignal delay:0.3] subscribeNext:^(NSString *text) { }]; 

What we like about Reactive Cocoa



With the help of the RAC framework, you can create and convert value sequences in a better, higher level way. RAC makes it easier to manage everything that is waiting for the completion of an asynchronous operation: network response, change of dependent value and subsequent reaction. At first glance, it is difficult to deal with him, but ReactiveCocoa is contagious!

Source: https://habr.com/ru/post/353552/


All Articles