FRP, или функциональное реактивное программирование, не является новой парадигмой программирования. Его происхождение можно проследить до документа 1997 года с реализацией на известном функциональном языке программирования Haskell. Потребовалось больше времени, чтобы он получил широкое распространение на других языках, а зрелые варианты библиотек в последние несколько лет были доступны только разработчикам JavaScript.

Многие разработчики JavaScript опасаются вкладывать средства в «следующую большую вещь». Веб-сообщество испытало невероятную текучку идей, когда инвестиции в передовой опыт вскоре после этого становятся бесполезными.

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

В Yelp, где я уже 3 года работаю разработчиком программного обеспечения с полным стеком, большая часть кодовой базы содержится в монолите, который существует с момента создания продукта. Помимо этого монолита, команды Yelp также владеют и поддерживают множество микросервисов. В целом ожидается, что разработчик программного обеспечения с полным стеком будет перемещаться по миллионам строк кода через границы репозитория и API. Иногда, несмотря на это, написание кода происходит очень естественно. Новый код в Yelp имеет тенденцию следовать схожим шаблонам и включает в себя парадигмы функционального программирования, для которых создан Python, наш предпочтительный язык. Создавая на основе коллективной работы сотен разработчиков, написанной более десяти лет, я обнаружил, что есть одна вещь, а не возраст программного обеспечения, которая позволяет мне эффективно разрабатывать кодовую базу Yelp. Если фрагменты кода, на основе которых я строю, «выразительны», большая часть утомительной работы в устаревших системах устраняется.

Выразительный код:

  1. Является декларативным
  2. Составной
  3. Помогает разработчикам упасть в яму успеха

Вот как FRP, и в частности RxJS, естественно приводит к выразительному коду.

декларативный

Одна из вещей, которые я все больше и больше наблюдаю в коде по мере взросления как разработчика программного обеспечения, — это то, насколько эффективно код действительно представляет проблему, если смотреть через призму преобразования данных и манипулирования ими.

Рассмотрим два подхода к созданию объекта из двух списков в JavaScript: «императивный» подход и «декларативный» подход.

На первый взгляд, первый пример кода может показаться лучше. Это меньше строк кода, «проще» и не зависит от библиотеки. Зачем утруждать себя решением тривиальной проблемы вторым путем?

Давайте подумаем, чего мы на самом деле пытаемся достичь с нашими данными. У нас есть два списка, где индексы элементов связывают элементы друг с другом. Вместо этого мы хотим создать единый объект, в котором эти списки находятся в форме key: value form. Первый пример технически правильный (и эквивалентный), но он нечетко говоритчто код пытается выполнить. Разработчику, читающему код, написанный таким образом, возможно, придется тщательно его обдумать, прежде чем он будет уверен в механизме, с помощью которого формируется результирующий объект.

Второй пример фактически выражает отношение между нашими данными. zip — один из многих операторов RxJS, которые создают и изменяют потоки. В этом случае zip работает точно так, как следует из его названия — он принимает два потока событий и объединяет их вместе на основе совпадающих индексов. Кроме того, мы используем scan (по сути, RxJS-версию reduce для потоков), которая берет список этих объединенных вместе событий и итеративно создает объект. Вместо того, чтобы просто выполнять задачу, разработчик, читающий этот декларативный код, имеет больше шансов понять, как соотносятся наши данные и как наш код приближается к этому взаимоотношению.

Составной

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

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

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

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

Второй пример уже перспективен по своей архитектуре. Мы снова полагаемся на нашего знакомого оператора scan для создания двух потоков — iceCreamFlavors$ и iceCreamFormPrices$, которые будут каноническими представлениями всех вкусов и цен, которые у нас есть в приложении. Мы уже видим, что эти потоки полезны помимо нашей непосредственной потребности в отображении! Лучше всего то, что здесь нам не нужно самим мутировать объекты. По мере появления новых вкусовых и ценовых событий они будут просто добавляться в копии списков, которые мы итеративно создаем. Как видите, добавление в эту архитектуру других источников потока для вкусов и цен требует минимальных усилий — на самом деле, нам вообще не нужно было трогать наш iceCreamFlavorToPrice$ Observable! Это потому, что у нас есть компонуемая архитектура данных, которая выражает то, что она делает. iceCreamFlavorToPrice$ принимает поток ароматов и поток цен, ему не нужно заботиться о деталях того, как эти события генерируются.

