Какво е реактивно програмиране. Разбиране на рамката ReactiveX и писане в реактивен стил за Android

Принципите на реактивното програмиране не са нови и могат да бъдат проследени до основополагащата работа на Джим Грей и Пат Хеланд върху тандемната система през 70-те и 80-те години.

Тези хора бяха много по-напред от времето си. Само през последните 5-10 години технологичната индустрия беше принудена да преосмисли съществуващите " най-добри практики» за развитие на корпоративната система. Тя се е научила да прилага знания за реактивните принципи на днешния свят на многоядрени и облачни изчисления.

Основата на реактивната система е предаването на съобщения, което създава времева граница между компонентите, позволява им да бъдат отделени във времето с помощта на паралелизъм и пространство, което разпределя натоварването и осигурява мобилност. Това отделяне е изискване за пълна изолация между компонентите и формира основата както за стабилността, така и за устойчивостта на системите.

Основи на реактивното програмиране

Това програмиране се фокусира върху потока от информация и разпространението на промените в данните. Когато се използват езици за програмиране, е лесно да се направи разлика между статични и динамични нишки, докато базов моделавтоматично ще разпространи промените във всички потоци от данни. С прости думи, при Rx програмиране, излъчено от един компонент и основната структура, предоставена от Rx библиотеките, ще разпространи тези промени към друг компонент, регистриран да получава тези промени. Реактивното програмиране Rx се състои от три ключови точки.

Основните функции на компонентите:

  1. Наблюдаваните не са нищо друго освен потоци от данни. Наблюдаваното пакетира данни, които могат да се предават от един поток към друг. Те основно излъчват данни периодично или само веднъж в жизнения си цикъл въз основа на конфигурации. Има различни оператори, които могат да помогнат на наблюдателя да изпрати някои специфични данни въз основа на определени събития.
  2. Наблюдателите консумират потока от данни, излъчван от наблюдаемото. Наблюдателите се абонират, използвайки метода за реактивно програмиране subscribeOn(), за да получават данни, предавани към наблюдаеми. Всеки път, когато наблюдаем подава данни, всички регистрирани наблюдатели получават данните в обратното извикване onNext(). Тук те могат да извършват различни операции, като анализиране на JSON отговор или актуализиране на потребителския интерфейс. Ако има грешка, причинена от наблюдаемото, наблюдателят ще го получи в onError().
  3. Schedulers (schedule) е компонент в Rx, който казва на наблюдателите и наблюдателите на коя нишка трябва да се изпълняват. Можете да използвате метода observOn(), за да кажете на наблюдателите коя нишка трябва да наблюдават. Алтернативно, schedOn() може да се използва, за да се каже на наблюдаемата в коя нишка трябва да се изпълняват.

При реактивно програмиране, използвайки основни нишки по подразбиране на RxJava, като Schedulers.newThread(), ще се създаде нов фон. Schedulers.io() ще изпълни кода на I/O нишката.

Основните предимства на Rx са повишено използване на изчислителни ресурси на многоядрен и многопроцесорен хардуер, подобрена производителност чрез намаляване на точките и подобрена производителност чрез намаляване на точките за сериализация, съгласно Закона на Амдал и Закона за универсалната мащабируемост на Гюнтер.

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

Когато се появи Rx, се създава процесът на създаване на компоненти и съставянето на работни потоци. За да се възползвате напълно от асинхронното изпълнение, разрешаването на обратното налягане е от решаващо значение, за да се избегне прекомерна употреба или по-скоро неограничена консумация на ресурси. За да се осигури стабилно състояние по отношение на потока от данни, базираното на натоварването обратно налягане изпраща търсене нагоре по веригата и получава съобщения.

И така, основните предимства на системата са:

  1. Повишена производителност - благодарение на способността за бързо и последователно обработване на огромни количества данни.
  2. Подобрен UX - поради факта, че приложението е по-отзивчиво към потребителя.
  3. Опростени модификации и актуализации - благодарение на по-четливия и по-лесен за прогнозиране код.

Но въпреки че реактивното програмиране е много полезно нещо при изграждането на модерен софтуер, за да мислите за системата на по-високо ниво, трябва да използвате друг инструмент - реактивна архитектура за процеса на проектиране на реактивни системи. Освен това е важно да запомните, че има много парадигми за програмиране и Rx е само една от тях, като всеки инструмент, той не е предназначен за всички случаи на употреба.

Стабилност на реактивната система

Устойчивостта е отзивчивост към отказ и е присъщо функционално свойство на системата. Нуждае се от развитие, а не само от добавяне със задна дата към системата. Устойчивостта на реактивното програмиране с javascript надхвърля толерантността към грешки и не се дължи на влошаване, но в случай на повреда може напълно да се коригира.

Това изисква изолиране на компоненти и ограничаване на грешките, за да се избегнат откази, разпространяващи се към съседни компоненти, което може да доведе до каскадни сценарии за катастрофални откази. По този начин ключът към изграждането на устойчиви системи е, че те могат да бъдат характеризирани като съобщения, изпратени до други компоненти, които действат като надзорници и се управляват от защитен контекст извън повредения компонент.

Тук, тъй като се управляват от съобщения, тези съоръжения се отдалечават от тясно свързани, крехки, дълбоко вложени синхронни вериги за повиквания, които най-често се игнорират. Идеята е да се отдели обработката на повреди от веригата на повикванията, например чрез освобождаване на клиента от отговорността за обработка на грешки на сървъра.

Тъй като повечето системи по своята същност са сложни, един от най-важните аспекти е да се гарантира, че системната архитектура гарантира, че има минимално влошаване на производителността както при разработването, така и при поддръжката на компоненти, като в същото време се намалява произволната сложност до минимум. Това е важно, тъй като по време на кръговат на животасистемите, ако не са проектирани правилно, ще стават все по-трудни за поддръжка и ще изискват все повече време и усилия за разбиране, за да се изолират и коригират проблемите.

Реактивните системи представляват най-продуктивната системна архитектура в контекста на многоядрени, облачни и мобилни архитектури:

  1. Изолирането на повреди предлага прегради между компонентите, предотвратявайки каскадни повреди и ограничавайки обхвата и сериозността на повредите.
  2. Йерархиите на супервайзорите предлагат множество нива на защита, съчетани с възможности за самовъзстановяване, елиминирайки много временни повреди на каквито и да е оперативни разходи за разследване.
  3. Пропускането на съобщения и прозрачността на местоположението ви позволяват да деактивирате и замените компоненти, без да се засяга изживяването на крайния потребител. Това намалява разходите за повреди, тяхната относителна спешност и ресурсите, необходими за диагностицирането и отстраняването им.
  4. Репликацията намалява риска от загуба на данни и намалява влиянието на повредата върху наличността на извличане и съхранение на информация.
  5. Еластичността позволява ресурсите да бъдат запазени при колебания в използването, минимизирайки оперативните разходи при ниско натоварване и риска от повреда или спешна инвестиция в скалируемост при увеличаване на натоварването.

Уеб приложенията могат да се възползват значително от стила на разработка Rx, който ви позволява да композирате работни потоци заявка-отговор, които включват разклоняване в извиквания на услуги, асинхронно извличане на ресурси и съставяне на отговор и последващо сортиране за клиента. Съвсем наскоро събитията от типа push-to-server и уеб сокетите стават все по-често срещани в практиката и извършването на това в мащаб изисква ефективен начин за съхраняване на много отворени връзки и където IO не блокира.

Има инструменти за това, като потоци и фючърси, които правят неблокиращите и асинхронните преобразувания лесни и ги изпращат до клиентите. Реактивно програмиране със слой за достъп до данни - актуализира ги и ги запитва ефективен ресурс, за предпочитане с помощта на основи SQL данниили NoSQL с асинхронни драйвери.

Уеб приложенията също се възползват от разработването на реактивна система за неща като разпределено кеширане, съгласуваност на данните и известия с множество възли. Традиционните уеб приложения обикновено използват стоящи възли. Но веднага щом програмистите започнат да използват Server-Sent-Events (SSE) и WebSockets, тези възли стават оперативни, защото като минимум поддържат състоянието на клиентската връзка и съответно им се изпращат насочени известия. Това изисква разработването на реактивна система, тъй като това е област, в която адресирането на получателите чрез съобщения е важно.

Същността на Java реактивното програмиране

Не е задължително да се използва Rx в реактивни системи. Тъй като Rx програмирането и реактивните системи не са едно и също нещо. Въпреки че често се използват взаимозаменяемо, те не са точни синоними и отразяват различни неща. Системите представляват следващото ниво на "реактивност". Това ниво предполага специфични дизайнерски и архитектурни решения, които ви позволяват да създавате стабилни и гъвкави приложения.

Въпреки това, една много добра идея - комбинация от методи - носи още повече ползи за приложенията, тъй като ги прави още по-свързани, позволява по-ефективно използване на ресурсите и осигурява по-ниска латентност. Когато става въпрос за огромни количества данни или многозадачност, често е необходима асинхронна обработка, за да поддържат системите бързи и отзивчиви.

В Java, наследството на старото обектно-ориентирано програмиране, асинхронността може да стане наистина сложна и да направи кода труден за разбиране и поддръжка. По този начин Rx е особено полезен за тази "чисто" обектно-ориентирана среда, защото улеснява работата с асинхронни нишки.

С последните си издания, започвайки с Java 8, самата Java направи някои опити за внедряване на естествена реактивност, но тези опити не са много популярни сред разработчиците днес. Има обаче някои живи и редовно актуализирани реализации на трети страни за реактивно програмиране на Java, които могат да спасят положението и затова са особено ценени от разработчиците на Java.

В едно типично приложение е обичайно многократно да се изпълняват някои реактивни програмни операции с помощта на RxJava, така че трябва да сравните скоростта, използването на процесора и паметта със същите операции, които са били реализирани както с Kotlin съпрограми, така и с RxJava. Това е първоначален тест за ефективност.

Всеки път, когато се прилага нов инструмент, който ще се използва широко в целия код, е важно да се разбере дали това ще повлияе на цялостната производителност на приложението, преди да се реши колко подходящо е да се използва. Практиката на използване дава краткия отговор: в повечето случаи потребителите трябва да обмислят замяната на RxJava със съпрограми Kotlin, особено в Android.

Реактивното програмиране с помощта на RxJava все още може да се използва в ограничен брой случаи и в тези случаи както RxJava, така и съпрограммите могат да се смесват.

Прости причини:

  1. Те осигуряват много повече гъвкавост от обикновения Rx.
  2. Осигурява богат набор от оператори на колекции, които ще изглеждат по същия начин като операторите на RxJava.
  3. Реактивното програмиране на Kotlin може да взаимодейства, когато е необходимо, с помощта на rxjava.
  4. Те са много леки и ефективни предвид по-високото използване на процесора за събиране на боклук от всички обекти, създадени от RxJava.

Реактивни разширения

Reactive Extensions (ReactiveX или RX) е библиотека, която следва принципите на Rx, т.е. композиране на асинхронни програми и програми, базирани на събития, използвайки видима последователност. Тези библиотеки предоставят много интерфейси и методи, които помагат на разработчиците да пишат чист и прост код.

Реактивните разширения се предлагат на няколко езика. Програмистите се интересуват особено от RxJava и RxAndroid, тъй като android е най-фокусираната област.

Реактивното програмиране с помощта на RxJava е реализация на Java Reactive Extension от Netflix. По принцип това е библиотека, която съставя асинхронни събития, следвайки модела на наблюдателя.

Възможно е да се създаде асинхронен трафик, да се трансформира и консумира от наблюдателя в различни потоци от данни. Библиотеката предлага широк набор от невероятни оператори като map, join и filter, които могат да бъдат приложени към потока от данни. Когато програмистът започне да използва действителни примери за код, той ще научи повече за операторите и преобразуванията.

Многопоточност в приложенията за Android

Android" class="if uuid-2938324" src="/misc/i/gallery/73564/2938324.jpg" />

Реактивното програмиране на Android (RxAndroid) е специфично за Android платформис няколко добавени класа върху RxJava. По-конкретно, програмистите за планиране са въведени в RxAndroid (AndroidSchedulers.mainThread()), което играе важна роля в поддържането на концепцията за многопоточност в приложенията за Android.

Освен всичко друго, експертите съветват да използвате само библиотеката RxJava. Дори благодарение на големия брой планировчици, използвани в програмирането за Android.

По-долу е даден списък със програмисти и тяхното резюме:

  1. Schedulers.io() – Използва се за извършване на неинтензивни операции като мрежови повиквания, четене на диск/файл, операции с бази данни и който поддържа пул от нишки.
  2. AndroidSchedulers.mainThread() - Осигурява достъп до основната нишка/UI тема. Обикновено операциите се извършват в тази нишка, като например актуализиране на потребителския интерфейс, взаимодействие с потребителя. Експертите съветват потребителите да не извършват никакви интензивни операции върху този поток, тъй като това може да доведе до срив на приложението или ANR диалог.
  3. Schedulers.newThread() - Използвайки това, ще се създава нова нишка всеки път, когато се планира задача. Обикновено се препоръчва да не се използва графикът за много дълги работни места. Нишките, създадени с newThread(), няма да бъдат използвани повторно.
  4. Schedulers.computation () - този график може да се използва за извършване на интензивни CPU операции, за обработка на огромни данни от центъра за реактивно програмиране, обработка растерни изображения. Броят на нишките, създадени с помощта на този планировчик, зависи изцяло от броя на наличните процесорни ядра.
  5. Schedulers.single() - Този планировчик ще изпълни всички задачи в следния ред, който може да се използва, когато е необходимо последователно изпълнение.
  6. Schedulers.immediate() – Този планировчик изпълнява задачата незабавно чрез синхронно блокиране на основната нишка.
  7. Schedulers.trampoline() - Изпълнява задачи в режим First In-First Out. Всички планирани задачи ще се изпълняват една след друга, ограничавайки броя на фоновите нишки до една.
  8. Schedulers.from () - позволява ви да създадете планировчик от изпълнител, ограничавайки броя на създадените нишки. Когато пулът от нишки е зает, задачите ще бъдат поставени на опашка.

Сега, след като имате добър опит в RxJava и RxAndroid, можете да преминете към някои примери на код, за да разберете по-добре концепцията. За да започнете, трябва да добавите зависимостите RxJava и RxAndroid към проектите build.gradle и да синхронизирате проекта.

Програмиране.

Наблюдателят е абониран за Observable, така че да може да започне да получава данни, като използва два метода:

  1. SubscribeOn(Schedulers.io()) - Казва на Observable да започне задача във фонова нишка.
  2. ObservOn(AndroidSchedulers.mainThread()) - Казва на наблюдателя да получава данни в нишката на Android UI.

Това е всичко, за да може програмистът да напише първата си програма за реактивно програмиране с RxJava.

Предприятията и доставчиците на междинен софтуер започнаха да използват Reactive и през 2016-2018 г. се наблюдава огромно увеличение на корпоративния интерес към приемането на тази парадигма.

Rx предлага производителност за разработчиците чрез ефективност на ресурсите на ниво компонент за вътрешна логика и трансформация на потока от данни, докато реактивните системи предлагат производителност за архитекти и DevOps чрез устойчивост и еластичност на системно ниво. Те се използват за създаване на "Cloud Native" и други широкомащабни разпределени системи. На практика книги за реактивен Java програмиранес методи, позволяващи да се комбинират принципите на проектиране на реактивни системи.

Към днешна дата съществуват редица методологии за програмиране на сложни системи в реално време. Една от тези методологии се нарича FRP (FRP). Той е абсорбирал модел на проектиране, наречен Наблюдател (Наблюдател), от една страна, а от друга, както можете да се досетите, принципите на функционалното програмиране. В тази статия ще разгледаме функционалното реактивно програмиране, като използваме примера за неговото внедряване в библиотеката Натрийза езика Haskell.

Теоретична част

Традиционно програмите са разделени на три класа:

  • Партида
  • Интерактивен
  • струя

Разликите между тези три класа програми са в типа на тяхното взаимодействие с външния свят.

Изпълнението на пакетни програми по никакъв начин не е синхронизирано с външния свят. Те могат да бъдат стартирани в произволен момент от време и завършени, когато първоначалните данни бъдат обработени и резултатът е получен. За разлика от пакетните програми, интерактивните и реактивните програми обменят данни с външния свят непрекъснато в хода на своята работа от момента на стартиране до момента на спиране.

Реактивната програма, за разлика от интерактивната, е тясно синхронизирана с външната среда: от нея се изисква да реагира на събития във външния свят с приемливо забавяне на скоростта на възникване на тези събития. В същото време е допустимо интерактивна програма да накара външната среда да изчака. Например, текстов редакторе интерактивна програма: потребителят, който е започнал процеса на коригиране на грешки в текста, трябва да изчака той да завърши, за да продължи редактирането. Въпреки това, автопилотът е реактивна програма, защото когато възникне препятствие, той трябва незабавно да коригира курса, за да извърши маневра за прелитане, тъй като реалният свят не може да бъде спрян като потребител на текстов редактор.

