Reactive programming in Objective-C

Original author: Dima Vorona
  • Transfer
Over time, programming languages ​​are constantly changing and developing 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 scope of the imperative style of Objective-C, and this programming approach has something to offer the standard paradigm. This, of course, attracts the attention of iOS developers.

ReactiveCocoa brings declarative style to Objective-C. What do we mean by that? The traditional imperative style used by languages ​​such 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 the control flow as a sequence of actions, “what to do”, without defining “how to do”.

ReactiveCocoa

Imperative vs Functional Programming


An imperative approach to programming 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 characteristic feature of most programming languages.

On the contrary, a 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 between languages:

1. State changes


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

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

  • the only notable conclusion is the return value
  • the only dependency of the input parameters is the arguments
  • arguments are fully defined before generating any output

Despite the fact that a 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 referential transparency, and this may be the only difference between a declarative approach and an imperative one. Side effects are widely used to implement state and I / O. Commands in the source language can change state, resulting in different values ​​for the same language expression.

How about Reactive Cocoa? 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, the side effects are not limited.

2. Objects of the first class


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 the 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 characteristics regarding first-class expressions. What about Objective-C? It has blocks as closure implementations. Higher order functions (FWP) can be modeled by accepting 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 FVP in functional languages ​​is a faster way and requires fewer lines of code.

3. Mainstream management


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

But! We can write a function without using loops 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 features are useful for refactoring source code. They reduce duplication and do not require writing a separate function. (read on, we have more information about this!)

4. The order of execution


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

The functional order of imperative expressions depends on a volatile state. Therefore, the execution order matters and is implicitly determined by the organization of the source code. In this matter, we can point out the difference between the strategies for evaluating both approaches.

Deferred calculations or calculations, called up when necessary, in functional programming languages ​​are strategies. In this case, the evaluation of the expression is postponed until its value is necessary, whereby we avoid repeated evaluations. In other words, expressions are evaluated only when evaluating the dependent expression. The order of operations becomes uncertain.

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

5. Code amount


This is important, a functional approach requires writing less code than imperative. This means fewer crashes, less code to test, and a more productive development cycle. Since the system is constantly evolving and growing, this is important.

Main components of ReactiveCocoa


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

Signal

Signal


Future and promise are presented as signals in reactive programming. RACSignal is the main component of ReactiveCocoa. It provides an opportunity to present a stream of events that will be presented in the future. You subscribe to a signal and gain access to events that will occur over time. A signal is a push-driven stream and can be a button press, asynchronous network operations, timers, other UI events, or anything else that changes over time. They can bind 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 stream. This is a kind of collection that has the same purpose as NSArray. RACSequence allows certain operations to be performed when you need them, rather than sequentially, as with the NSArray collection . Values ​​in the sequence are evaluated only when specified by default. Using only part of the sequence potentially improves performance. RACSequence allows Cocoa collections to be handled in a universal and declarative way. RAC adds the -rac_sequence method to most Cocoa collection classes so that they can be used as RACSequences .

Command


In response to certain actions, RACCcommand is created and subscribes to the signal. This applies primarily to UI interactions. ReactiveCocoa provided UIKit categories for most UIKit controls, give us the correct way to handle UI events. Let's imagine that we need to register a user in response to a button click. In this case, the command may 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 “on 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 on how basic RACSignals operations work:

Merge


+ (RACSignal *)merge:(id)signals;

Merge

Result streams have both event streams merged together. Thus, "+ merge" is useful when you do not care about a specific 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)signals reduce:(id (^)())reduceBlock;

As a result, the stream contains the latest values ​​of the transmitted streams. If one of the threads does not matter, then 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 if 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 threads for each value in the original thread using this function (f). The result stream returns new signals based on the values ​​generated in the source streams. Therefore, it can be asynchronous.

Flattenmap

Let's imagine that your request for authorization in the system consists of two separate parts: receive data from Facebook (identifier, 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 boilerplate code, especially if you can log in from several 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 FBSession . If necessary, this can lead to a Facebook login .

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

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

Filter


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

As a result, the stream contains the values ​​of stream “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 property “a” passes through the given function f (x + 1) and returns the displayed initial value.

Map

Suppose you want to enter the model title on the screen, applying some attributes to it. Map comes into play when “Applying some 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: unites self.titleLabel.text with changes model.title , apply custom attributes to it.

Zip


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

Results flow events are created when each thread 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 responses 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;

Using a timer set for a certain period of time, the first value of the stream “a” is transmitted to the result stream only at the end of the timer. In the event that a new value is produced within a given time interval, it holds the first value, preventing it from being transferred to the result stream. Instead, a second value appears in the result stream.

Throttle

The amazing case: we need to do a search on demand when the user changes the searchField. Standard task, huh? However, it is not very effective for building and sending a network request each time the text is changed, since 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 actually execute the network request. This is usually achieved by adding NSTimer. With ReactiveCocoa it is much easier!

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

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

Delays / Delay


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

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

Delay


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

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

What we like about Reactive Cocoa


  • Introduces Cocoa Bindings with iOS
  • Ability to create operations on future data. Here is a little theory on futures & promises from Scala.
  • The ability to present asynchronous operations in a synchronous manner. Reactive Cocoa simplifies asynchronous software, such as network code.
  • Удобная декомпозиция. Код, который связанный с пользовательскими событиями и изменениями состояния приложения, может стать очень сложным и запутанным. Reactive Cocoa делает модели зависимых операций особенно простыми. Когда мы представляем операции в виде объединенных потоков (например, обработка сетевых запросов, пользовательские события, и т.д.), мы можем достигнуть высокой модульности и свободной связи, что приводит к более частому использованию кода повторно.
  • Поведения и отношения между свойствами определены как декларативные.
  • Решает проблемы с синхронизацией — если Вы объединяете несколько сигналов, тогда есть одно единое место для обработки всех результатов (будь то следующее значение, сигнал завершения или ошибки)

Using the RAC framework, you can create and transform sequences of values ​​in a better, higher-level way. RAC makes it easier to manage everything that expects the completion of an asynchronous operation: network response, change in the dependent value, and subsequent reaction. It is hard to deal with at first glance, but ReactiveCocoa is contagious!

Also popular now: