Главная » Хабрахабр » [Перевод] Реактивное программирование в Objective-C

[Перевод] Реактивное программирование в Objective-C

Со временем языки программирования постоянно изменяются и развиваются из-за появления новых технологий, современных требований или простого желания освежить стиль написания кода. Реактивное программирование можно реализовать с помощью различных фреймворков, таких как Reactive Cocoa. Он изменяет рамки императивного стиля языка Objective-C и у такого подхода к программированию есть что предложить стандартной парадигме. Это, безусловно, и привлекает внимание iOS разработчиков.

Что мы подразумеваем под этим? ReactiveCocoa привносит декларативный стиль в Objective-C. д. Традиционный императивный стиль, который используют такие языки как: C, С++, Objective-C, и Java и т. Другими словами, вы говорите «как сделать» что-то. можно описать так: Вы пишете директивы для компьютерной программы, которые должны быть выполнены определенным способом. В то время как декларативное программирование позволяет описать поток управления как последовательность действий, «что сделать», не определяя, «как делать».

ReactiveCocoa

Императивное vs Функциональное программирование

Императивный подход к программированию подразумевает детальное описание каждого шага, который компьютер должен принять для выполнения задач. На самом деле, императивный стиль используют в нативных языках программирования (или используют при написании машинного кода). Это, кстати, является характерной чертой большинства языков программирования.

Вы определяете входные параметры для каждой функции, и то, что возвращает каждая функция. Напротив, функциональный подход решает задачи с помощью набора функций, которые необходимо выполнить. Эти два подхода программирования очень разные.

Вот основные различия языков:

1. Изменения состояния

Для чистого функционального программирования изменение состояния не существует, поскольку нет никаких побочных эффектов. Побочный эффект подразумевает изменения состояния в дополнение к возвращаемому значению из-за некоторого внешнего взаимодействия. СП (ссылочная прозрачность) подвыражения часто определяется как «отсутствие побочных эффектов» и в первую очередь относится к чистым функциям. СП не позволяет выполнению функции иметь внешний доступ к непостоянному состоянию функции, потому что каждое подвыражение — это вызов функции по определению.

Чтобы прояснить суть дела, у чистых функций есть следующие атрибуты:

  • единственный заметный вывод — возвращаемое значение
  • единственная зависимость входных параметров – аргументы
  • аргументы полностью определяются перед генерированием любого вывода

Несмотря на тот факт, что функциональный подход минимизирует побочные эффекты, их невозможно полностью избежать, поскольку они являются внутренней частью любой разработки.

Побочные эффекты широко используются для реализации состояния и ввода-вывода. С другой стороны, функции в императивном программировании не имеют ссылочной прозрачности, и это может быть единственным отличием декларативного подхода от императивного. Команды в исходном языке могут изменить состояние, что приводит к разным значениям для одного выражения языка.

Этот функциональный фреймворк для Objective-С, который является концептуально императивным языком, не включая явно чистые функции. Как насчет ReactiveСocoa? При попытке избежать изменения состояния побочные эффекты не ограничиваются.

2. Объекты первого класса

В функциональном программировании есть объекты и функции, которые являются объектами первого класса. Что это значит? Это означает, что функции могут передаваться в качестве параметра, присваиваться переменной, возвращаться из функции. Почему это удобно? Это позволяет легко управлять блоками выполнения, создавать и объединять функции различными способами без затруднений, таких как указатели функции (char *(*(**foo[][8])())[]; — развлекайтесь!).

Как насчет Objective-C? У языков, которые используют императивный подход, есть свои собственные особенности относительно выражений первого класса. Функции высшего порядка (ФВП) могут быть смоделированы путем принятия блоков в качестве параметров. У него есть блоки в качестве реализаций замыкания. В этом случае, блок является замыканием, и функция высшего порядка может быть создана из определенного набора блоков.

Однако процесс манипулирования с ФВП в функциональных языках является более быстрым способом и требует меньше строк кода.

3. Управление основным потоком

Циклы в императивном стиле представлены как вызовы функции рекурсии в функциональном программировании. Итерация в функциональных языках обычно выполняется через рекурсию. Почему? Наверное, ради сложности. Для Objective-C разработчиков, циклы кажутся гораздо более благоприятными для программиста. Рекурсии могут вызвать трудности, например, чрезмерное потребление оперативной памяти.

Мы можем написать функцию без использования циклов или рекурсий. Но! Эти функции полезны для реорганизации исходного кода. Для каждого из бесконечно возможных специализированных действий, которые могут быть применены к каждому элементу коллекции, функциональное программирование использует многоразовые итеративные функции, такие как “map”, “fold”, “filter”. (читайте дальше, у нас есть больше информации об этом!) Они уменьшают дублирование и не требуют записи отдельной функции.

4. Порядок выполнения

Декларативные выражения показывают только логические связи аргументов функции подвыражения и отношения постоянного состояния. Так при отсутствии сайд эффектов, переход состояния каждого вызова функции происходит независимо от других.