Тези два класа програми се появиха малко по-късно, когато компютрите започнаха да се използват за управление на машини и започнаха да се развиват потребителски интерфейси. Оттогава много методи за внедряване са се променили, всеки от които е предназначен да коригира недостатъците на предишните. Първоначално това бяха прости програми, управлявани от събития: в системата беше разпределен определен набор от събития, на които беше необходимо да се отговори, и бяха създадени манипулатори за тези събития. Манипулаторите от своя страна също могат да генерират събития, които отиват във външния свят. Този модел беше прост и решаваше набор от прости интерактивни проблеми.

Но с течение на времето интерактивните и реактивните програми станаха по-сложни и управляваното от събития програмиране се превърна в ад. Имаше нужда от по-модерни инструменти за синтез на управлявани от събития системи. Имаше два основни недостатъка в тази методология:

  • имплицитно състояние
  • Недетерминизъм

За да се коригира това, първо беше създаден шаблон наблюдател (наблюдател), който трансформира събитията в променливи във времето стойности. Наборът от тези стойности представлява изричното състояние, в което е била програмата. Така обработката на събития беше заменена от обичайната работа с данни за пакетни програми. Разработчикът може да променя наблюдаваните стойности и да абонира манипулатори за промяна на тези стойности. Стана възможно да се приложат зависими стойности, които се променят според даден алгоритъм, когато се променят стойностите, от които зависят.

Въпреки че събитията в този подход са отминали, необходимостта от тях все още остава. Едно събитие не винаги предполага промяна в стойността. Например, събитие в реално време предполага увеличение на брояча на времето със секунда, но алармено събитие всеки ден в определен час не предполага никаква стойност. Разбира се, можете също да свържете това събитие с определено значение, но това ще бъде изкуствено устройство. Например можем да въведем стойност: време_на_аларма == остатък_от_деление (текущо_време, 24*60*60). Но това няма да е точно това, което ни интересува, тъй като тази променлива е свързана със секунда и всъщност стойността се променя два пъти. За да разбере, че алармата е задействана, абонатът трябва да определи, че стойността е станала вярна, а не обратното. Стойността ще бъде вярна точно за една секунда и ако променим периода на тиктака от секунда на, да речем, 100 милисекунди, тогава истинската стойност вече няма да е секунда, а тези 100 милисекунди.

Появата на методологията на функционалното реактивно програмиране е своеобразен отговор на функционалистите на модела наблюдател. Във FRP разработените подходи бяха преосмислени: събитията (Събития) не изчезнаха, но наблюдаваните стойности също се появиха и бяха наименувани характеристики (поведения). Събитието в тази методология е дискретно генерирана стойност, а характеристиката е непрекъснато генерирана. И двете могат да бъдат свързани: характеристиките могат да генерират събития, а събитията могат да действат като източници на характеристики.

Проблемът с неопределеността на състоянието е много по-сложен. Това произтича от факта, че управляваните от събития системи са де факто асинхронни. Това води до появата на междинни състояния на системата, които могат да бъдат неприемливи по някаква причина. За да се реши този проблем, се появи така нареченото синхронно програмиране.

Практическа част

Библиотека Натрийсе появи като проект за изпълнение FRPс общ интерфейс на различни езици за програмиране. Той съдържа всички елементи на методологията: примитиви (Събитие, Поведение) и модели на тяхното използване.

Примитиви и взаимодействие с външния свят

Двата основни примитива, с които трябва да работим, са:

  • събитие а- събитие със стойност на типа а
  • Поведение а- характеристика (или променяща се стойност) на тип а

Можем да създаваме нови събития и стойности с функции новоСъбитиеи newBehavior:

newEvent::Reactive(Event a, a -> Reactive()) newBehavior::a -> Reactive(Behavior a, a -> Reactive())

Както можете да видите, и двете функции могат да бъдат извикани само в монада. Реактивен, като резултат се връща самият примитив, както и функцията, която трябва да бъде извикана, за да активира събитието или да промени стойността. Функцията за създаване на функция приема първоначалната стойност като първи аргумент.

За да свържете реалния свят с реактивна програма, има функция синхронизиране, а за свързване на програмата с външния свят има функция слушам:

Sync:: Reactive a -> IO a listen:: Event a -> (a -> IO ()) -> Reactive (IO ())

Първата функция, както подсказва името, изпълнява някакъв реактивен код синхронно, позволява ви да влезете в контекста Реактивенизвън контекста IO, а вторият се използва за добавяне на манипулатори на събития, които възникват в контекста Реактивен, изпълнявайки се в контекста IO. функция слушамвръща функция не слушамТрябва да се извика за отделяне на манипулатора.

Така се реализира своеобразен механизъм за транзакции. Когато правим нещо вътре в реактивната монада, кодът се изпълнява в рамките на същата транзакция по време на извикването на функцията. синхронизиране. Състоянието се определя само извън контекста на транзакция.

Това е основата на реактивното функционално програмиране, което е достатъчно за писане на програми. Може да е малко объркващо, че можете да слушате само събития. Точно така трябва да бъде, както ще видим по-късно, има тясна връзка между събития и характеристики.

Операции върху основни примитиви

За удобство към методологията са добавени допълнителни функции, които трансформират събития и характеристики. Нека разгледаме някои от тях:

Събитие, което никога няма да се случи -- (може да се използва като мъниче) never:: Event a -- Обединяване на две събития от един и същи тип в едно -- (удобно за дефиниране на един манипулатор за клас събития) merge:: Event a -> Събитие a -> Събитие a -- Извличане на стойности от може би събития -- (отделяне на житото от плявата) filterJust:: Събитие (Може би a) -> Събитие a -- Превръщане на събитие в характеристика с начален стойност -- (промяна на стойността при възникване на събития) задържане :: a -> Събитие a -> Реактивно (Поведение a) -- Превръща характеристика в събитие -- (генерира събития, когато стойността се промени) актуализира:: Поведение a -> Събитие a -- Превръща характеристика в събитие -- (също хвърля събитие за първоначалната стойност) стойност:: Поведение a -> Събитие a -- Когато настъпи събитие, приема стойността на характеристиката, -- прилага функцията и генерира моментна снимка на събитие:: (a -> b -> c) -> Събитие a -> Поведение b - > Събитие c -- Получава текущата стойност на sample:: Поведение a -> Реактивно a -- Намалява повтарящите се събития в едно обединяване:: (a -> a -> a) -> Събитие a -> Събитие a -- Потиска всички събития с изключение на първото еднократно:: Събитие a -> Събитие a -- Разделя събитие от разделяне на списък с множество събития:: Събитие [a] -> Събитие a

Сферични примери във вакуум

Нека се опитаме да напишем нещо:

Import FRP.Sodium main = do sync $ do -- create event (e1, triggerE1)<- newEvent -- создаём характеристику с начальным значением 0 (v1, changeV1) <- newBehavior 0 -- определяем обработчик для события listen e1 $ \_ ->putStrLn $ "e1 triggered" -- дефиниране на манипулатор за промяна на стойността на характеристиката listen (стойност v1) $ \v -> putStrLn $ "v1 стойност: " ++ show v -- Генериране на събитие без стойност triggerE1 () -- Промяна на стойността на характеристиката changeV1 13

Инсталирайте пакета Натрийкато се използва Кабали стартирайте примера в интерпретатора:

# ако искаме да работим в отделна пясъчна среда # създайте я > cabal sandbox init # install > cabal install sodium > cabal repl GHCi, версия 7.6.3: http://www.haskell.org/ghc/ :? за помощ Зареждане на пакет ghc-prim ... свързване ... готово. Зареждане на пакет integer-gmp ... свързване ... готово. Зареждане на пакетна база... свързване... готово. # зареди пример Prelude> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. # стартирайте примера *Main>

Сега нека експериментираме. Нека коментираме реда, където променяме нашата стойност (changeV1 13) и рестартирайте примера:

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main e1 задейства v1 стойност: 0

Както можете да видите, първоначалната стойност вече се показва, това е така, защото функцията стойностгенерира първото събитие с началната стойност на характеристиката. Нека заменим функцията стойностна актуализациии вижте какво се случва:

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main e1 задейства

Сега първоначалната стойност не се показва, но ако разкоментирате реда, в който сме променили стойността, променената стойност ще продължи да се показва. Нека върнем всичко както си беше и генерираме събитие e1два пъти:

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main e1 triggered e1 triggered v1 стойност: 13

Както можете да видите, събитието също се задейства два пъти. Нека се опитаме да избегнем това, защо във функцията слушамзамени аргумента e1на (веднъж e1), като по този начин създава ново събитие, което се задейства веднъж:

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main e1 задейства v1 стойност: 13

Когато едно събитие няма аргумент, за нас е важен самият факт на присъствието или отсъствието на събитието, така че функцията веднъжза комбиниране на събития е правилният избор. Въпреки това, когато има спор, това не винаги е подходящо. Нека пренапишем примера, както следва:

<- newEvent (v1, changeV1) <- newBehavior 0 listen e1 $ \v ->putStrLn $ "e1 задействано с: " ++ show v listen (стойност v1) $ \v -> putStrLn $ "v1 стойност: " ++ show v triggerE1 "a" triggerE1 "b" triggerE1 "c" changeV1 13

Получаваме, както се очаква, всички събития със стойности в същия ред, в който са били генерирани:

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main e1 задействано с: "a" e1 задействано с: "b" e1 задействано с: "c" v1 стойност: 13

Ако използваме функцията веднъжс e1, тогава получаваме само първото събитие, така че нека се опитаме да използваме функцията сливат се, за което заместваме аргумента e1в слушамаргумент (сливане (\_ a -> a) e1):

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main e1, задействан с: "c" v1 стойност: 13

И наистина, получихме само последното събитие.

Още примери

Нека да разгледаме по-сложни примери:

Импортирайте FRP.Sodium main = направете синхронизиране $do(e1,triggerE1)<- newEvent -- создаём характеристику, изменяемую событием e1 v1 <- hold 0 e1 listen e1 $ \v ->putStrLn $ "e1 задействано с: " ++ show v listen (стойност v1) $ \v -> putStrLn $ "v1 стойност е: " ++ show v -- излъчване на събития triggerE1 1 triggerE1 2 triggerE1 3

Ето резултата:

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main e1 задействано с: 1 e1 задействано с: 2 e1 задействано с: 3 v1 стойността е: 3

Стойността на характеристиката се показва само веднъж, въпреки че са генерирани няколко събития. Това е особеността на синхронното програмиране: характеристиките се синхронизират с повикването синхронизиране. За да демонстрираме това, нека леко модифицираме нашия пример:

<- sync $ do (e1, triggerE1) <- newEvent v1 <- hold 0 e1 listen e1 $ \v ->putStrLn $ "e1 задействано с: " ++ show v listen (стойност v1) $ \v -> putStrLn $ "v1 стойност е: " ++ show v return triggerE1 sync $ triggerE1 1 sync $ triggerE1 2 sync $ triggerE1 3

Току-що преместихме тригера на събитието във външния свят и го извикахме в различни фази на синхронизация:

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main v1 стойност е: 0 e1 задействана с: 1 v1 стойност е: 1 e1 задействана с: 2 v1 стойност е: 2 e1 задействана с: 3 v1 стойност е: 3

Сега всяко събитие показва нова стойност.

Други операции върху примитиви

Помислете за следната група полезни функции:

Обединява събития с помощта на mergeWith:: (a -> a -> a) -> Събитие a -> Събитие a -> Събитие a -- Филтрира събития, оставяйки само тези -- за които функцията връща true filterE:: (a -> Bool ) -> Събитие a -> Събитие a -- Позволява "изключване" на събития -- когато характеристиката е False gate:: Event a -> Behavior Bool -> Event a -- Организира конвертор на събития -- с вътрешно състояние collectE: : (a -> s -> (b, s)) -> s -> Събитие a -> Реактивно (Събитие b) -- Организира преобразувател на функции -- с вътрешно събиране на състояние:: (a -> s -> ( b , s)) -> s -> Поведение a -> Реактивно (Поведение b) -- Създава характеристика в резултат на натрупване на събития accum:: a -> Събитие (a -> a) -> Реактивно (Поведение a )

Разбира се, това не са всички функции, предоставени от библиотеката. Има и много по-екзотични неща, които са извън обхвата на тази статия.

Примери

Нека опитаме тези функции в действие. Нека започнем с последното, организирайте нещо като калкулатор. Нека имаме някаква стойност, към която можем да приложим аритметични функции и да получим резултата:

Импортирайте FRP.Sodium main = направете triggerE1<- sync $ do (e1, triggerE1) <- newEvent -- пусть начальное значение будет равно 1 v1 <- accum (1:: Int) e1 listen (value v1) $ \v ->putStrLn $ "v1 стойност е: " ++ show v return triggerE1 -- ​​​​добавете 1 sync $ triggerE1 (+ 1) -- умножете по 2 sync $ triggerE1 (* 2) -- извадете 3 sync $ triggerE1 (+ (- 3) ) -- добавете 5 sync $ triggerE1 (+ 5) -- повдигнете на степен 3 sync $ triggerE1 (^ 3)

Да бягаме:

*Main> :l Example.hs Компилиране на Main (Example.hs, интерпретиран) Добре, заредени модули: Main. *Main> main v1 стойност е: 1 v1 стойност е: 2 v1 стойност е: 4 v1 стойност е: 1 v1 стойност е: 6 v1 стойност е: 216

Може да изглежда, че наборът от функции е доста оскъден, но всъщност не е така. В крайна сметка имаме работа с Haskell, апликативните функтори и монадите не са изчезнали. Можем да извършваме всякакви операции върху характеристики и събития, които сме свикнали да извършваме върху чисти стойности. В резултат на това се получават нови характеристики и събития. За характеристики са внедрени клас функтор и апликативен функтор, за събития, по очевидни причини, само функтор.

Например:

<$>), (<*>)) import FRP.Sodium main = do(setA, setB)<- sync $ do (a, setA) <- newBehavior 0 (b, setB) <- newBehavior 0 -- Новая характеристика a + b let a_add_b = (+) <$>а<*>b -- Нова функция a * b let a_mul_b = (*)<$>а<*>b слушай (стойност a) $ \v -> putStrLn $ "a = " ++ показвай v слушай (стойност b) $ \v -> putStrLn $ "b = " ++ показвай v слушай (стойност a_add_b) $ \v - > putStrLn $ "a + b = " ++ show v listen (стойност a_mul_b) $ \v -> putStrLn $ "a * b = " ++ show v return (setA, setB) sync $ do setA 2 setB 3 sync $ setA 3 синхронизиране $setB 7

Ето какво ще бъде изведено в интерпретатора:

λ> основен a = 0 b = 0 a + b = 0 a * b = 0 a = 2 b = 3 a + b = 5 a * b = 6 a = 3 a + b = 6 a * b = 9 b = 7 a + b = 10 a * b = 21

Сега нека видим как нещо подобно работи със събития:

ImportControl.Applicative((<$>)) импортирайте FRP.Sodium main = do sigA<- sync $ do (a, sigA) <- newEvent let a_mul_2 = (* 2) <$>нека a_pow_2 = (^2)<$>a слушане a $ \v -> putStrLn $ "a = " ++ show v слушане a_mul_2 $ \v -> putStrLn $ "a * 2 = " ++ show v слушане a_pow_2 $ \v -> putStrLn $ "a ^ 2 = "++ show v return sigA sync $do sigA 2 sync $sigA 3 sync $sigA 7

Ето какво ще бъде изведено:

λ> основно a = 2 a * 2 = 4 a ^ 2 = 4 a = 3 a * 2 = 6 a ^ 2 = 9 a = 7 a * 2 = 14 a ^ 2 = 49

Документацията съдържа списък с екземпляри на класове, които са внедрени за Поведениеи събитие, но нищо не ви пречи да внедрите екземпляри на липсващите класове.

Недостатъкът на реактивността

Функционалното реактивно програмиране със сигурност опростява разработването на сложни системи в реално време, но има много аспекти, които трябва да се имат предвид при използването на този подход. Затова тук ще разгледаме най-често срещаните проблеми.

Неедновременност

Синхронното програмиране предполага определен механизъм за транзакции, който осигурява последователността на последователните състояния на системата и следователно липсата на междинни неочаквани състояния. AT Натрийобажданията отговарят за транзакциите синхронизиране. Въпреки че състоянието в транзакцията не е дефинирано, не може да се счита, че всичко в нея се случва едновременно. Стойностите се променят в определен ред, което се отразява на резултата. Например споделянето на събития и характеристики може да има неочаквани ефекти. Помислете за пример:

ImportControl.Applicative((<$>)) import FRP.Sodium main = do setVal<- sync $ do (val, setVal) <- newBehavior 0 -- создаём булеву характеристику val >2 нека gt2 = (> 2)<$>val -- създайте събитие със стойности, които са > 2 let evt = gate (стойност val) gt2 listen (стойност val) $ \v -> putStrLn $ "val = " ++ show v listen (стойност gt2) $ \ v -> putStrLn $ "val > 2 ? " ++ show v listen evt $ \v -> putStrLn $ "val > 2: " ++ show v return setVal sync $ setVal 1 sync $ setVal 2 sync $ setVal 3 sync $ setVal 4 синхронизиране $setVal 0

Можете да очаквате резултат като този:

Val = 0 val > 2? False val = 1 val > 2? False val = 2 val > 2? False val = 3 val > 2? Истинска стойност > 2: 3 стойност = 4 стойност > 2? Истинска стойност > 2: 4 стойност = 0 стойност > 2? Невярно