Яма успеха

Это хорошо известный принцип, связанный с проектированием программного обеспечения, впервые сформулированный в блоге Coding Horror 2007 года Falling Into The Pit of Success. Из статьи:

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

RxJS превосходен в том, чтобы направлять разработчиков к «правильным вещам» во многих отношениях, но вот несколько замечаний.

Асинхронный

Асинхронное выполнение кода всегда было неотъемлемой частью JavaScript — цикл событий — одна из основных частей, которая отличает его от других языков программирования высокого уровня. К сожалению, эффективное использование мощности параллельного выполнения в JavaScript исторически было громоздким. Мы не так уж далеки от дней ада обратных вызовов Node.js и полифилов Promise. Недавно современный JavaScript представил async-await, который устранил большую часть мусора, связанного с этими проблемами. К сожалению, неправильное использование async-await может легко превратиться в код, который эффективно выполняется синхронно.

Обратите внимание, что самый «эффективный» способ параллельного получения результатов промисов на самом деле довольно неинтуитивен. Теперь давайте посмотрим, как RxJS обрабатывает параллелизм:

С такими операторами, как flatMap, которые будут объединять события из нескольких потоков вместе по мере их создания, разработчики JS могут выразительно добиться оптимальной производительности в своих приложениях без вызывающих головную боль манипуляций с Promise.

Утечки памяти

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

В Angular, JS-фреймворке со встроенными потоками, мы можем принудительно завершить наши потоки и освободить память до того, как используемый ими компонент будет уничтожен, с помощью оператора takeUntil.

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

Как видите, с FRP многие сложности управления памятью могут быть абстрагированы от нас.

Очистить зависимости данных

Как говорится в сообщении блога Coding Horror:

Хорошо спроектированная система делает неправильные действия раздражающими (но не невозможными).

Один из первых больших препятствий для начинающих FRP-разработчиков, который я наблюдал, — это трудности с жонглированием многими зависимыми потоками.

Разработчики могут столкнуться с тем, что создают потоки, подобные приведенным выше, пытаясь агрегировать множество потоков и одновременно манипулировать событиями. Это станет ужасно утомительным, поскольку линейная природа операторов RxJS становится очевидной — для большинства операторов вы получаете доступ только к тому, что вы в конечном итоге возвращаете в предыдущем операторе. Это эффективно превращает каждый оператор в свою собственную чистую функцию, выполняемую последовательно (действительно, RxJS поощряет вас писать свои собственные операторы как чистые функции). Это способствует продуманному структурированию данных в потоке. Естественно возникают такие вопросы, как: к чему эта конкретная функция действительно нуждается в доступе в данный момент времени? Я пытаюсь сделать слишком много с этим оператором? Какая информация мне понадобится в дальнейшем? Как мне упорядочить преобразования?

Эти вопросы, рожденные от раздражения, в конечном итоге приводят к продуманным, выразительным потокам. Вот переосмысление предыдущего потока без раздражения:

Как видите, мы используем пользовательский оператор (filterLoggedIn$) и функцию, которая создает поток (combinedInfoFactory$), чтобы разбить наш код на части и четко установить составные части. Эти части будут получать нужные им данные и ничего более. В конечном счете, такой выбор дизайна будет благом для нашего приложения по мере его усложнения. С полезными ограничениями, которые RxJS накладывает на то, как мы можем манипулировать нашими данными, мы в конечном итоге попадаем в яму успеха дизайна, просто естественным образом написав код на основе потоков.

Хорошо написанный код FRP является декларативным, составляемым и помогает разработчикам попасть в яму успеха. Современный JavaScript, хотя и более функциональный как автономный язык, чем когда-либо, по-прежнему поддерживается мощными реактивными библиотеками, такими как RxJS.

Чтобы увидеть практический пример работы FRP в приложении, посетите мой личный сайт thatguyjackguy.com (исходный код доступен здесь).