Поэтому порядок выполнения имеет значение и неявно определяется организацией исходного кода. Функциональный порядок выполнения императивных выражений зависит от непостоянного состояния. В этом вопросе, мы можем указать на различие между стратегиями оценки обоих подходов.

В таком случае, оценка выражения откладывается до тех пор, пока его значение не будет необходимо, там самым мы избегаем повторных оценок. Отложенные вычисления или вычисления, вызванные по необходимости, на языках функционального программирования являются стратегиями. Порядок операций становится неопределенным. Иными словами, выражения оцениваются только при оценке зависимого выражения.

Это подразумевает диктовку порядка выполнения, Таким образом, легче определить когда подвыражения (включая функции) будут просчитаны, потому что подвыражения могут иметь побочные эффекты, которые влияют на просчет других выражений. Напротив, энергичное вычисление в императивном языке означает, что выражение будет оценено, как только оно будет привязано к переменной.

5. Количество кода

Это важно, функциональный подход требует написания меньшего количества кода, чем императивный. Это означает меньше сбоев, меньше кода для тестирования, и более производительный цикл разработки. Так как система постоянно развивается и растет, это важно.

Главные компоненты ReactiveCocoa

Функциональное программирование работает с понятиями, известными как future (представление переменной, доступное только для чтения) и promise (представление переменной, доступное только для чтения future). Что же хорошего в них? В императивном программировании Вы должны работать с уже существующими значениями, что приводит к необходимости синхронизации асинхронного кода и других трудностей. Но понятия futures и promises позволяют работать со значениями, которые еще не созданы (асинхронный код записан синхронным способом).

Signal

Сигнал

Future и promise представлены как сигналы в реактивном программировании. RACSignal — основной компонент ReactiveCocoa. Он дает возможность представить поток событий которые будут представлены в будущем. Вы подписываетесь на сигнал и получаете доступ к событиям, которые произойдут со временем. Сигнал — это push-driven поток и может представлять собой нажатие кнопки, асинхронные сетевые операции, таймеры, другие события UI или что-либо еще, что изменяется в течение долгого времени. Они могут связать результаты асинхронных операций и эффективно объединить многократные источники события.

Последовательность

Другим типом потока является последовательность. В отличие от сигнала, последовательность — это pull-driven поток. Это своего рода коллекция, которая имеет аналогичное назначение, что и NSArray. RACSequence позволяет определенным операциям выполняться, когда Вы в них нуждаетесь, а не последовательно, как с коллекцией NSArray. Значения в последовательности оцениваются только когда это указано по умолчанию. Использование только части последовательности потенциально улучшает производительность. RACSequence позволяет коллекциям Cocoa обрабатываться универсальным и декларативным способом. RAC добавляет метод -rac_sequence к большинству классов коллекции Cocoa, чтобы их можно было использовать в качестве RACSequences.

Команда

В ответ на определенные действия создается RACCcommand и подписывается на сигнал. Это применяется, прежде всего, к UI взаимодействиям. Категории UIKit, предусмотренных ReactiveCocoa для большинства средств управления UIKit, дают нам корректный способ обработки событий UI. Давайте представим, что мы должны зарегистрировать пользователя в ответ на нажатие кнопки. В этом случае команда может представлять сетевой запрос. Когда начинается выполнение процесса кнопка меняет свое состояние на «неактивно» и наоборот. Что еще? Мы можем передать активный сигнал в команде (Достижимость — хороший пример). Поэтому, если сервер будет недоступен (который является нашим “включенным сигналом”), то команда будет недоступна, и каждая команда сопоставленного элемента управления будет отражать это состояние.

Примеры основных операций

Вот некоторые схемы о том, как работают основные операции с RACSignals:

Слияние/Merge

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

Merge

У потоков результата есть оба потока событий, объединенных вместе. Таким образом, "+ merge" является полезным, когда вы не заботитесь о конкретном источнике событий, но хотели бы обработать их в одном месте. В нашем примере stateLabel.text использует 3 различных сигнала: выполнение, завершение, ошибки.

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) ] 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;

В результате поток содержит последние значения передаваемых потоков. Если один из потоков не имеет значение, то результат окажется пустым.

CombineLatest

Когда мы можем использовать его? Давайте возьмем наш предыдущий пример и добавим больше логики к нему. Полезно включить кнопку входа в систему только в случае, когда пользователь ввел правильный email и пароль, верно? Мы можем объявить это правило следующим образом:

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

* Теперь давайте немного изменим нашу команду входа в систему и подключим ее к фактическому 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;

Вы создаете новые потоки для каждого значения в исходном потоке, используя данную функцию (f). Поток результата возвращает новые сигналы на основе значений, сформированных в исходных потоках. Поэтому он может быть асинхронным.

FlattenMap