Въпреки това, всъщност линията стойност > 2: 3ще отсъства, а в края ще има линия стойност > 2: 0. Това е така, защото събитието за промяна на стойността (стойност стойност)се генерира преди да се изчисли зависимата характеристика gt2, и така събитието евтне възниква за зададената стойност 3. В края, когато отново зададем 0, изчисляването на характеристиката gt2късно е.

Като цяло ефектите са същите като при аналоговата и цифровата електроника: сигнални състезания, за борба с които те използват различни техники. По-специално синхронизацията. Ето какво ще направим, за да накараме този код да работи правилно:

ImportControl.Applicative((<$>)) import FRP.Sodium main = do(sigClk, setVal)<- sync $ do -- Мы ввели новое событие clk -- сигнал синхронизации -- прям как в цифровой электронике (clk, sigClk) <- newEvent (val, setVal) <- newBehavior 0 -- Также вы создали альтернативную функцию -- получения значения по сигналу синхронизации -- и заменили все вызовы value на value" let value" = snapshot (\_ v ->v) clk нека gt2 = (> 2)<$>val let evt = gate (value" val) gt2 listen (value" val) $ \v -> putStrLn $ "val = " ++ show v listen (value" gt2) $ \v -> putStrLn $ "val > 2 ? " ++ show v listen evt $ \v -> putStrLn $ "val > 2: " ++ show v return (sigClk, setVal) -- Въведена е нова функция sync" -- която извиква синхронизиращ сигнал -- в края на всяка транзакция - - И заместени с него всички извиквания sync let sync" a = sync $ a >> sigClk () sync" $ setVal 1 sync" $ setVal 2 sync" $ setVal 3 sync" $ setVal 4 sync" $ setVal 0

Сега нашият резултат е според очакванията:

λ> main val = 0 val > 2? False val = 1 val > 2? False val = 2 val > 2? False val = 3 val > 2? Истинска стойност > 2: 3 стойност = 4 стойност > 2? Истинска стойност > 2: 4 стойност = 0 стойност > 2? Невярно

мързел

Проблеми от различен вид са свързани с мързеливия характер на изчисленията в Haskell. Това води до факта, че при тестване на кода в интерпретатора, някои резултати в края може просто да липсват. Това, което може да се предложи в този случай, е да се направи безполезна стъпка за синхронизиране в края, като sync$return().

Заключение

Достатъчно за сега, мисля. В момента един от авторите на библиотеката Натрийпише книга за FRP. Надяваме се, че това по някакъв начин ще запълни празнините в тази област на програмиране и ще послужи за популяризиране на прогресивни подходи в нашите закостенели умове.

С течение на времето езиците за програмиране непрекъснато се променят и развиват поради появата на нови технологии, модерни изисквания или просто желание за опресняване на стила на писане на код. Реактивното програмиране може да се реализира с помощта на различни рамки като Reactive Cocoa. Той променя обхвата на императивния стил на езика Objective-C и този подход към програмирането има какво да предложи на стандартната парадигма. Това, разбира се, привлича вниманието на iOS разработчиците.

ReactiveCocoa внася декларативен стил в Objective-C. Какво имаме предвид с това? Традиционният императивен стил, използван от езици като C, C++, Objective-C и Java и т.н., може да бъде описан по следния начин: Вие пишете директиви за компютърна програмакоето трябва да се направи по определен начин. С други думи, вие казвате "как да направя" нещо. Докато декларативното програмиране ви позволява да опишете потока на управление като последователност от действия, "какво да правя", без да дефинирате "как да го направя".

Императивно срещу функционално програмиране

Императивният подход към програмирането е подробно описание на всяка стъпка, която компютърът трябва да предприеме, за да изпълни задачите. Всъщност императивният стил се използва в родните езици за програмиране (или се използва при писане на машинен код). Това, между другото, е характерна черта на повечето езици за програмиране.

Напротив, функционалният подход решава проблеми с набор от функции, които трябва да бъдат изпълнени. Вие определяте входните параметри за всяка функция и какво връща всяка функция. Тези два програмни подхода са много различни.

Ето основните езикови разлики:

1. Промени в състоянието

За чисто функционално програмиране няма промяна на състоянието, защото няма странични ефекти. Страничен ефект предполага промени в състоянието в допълнение към върнатата стойност поради някакво външно взаимодействие. SR (референтна прозрачност) на подизраз често се определя като „липса на странични ефекти“ и се отнася предимно за чисти функции. SP не позволява изпълнението на функцията да има външен достъп до променливото състояние на функцията, тъй като всеки подизраз е извикване на функция по дефиниция.

За да изясним нещата, чистите функции имат следните атрибути:

  • единственият забележим изход е върнатата стойност
  • единствената зависимост на входните параметри са аргументите
  • аргументите са напълно квалифицирани, преди да генерират изход

Въпреки факта, че функционалният подход минимизира страничните ефекти, те не могат да бъдат напълно избегнати, защото са неразделна част от всяко развитие.

От друга страна, функциите в императивното програмиране нямат референтна прозрачност и това може да е единствената разлика между декларативен и императивен подход. Страничните ефекти се използват широко за прилагане на състояние и I/O. Командите в изходния език могат да променят състоянието, което води до различни стойности за един и същи езиков израз.

Какво ще кажете за ReactiveCocoa? Това е функционална рамка за Objective-C, който е концептуално императивен език, който не включва изрично чисти функции. Когато се опитвате да избегнете промяна на състоянието, страничните ефекти не са ограничени.

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

Във функционалното програмиране има обекти и функции, които са първокласни обекти. Какво означава? Това означава, че функциите могат да бъдат предавани като параметър, присвоени на променлива или върнати от функция. Защо е удобно? Това улеснява управлението на блоковете за изпълнение, създаването и комбинирането на функции различни начинибез усложнения като функционални указатели (char *(*(**foo)()); - забавлявайте се!).

Езиците, които използват императивния подход, имат свои собствени странности по отношение на първокласните изрази. Какво ще кажете за Objective-C? Има блокове като изпълнения на затваряне. Функции от по-висок порядък (HFOs) могат да бъдат моделирани чрез приемане на блокове като параметри. В този случай блокът е затваряне и функция от по-висок ред може да бъде създадена от определен набор от блокове.

Въпреки това, процесът на манипулиране на FVP във функционални езици е повече бърз начини изисква по-малко редове код.

3. Управление на главния поток

Циклите в императивен стил са представени като извиквания към функцията за рекурсия във функционалното програмиране. Итерацията във функционалните езици обикновено се извършва чрез рекурсия. Защо? Вероятно в името на сложността. За разработчиците на Objective-C циклите изглеждат много по-удобни за програмисти. Рекурсиите могат да причинят затруднения, като например прекомерна консумация на RAM.

Но! Можем да напишем функция без да използваме цикли или рекурсии. За всяко от безкрайно възможните специализирани действия, които могат да бъдат приложени към всеки елемент от колекция, функционалното програмиране използва итеративни функции за многократна употреба като „ карта”, “гънка”, “". Тези функции са полезни за реорганизация програмен код. Те намаляват дублирането и не изискват писане на отделна функция. (прочетете, имаме повече информация за това!)

4. Ред за изпълнение

Декларативните изрази показват само логическите връзки на аргументите на функцията на подизраза и връзките на постоянно състояние. Така че при липса на странични ефекти, преходът на състоянието на всяко извикване на функция се случва независимо от другите.

Функционалният ред на изпълнение на императивни изрази зависи от променливото състояние. Следователно редът на изпълнение има значение и се определя имплицитно от организацията на изходния код. В този въпрос можем да посочим разликата между стратегиите за оценка на двата подхода.

Мързеливите оценки или оценките повикване по нужда са стратегии във функционалните езици за програмиране. В такъв случай оценката на израза се отлага, докато стойността му е необходима, като по този начин избягваме повторни оценки. С други думи, изразите се оценяват само когато се оценява зависимият израз. Редът на операциите става неопределен.

За разлика от това, енергичната оценка в императивен език означава, че изразът ще бъде оценен веднага щом бъде обвързан с променлива. Това включва диктуване на реда на изпълнение.По този начин е по-лесно да се определи кога подизразите (включително функциите) ще бъдат оценени, тъй като подизразите могат да имат странични ефекти, които засягат оценката на други изрази.

5. Номер на код

Това е важно, функционалният подход изисква писане на по-малко код от императивния. Това означава по-малко сривове, по-малко код за тестване и по-продуктивен цикъл на разработка. Тъй като системата непрекъснато се развива и расте, това е важно.

Основни компоненти на Reactive Cocoa

Функционалното програмиране се занимава с концепции, известни като бъдеще (представяне само за четене на променлива) и обещание (представяне само за четене на променлива бъдеще). Какво им е хубавото? При императивното програмиране трябва да работите със стойности, които вече съществуват, което води до необходимостта от синхронизиране на асинхронен код и други трудности. Но концепциите за фючърси и обещания ви позволяват да работите със стойности, които все още не са създадени (асинхронният код е написан по синхронен начин).


Сигнал

Бъдещето и обещанието се представят като сигнали в реактивното програмиране. - основният компонент на ReactiveCocoa. Той дава възможност да се представи потокът от събития, които ще бъдат представени в бъдеще. Абонирате се за сигнал и получавате достъп до събития, които ще се случат във времето. Сигналът е нишка, управлявана от натискане и може да бъде натискане на бутон, асинхронни мрежови операции, таймери, други събития от потребителския интерфейс или нещо друго, което се променя с времето. Те могат да свързват резултатите от асинхронни операции и ефективно да комбинират множество източници на събития.

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

Друг тип поток е последователност. За разлика от сигнала, последователността е поток, задвижван от изтегляне. Това е вид колекция, която има подобна цел като NSArray. RACSequence позволява извършването на определени операции, когато имате нужда от тях, вместо последователно, както при колекция NSArray. Стойностите в последователност се оценяват само когато са посочени по подразбиране. Използването само на част от последователността потенциално подобрява производителността. RACSequenceпозволява колекциите Cocoa да се обработват по общ и декларативен начин. RAC добавя метода -rac_sequence към повечето класове за събиране на Cocoa, така че да могат да се използват като RACSequences.

Екип

В отговор на определени действия, RACC командаи се абонирайте за сигнала. Това се отнася предимно за взаимодействията с потребителския интерфейс. Категории UIKitпредоставени от ReactiveCocoa за повечето контроли UIKit, ни дайте правилния начин за обработка на събития от потребителския интерфейс. Нека си представим, че трябва да регистрираме потребител в отговор на натискане на бутон. В този случай командата може да представлява мрежова заявка. Когато процесът започне, бутонът променя състоянието си на "неактивен" и обратно. Какво друго? Можем да изпратим активен сигнал в командата (Достъпно - добър пример). Следователно, ако сървърът е недостъпен (което е нашият "сигнал включен"), тогава командата ще бъде недостъпна и всяка команда от асоциирания контрол ще отразява това състояние.

Примери за основни операции

Ето някои диаграми за това как работят основните операции с RACSignals:

Обединяване

+ (RACSignal *) обединяване: (id )сигнали;


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

RACCommand *loginCommand = [ initWithSignalBlock:^RACSignal *(id input) ( // нека влезем!)]; RACSignal *executionSignal = ; RACSignal *completionSignal = filter:^BOOL(RACEvent *event) ( return event.eventType == RACEventTypeCompleted; )] map:^id(id value) ( ​​​​return @"Done"; )]; )]; RACSignal *errorSignal =; RAC(self.stateLabel, text) = ];

+ (RACSignal *)combineLatest:(id )сигнали за намаляване:(id (^)())reduceBlock;

В резултат на това потокът съдържа най-новите стойности на предаваните потоци. Ако един от потоците няма значение, резултатът ще бъде празен.


Кога можем да го използваме? Нека вземем нашия предишен пример и добавим повече логика към него. Полезно е да активирате бутона за влизане само когато потребителят е въвел правилния имейл и парола, нали? Можем да декларираме това правило така:

ACSignal *enabledSignal = reduce:^id (NSString *email, NSString *password) ( return @( && password.length > 3); )];

*Сега нека променим малко нашата команда за влизане и да я свържем с действителния loginButton

RACCommand *loginCommand = [ initWithEnabled:enabledSignal signalBlock:^RACSignal *(id input) ( // нека влезем! )]; ;

- (RACSignal *)flattenMap:(RACStream * (^)(id value))блок;

Създавате нови нишки за всяка стойност в оригиналната нишка, като използвате тази функция(е). Резултатният поток връща нови сигнали въз основа на стойностите, генерирани в оригиналните потоци. Така че може да бъде асинхронно.


Нека си представим, че вашата заявка за оторизация към системата се състои от две отделни части: вземете данни от Facebook (ID и т.н.) и ги предайте на Backend. Едно от изискванията е да можете да отмените влизането. Следователно клиентският код трябва да обработва състоянието на процеса на влизане, за да може да го отмени. Това дава много шаблонен код, особено ако можете да влезете от различни места.

Как ви помага ReactiveCocoa? Това може да е внедряване на вход:

- (RACSignal *)authorizeUsingFacebook ( return [[ flattenMap:^RACStream *(FBSession *session) ( return ; )] flattenMap:^RACStream *(NSDictionary *profile) ( return ; )]; )

легенда:

+ - сигнал, който води до отварянето FBSession. Ако е необходимо, това може да доведе до влизане в Facebook.

- - сигнал, който извлича данни от профила чрез сесия, която се предава като себе си.

Предимството на този подход е, че за потребителя целият поток е размит, представен от един сигнал, който може да бъде отменен на всеки "етап", независимо дали е Facebook входили извикване на Backend.

Филтър

- (RACSignal *) филтър: (BOOL (^) (id стойност)) блок;

В резултат на това потокът съдържа стойностите на потока "a", филтрирани според дадената функция.


RACSequence *sequence = @[@"Some", @"example", @"of", @"sequence"].rac_sequence; RACSequence *filteredSequence = ; )];

Карта

- (RACSignal *)map:(id (^)(id value))блок;

За разлика от FlattenMap, Map работи синхронно. Стойността на свойството "a" преминава през дадената функция f (x + 1) и връща нанесената първоначална стойност.


Да приемем, че искате да въведете заглавие на модел на екрана, като приложите някои атрибути към него. Картата влиза в действие, когато „Прилагане на някои атрибути“ е описано като самостоятелна функция:

RAC(self.titleLabel, текст) = initWithString:modelTitle attributes:attributes]; )];

Как работи: обединява self.titleLabel.textс промени модел.заглавиечрез прилагане на персонализирани атрибути към него.

Цип

+ (RACSignal *)zip:(id )потоци намаляват:(id (^)())reduceBlock;

Събитията от потока на резултатите се генерират, когато всеки от потоците е генерирал равен брой събития. Той съдържа стойности, по една от всеки от 3-те комбинирани потока.


За някои практически примери zip може да бъде описан като dispatch_group_notifyНапример, имате 3 отделни сигнала и искате да комбинирате техните отговори в една точка:

NSArray *сигнали = @; връщане;

- (RACSignal *)дросел:(NSTimeInterval)интервал;

С таймер, зададен за определен период от време, първата стойност на потока "a" се предава на потока с резултати само когато таймерът изтече. В случай, че се създаде нова стойност в рамките на дадения интервал от време, тя запазва първата стойност, предотвратявайки предаването й към потока от резултати. Вместо това в потока от резултати се появява втора стойност.


Изненадващ случай: трябва да търсим заявка, когато потребителят промени searchField. Стандартен проблем, нали? Въпреки това, той не е много ефективен за изграждане и изпращане на мрежова заявка всеки път, когато текстът се промени, защото textField може да генерира много такива събития в секунда и в крайна сметка вие използвате неефективно мрежата.
Решението тук е да добавим забавяне, след което всъщност правим мрежовата заявка. Това обикновено се постига чрез добавяне на NSTimer. С ReactiveCocoa е много по-лесно!