Давайте представим, что Ваш запрос авторизаций в систему состоит из двух отдельных частей: получить данные от Facebook (идентификатор, и т.д.) и передать их на Backend. Одно из требований должно быть в состоянии отменить вход в систему. Поэтому клиентский код должен обработать состояние процесса входа в систему, чтобы иметь возможность отменить его. Это дает много шаблонного кода, особенно если Вы можете войти в систему из нескольких мест.

Это могло бы быть реализацией входа в систему: Как ReactiveCocoa помогает Вам?

- (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] — сигнал, который приводит к открытию FBSession. Если необходимо, то это может привести к входу в Facebook.

— [FBSession rac_fetchProfile] — сигнал, который извлекает данные профиля через сессию, которая передается как self.

Преимущество данного подхода заключается в том, что для пользователя весь поток нечеткий, представлен единственным сигналом, который можно отменить на любой «стадии», будь то вход Facebook или вызов Backend.

Фильтр/Filter

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

В результате поток содержит значения потока “а”, отфильтрованное согласно заданной функции.

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;

В отличие от FlattenMap, Map выполняется в синхронном режиме. Значение свойства “а” проходит через заданную функцию f (x + 1) и возвращает отображенное исходное значение.

Map

Допустим, нужно ввести на экран заголовок модели, применяя к ней некоторые атрибуты. Map вступает в игру, когда “Применение некоторых атрибутов” описано как отдельная функция:

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];
}];

Как это работаем: объеденяет self.titleLabel.text с изменениями model.title, применив пользовательские атрибуты к нему.

Zip

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

События потока результатов создаются, когда каждый из потоков сформировал равное количество событий. Он содержит значения, по одному от каждого из 3 объединенных потоков.

Zip

Для некоторых практических примеров, zip можно описать как dispatch_group_notify Например, у Вас есть 3 отдельных сигнала и необходимо объединить их ответы в единственной точке:

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;

С помощью таймера, установленного на определенный промежуток времени, первое значения потока “а” передается к потоку результата только по окончанию таймера. В случае, если новое значение производится в течение заданного временного интервала, он удерживает первое значение, не давая ему передаваться в поток результата. Вместо этого, в потоке результата появляется второе значение.

Throttle

Удивительный случай: нам нужно выполнить поиск по запросу, когда пользователь изменяет searchField. Стандартная задача, да? Впрочем, она не очень эффективна для построения и отправки сетевого запроса при каждом изменении текста, поскольку textField может генерировать много таких событий в секунду, и вы придете к неэффективному использованию сети.
Выход здесь заключается в том, чтобы добавить задержку, после которой мы на самом деле выполним сетевой запрос. Обычно это достигается добавлением NSTimer. С ReactiveCocoa это гораздо проще!

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

*Важным замечанием здесь является то, что все «предыдущие» textField изменяются до того, как «последние» будут удалены.

Задержки/Delay

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

Значение, полученное в потоке “а” задерживается и передается в поток результата через определенный интервал времени.

Delay

Как аналог -[RACSignal throttle:], delay только задержит отправку “следующих” и“завершенных” событий.

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

Что нам нравится в Reactive Cocoa

  • Знакомит Cocoa Bindings с iOS
  • Возможность создавать операции по будущим данным. Вот немного теории о futures & promises от Scala.
  • Возможность представлять асинхронные операции синхронным способом. Reactive Cocoa упрощает асинхронное программное обеспечение, например сетевой код.
  • Удобная декомпозиция. Код, который связанный с пользовательскими событиями и изменениями состояния приложения, может стать очень сложным и запутанным. Reactive Cocoa делает модели зависимых операций особенно простыми. Когда мы представляем операции в виде объединенных потоков (например, обработка сетевых запросов, пользовательские события, и т.д.), мы можем достигнуть высокой модульности и свободной связи, что приводит к более частому использованию кода повторно.
  • Поведения и отношения между свойствами определены как декларативные.
  • Решает проблемы с синхронизацией — если Вы объединяете несколько сигналов, тогда есть одно единое место для обработки всех результатов (будь то следующее значение, сигнал завершения или ошибки)

С помощью фреймворка RAC Вы можете создавать и преобразовывать последовательности значений в лучший, более высокого уровня, способ. RAC позволяет проще управлять всем тем, что ожидает завершения асинхронной операции: отклика сети, изменения зависимого значения и последующей реакции. На первый взгляд с ним трудно иметь дело, но ReactiveCocoa заразительный!


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Оптический приемопередатчик FTDI-POF

Мне предложили поучаствовать в одном проекте связанном с передачей данных по оптическому волокну и я с интересом взялся за эту работу. Привет, Хабр, я студент по специальности лазеры в инфо-коммуникационных системах и я впервые работал с оптоволокном. Это интернет, подключение ...

«Kubernetes во все поля!» – интервью с программным комитетом конференции DevOops

Раньше докер был крутым, молодежным, вещью в себе. А потом как-то докер перестал быть интересен: он просто есть, он у всех и во всем. На нем все микросервисы, Kubernetes, девопс — всё, что угодно. Вместе с тем, люди тащат контейнеры ...