[[ throttle:0.3] subscribeNext:^(NSString *text) ( // изпълнява мрежова заявка )];

*Важна забележка тук е, че всички „предишни“ текстови полета се променят, преди „последните“ да бъдат премахнати.

Закъснения

- (RACSignal *)закъснение:(NSTimeInterval)интервал;

Стойността, получена в поток „а“, се забавя и се прехвърля към потока с резултати след определен интервал от време.


Като -, закъснението ще забави само изпращането на "следващи" и "завършени" събития.

[ subscribeNext:^(NSString *text) ( )];

Какво обичаме в Reactive Cocoa

  • Въвежда Cocoa Bindings в iOS
  • Възможност за създаване на операции върху бъдещи данни. Ето малко теория за фючърси и обещания от Scala.
  • Способността да се представят асинхронни операции по синхронен начин. Reactive Cocoa опростява асинхронното софтуер, като мрежов код.
  • Удобно разлагане. Кодът, който се занимава с потребителски събития и промени в състоянието на приложението, може да стане много сложен и объркващ. Reactive Cocoa прави зависимите модели на работа особено лесни. Когато представяме операциите като конкатенирани нишки (напр. обработка на мрежови заявки, потребителски събития и т.н.), можем да постигнем висока модулност и хлабаво свързване, което води до повече повторно използване на код.
  • Поведенията и връзките между свойствата се определят като декларативни.
  • Решава проблеми със синхронизирането - ако комбинирате множество сигнали, тогава има едно единствено място за обработка на всички резултати (независимо дали следваща стойност, завършване или сигнал за грешка)

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

Проверете информацията. Необходимо е да се провери точността на фактите и надеждността на информацията, представена в тази статия. Трябва да има обяснения на страницата за разговори ... Wikipedia

Интерактивността е понятие, което разкрива характера и степента на взаимодействие между обектите. Използва се в области: теория на информацията, компютърни науки и програмиране, телекомуникационни системи, социология, индустриален дизайн и др. В ... ... Уикипедия

Тази статия трябва да бъде уикифицирана. Моля, оформете го според правилата за форматиране на статии. Този термин има други значения, вижте Електромаш (значения) ... Уикипедия

чужди психотерапевтични техники- ДЪЛБОКИ ТЕХНИКИ Активна психотерапия (Fromm Reichmann). Анализ на битието (Бинсвангер). Анализ на съдбата (Сонди). Анализ на характера (В. Райх). Анализ I (H. Kohut, E. Erickson). Аналитична игрова терапия (М. Клайн). Семейна аналитична терапия (Рихтер).… … Голяма психологическа енциклопедия

Книги

  • Реактивно програмиране на C++. Проектиране на паралелни и асинхронни приложения, Pai Praseed, Abraham Peter. Проектиране на паралелни и асинхронни приложения с помощта на библиотеката RxCpp и модерен C++17 Поддържани инструменти за паралелно програмиране Съвместно...
  • , Нуркевич Т., Кристенсен Б.. В тези дни, когато програмите са асинхронни и бързата реакция е най-важното свойство, реактивното програмиране ще помогне да се напише по-надежден, по-добре мащабируем и по-бърз код.…
  • Реактивно програмиране с RxJava, Томас Нуркевич, Бен Кристенсен. В наши дни, когато програмите са асинхронни и бързата реакция е от съществено значение, реактивното програмиране ще ви помогне да пишете по-надежден, по-добре мащабируем и по-бърз код.…

Отивам.

Реактивното програмиране първоначално звучи като името на зараждаща се парадигма, но всъщност се отнася до метод на програмиране, който използва подход, управляван от събития, за работа с асинхронни потоци от данни. Въз основа на постоянно актуални данни, реактивните системи реагират на тях чрез изпълнение на поредица от събития.
Реактивното програмиране следва дизайнерския модел на наблюдателя, който може да се дефинира по следния начин: ако настъпи промяна на състоянието в един обект, всички останали обекти се уведомяват и съответно се актуализират. Така че вместо да проверяват събития за промени, събитията се изпращат асинхронно, така че наблюдателите да могат да ги обработват. В този пример наблюдателите са функции, които се изпълняват при изпращане на събитие. И споменатият поток от данни е действително наблюдаваният.

Почти всички езици и рамки използват този подход в своята екосистема и най-новите Java версии- не е изключение. В тази статия ще обясня как можете да приложите реактивно програмиране с помощта на последна версия JAX-RS в Java EE 8 и функционалност на Java 8.

Реактивен манифест

Реактивният манифест изброява четири основни аспекта, от които едно приложение се нуждае, за да бъде по-гъвкаво, слабо свързано и лесно за мащабиране и следователно способно да бъде реактивно. Той казва, че приложението трябва да бъде отзивчиво, гъвкаво (и следователно мащабируемо), устойчиво и управлявано от съобщения.

Основната цел е наистина отзивчиво приложение. Да приемем, че имате приложение, което има една голяма нишка, обработваща потребителски заявки, и когато работата е свършена, тази нишка изпраща отговори обратно на първоначалните заявители. Когато дадено приложение получи повече заявки, отколкото може да обработи, този поток се превръща в пречка и приложението губи предишната си отзивчивост. За да остане отзивчиво, приложението трябва да бъде мащабируемо и устойчиво. Устойчиво приложение е това, което има функция за автоматично възстановяване. Според опита на повечето разработчици само управляваната от съобщения архитектура позволява на приложението да бъде мащабируемо, стабилно и отзивчиво.

Реактивното програмиране беше въведено в Java 8 и Java EE 8. Езикът Java въведе концепции като CompletionStage и неговата реализация CompletableFuture и Java започна да използва тези функции в спецификации като Reactive Client API в JAX-RS.

JAX-RS 2.1 API за реактивен клиент

Нека да разгледаме как реактивното програмиране може да се използва в приложенията на Java EE 8. За да разберете процеса, имате нужда от основни познания за Java EE API.

Представен е JAX-RS 2.1 нов начинсъздаване на REST клиент с поддръжка за реактивно програмиране. Реализацията по подразбиране на инвокера, предлагана от JAX-RS, е синхронна, което означава, че създаден клиентще изпрати блокиращо повикване до крайната точка на сървъра. Примерна реализация е дадена в листинг 1.

Отговор на отговор = ClientBuilder.newClient() .target("http://localhost:8080/service-url") .request() .get();
Започвайки с версия 2.0, JAX-RS осигурява поддръжка за създаване на асинхронен инвокатор на клиентския API с просто извикване на метода async(), както е показано в списък 2.

Бъдеще отговор = ClientBuilder.newClient() .target("http://localhost:8080/service-url") .request() .async() .get();
Използването на асинхронен инвокатор на клиента връща Future екземпляр от тип javax.ws.rs.core.Response. Това може да доведе до запитване за отговор, извикване на future.get() или регистриране на обратно извикване, което ще бъде извикано, когато има наличен HTTP отговор. И двете реализации са подходящи за асинхронно програмиране, но нещата стават по-сложни, ако искате да групирате обратни извиквания или да добавите условни случаи към тези минимуми за асинхронно изпълнение.

JAX-RS 2.1 предоставя реактивен начин за преодоляване на тези проблеми с новия JAX-RS Reactive Client API за клиентско сглобяване. Това е толкова просто, колкото извикването на метода rx() по време на изграждането на клиента. В листинг 3 методът rx() връща реактивен инвокатор, който съществува по време на изпълнението на клиента, а клиентът връща отговор с тип CompletionStage.rx(), който позволява преход от синхронен инвокатор към асинхронен инвокатор с прост повикване.

Етап на завършване отговор = ClientBuilder.newClient() .target("http://localhost:8080/service-url") .request() .rx() .get();
Етап на завършване<Т> - нов интерфейс, въведен в Java 8. Той представлява изчисление, което може да бъде стъпка в рамките на по-голямо изчисление, както подсказва името. Това е единствената реактивност на Java 8, която го направи в JAX-RS.
След като получа екземпляра на отговора, мога да извикам AcceptAsync(), където мога да осигуря част от кода, който ще бъде изпълнен асинхронно, когато отговорът стане достъпен, както е показано в листинг 4.

Response.thenAcceptAsync(res -> ( Temperature t = res.readEntity(Temperature.class); //правете неща с t ));
Добавяне на реактивност към REST крайна точка

Реактивният подход не е ограничен до страната на клиента в JAX-RS; може да се използва и от страната на сървъра. Като пример, първо ще създам прост скрипт, където мога да поискам списък с местоположения за една дестинация. За всяка позиция ще направя отделно обаждане с данните за местоположението до друга точка, за да получа температурните стойности. Взаимодействието на дестинациите ще бъде както е показано на фигура 1.

Фигура 1. Взаимодействие между дестинации

Първо просто дефинирам модела на домейна и след това услугите за всеки модел. Листинг 5 показва как е дефиниран класът Forecast, който обвива класовете Location и Temperature.

Публичен клас Температура (private Double temperature; private String scale; // getters & setters) public class Location ( String name; public Location() () public Location(String name) ( this.name = name; ) // getters & setters ) публичен клас Прогноза ( частно местоположение местоположение; частна температура на температурата; публична прогноза (местоположение на местоположение) ( this.location = местоположение; ) публична прогноза setTemperature (крайна температура на температурата) ( this.temperature = температура; върнете това; ) // getters )
За да обвие списъка с прогнози, класът ServiceResponse е имплементиран в листинг 6.

Публичен клас ServiceResponse( частен дълго време за обработка; частен списък прогнози = нов ArrayList<>(); public void setProcessingTime(long processingTime) ( this.processingTime = processingTime; ) public ServiceResponse прогнози(Списък прогнози) ( this.forecasts = прогнози; върнете това; ) // получатели )
LocationResource, показан в листинг 7, дефинира три модела на местоположения, върнати с пътя /location.

@Path("/location") публичен клас LocationResource ( @GET @Produces(MediaType.APPLICATION_JSON) публичен отговор getLocations() ( Списък местоположения = нов ArrayList<>(); locations.add(ново местоположение("Лондон")); locations.add(ново местоположение("Истанбул")); locations.add(ново местоположение("Прага")); върне Response.ok(нов GenericEntity >(локации)()).build(); ) )
TemperatureResource, показан в листинг 8, връща произволно генерирана температурна стойност между 30 и 50 за даденото местоположение. Към изпълнението е добавено забавяне от 500 ms, за да се симулира отчитането на сензора.

@Path("/temperature") public class TemperatureResource ( @GET @Path("/(city)") @Produces(MediaType.APPLICATION_JSON) public Response getAverageTemperature(@PathParam("city") String cityName) ( Temperature temperature = new Temperature(); temperature.setTemperature((double) (new Random().nextInt(20) + 30)); temperature.setScale("Celsius"); try ( Thread.sleep(500); ) catch (InterruptedException игнорирано) (ignored.printStackTrace(); ) return Response.ok(temperature).build(); ) )
Първо, ще покажа имплементация на синхронен ForecastResource (вижте листинг 9), който връща всички местоположения. След това, за всяка позиция, той извиква температурната услуга, за да получи стойностите в градуси по Целзий.

@Path("/forecast") public class ForecastResource ( @Uri("location") private WebTarget locationTarget; @Uri("temperature/(city)") private WebTarget temperatureTarget; @GET @Produces(MediaType.APPLICATION_JSON) public Response getLocationsWithTemperature () ( long startTime = System.currentTimeMillis(); ServiceResponse response = new ServiceResponse(); List местоположения = locationTarget .request() .get(нов GenericType >()()); forEach(location -> ( Temperature temperature = temperatureTarget .resolveTemplate("city", location.getName()) .request() .get(Temperature.class); response.getForecasts().add(new Forecast(location) .setTemperature (температура)); )); дълго крайно време = System.currentTimeMillis(); response.setProcessingTime(endTime - startTime); връщане Response.ok(response).build(); ) )
Когато прогнозната дестинация е поискана като /forecast, ще получите изход, подобен на листинг 10. Обърнете внимание, че обработката на заявката отне 1,533 ms, което е логично, тъй като синхронното изискване на температурни стойности от три различни местоположения добавя до 1,5 ms.

( "прогнози": [ ( "местоположение": ( "име": "Лондон" ), "температура": ( "скала": "Целзий", "температура": 33 ) ), ( "местоположение": ( "име ": "Истанбул"), "температура": ( "скала": "Целзий", "температура": 38)), ( "местоположение": ( "име": "Прага"), "температура": ( "скала ": "Целзий", "температура": 46 ) ) ], "Време за обработка": 1533 )
Засега всичко върви по план. Време е да се въведе реактивно програмиране от страната на сървъра, където повикванията към всяко местоположение могат да се правят паралелно, след като всички местоположения бъдат получени. Това може ясно да подобри синхронния поток, показан по-рано. Това се прави в листинг 11, който показва дефиницията на реактивна версия на прогнозната услуга.

@Path("/reactiveForecast") public class ForecastReactiveResource ( @Uri("location") private WebTarget locationTarget; @Uri("temperature/(city)") private WebTarget temperatureTarget; @GET @Produces(MediaType.APPLICATION_JSON) public void getLocationsWithTemperature (@Suspended final AsyncResponse async) ( long startTime = System.currentTimeMillis(); // Създаване на етап за извличане на местоположения на CompletionStage > locationCS = locationTarget.request() .rx() .get(нов GenericType >() ()); // Чрез създаване на отделен етап в етапа на местоположенията, // описан по-горе, съберете списъка с прогнози // като в един голям CompletionStage последен CompletionStage > forecastCS = locationCS.thenCompose(locations -> ( // Създаване на етап за получаване на прогнози // като CompletionStage List > forecastList = // Поточно предаване на местоположения и обработка на всяко едно // поотделно locations.stream().map(location -> ( // Създаване на стъпка, за да получите // температурите само на един град // по неговото име final CompletionStage tempCS = temperatureTarget .resolveTemplate("град", местоположение.getName()) .request() .rx() .get(Temperature.class); // След това създайте CompletableFuture, който // съдържа екземпляр на прогноза с // местоположение и стойност на температурата return CompletableFuture.completedFuture(new Forecast(location)) .thenCombine(tempCS, Forecast::setTemperature); )).collect(Collectors.toList()); // Връща крайния екземпляр на CompletableFuture, където // всички представени бъдещи обекти с възможност за завършване // са завършени return CompletableFuture.allOf(forecastList.toArray(new CompletableFuture)) .thenApply(v -> predictList.stream() .map(CompletionStage::toCompletableFuture ) .map(CompletableFuture::join) .collect(Collectors.toList())); )); // Създаване на екземпляр на ServiceResponse, който // съдържа пълен списъкпрогнози // заедно с времето за обработка. // Създайте неговото бъдеще и го комбинирайте с // forecastCS, за да получите прогнози // и вмъкнете в отговора на услугата CompletableFuture.completedFuture(new ServiceResponse()) .thenCombine(forecastCS, ServiceResponse::forecasts) .whenCompleteAsync((response, throwable) - > ( response.setProcessingTime(System.currentTimeMillis() - startTime); async.resume(response); )); ) )
Реактивната реализация може да изглежда сложна на пръв поглед, но след по-отблизо ще забележите, че е доста проста. В изпълнението на ForecastReactiveResource първо правя клиентско повикване към услугите за местоположение, използвайки JAX-RS Reactive Client API. Както споменах по-горе, това е допълнение за Java EE 8 и помага да се създаде реактивно повикване просто с метода rx().

Сега създавам нов етап въз основа на местоположението, за да събера списъка с прогнози. Те ще се съхраняват като списък с прогнози в един голям етап на завършване, наречен forecastCS. В крайна сметка ще създам отговор на обаждане за услуга, като използвам само прогнозCS.

Сега нека съберем прогнозите като списък от етапи на завършване, дефинирани в променлива прогнозЛист. За да създам етап на завършване за всяка прогноза, предавам данните за местоположението и след това създавам променлива tempCS, отново използвайки JAX-RS Reactive Client API, който извиква температурната услуга с името на града. Тук използвам метода resolveTemplate() за изграждане на клиента и това ми позволява да предам името на града на строителя като параметър.

Като последна стъпка от поточното предаване, правя извикване на CompletableFuture.completedFuture() , като предавам новия екземпляр на Forecast като параметър. Комбинирам това бъдеще с етапа tempCS, така че да имам температурната стойност за повторените местоположения.

Методът CompletableFuture.allOf() в листинг 11 преобразува списък от етапи на завършване в predictCS. Изпълнението на тази стъпка връща голямо завършващо бъдеще, когато всички предоставени завършващи фючърси са завършени.

Отговорът на услугата е екземпляр на класа ServiceResponse, така че създавам завършено бъдеще и след това свързвам етапа на завършване на predictCS със списъка с прогнози и изчислявам времето за реакция на услугата.

Разбира се, реактивното програмиране само принуждава страната на сървъра да се изпълнява асинхронно; клиентската страна ще блокира, докато сървърът не изпрати отговор обратно на заявителя. За да се преодолее този проблем, сървърните изпратени събития (SSE) могат да се използват за изпращане на частичен отговор веднага щом е наличен, така че температурните стойности за всяко местоположение да се изпращат на клиента една по една. Резултатът от ForecastReactiveResource ще бъде подобен на списък 12. Както е показано в изхода, времето за обработка е 515ms, което е идеалното време за изпълнение за получаване на температурни стойности от едно място.

( "прогнози": [ ( "местоположение": ( "име": "Лондон" ), "температура": ( "скала": "Целзий", "температура": 49 )), ( "местоположение": ( "име ": "Истанбул"), "температура": ( "скала": "Целзий", "температура": 32 )), ( "местоположение": ( "име": "Прага"), "температура": ( "скала ": "Целзий", "температура": 45 ) ) ], "Време за обработка": 515 )
Заключение

В примерите в тази статия за първи път показах синхронен начин за получаване на прогнози с помощта на услуги за местоположение и температура. След това преминах към реактивен подход, за да имам асинхронна обработка между извикванията на услугата. Когато използвате JAX-RS Reactive Client API в Java EE 8 заедно с класовете CompletionStage и CompletableFuture, налични в Java 8, силата на асинхронната обработка се отприщва чрез реактивно програмиране.

Реактивното програмиране е повече от просто внедряване на асинхронен модел от синхронен; също така опростява концепции като етапа на влагане. Колкото повече се използва, толкова по-лесно ще бъде управлението на сложни сценарии в паралелното програмиране.

Благодаря за вниманието. Както винаги, приветстваме вашите коментари и въпроси.

Можете да помогнете и да прехвърлите средства за развитието на сайта



Зареждане...
Горна част