Наверняка многие уже слышали о подходе FRP для организации асинхронного кода. На хабре уже писали об FRP (Реактивное программирование в Haskell, FRP на Bacon.js) и есть хорошие доклады на эту тему (Программировние UI с помощью FRP и Bacon.js, Functional Reactive Programming & ClojureScript, О Bacon.js от Juha Paananen — автора бекона)
Если коротко, FRP это подход похожий на Promise, но с неограниченным количеством возвращаемых значений, и бОльшим количеством методов для комбинирования / модифицирования потоков событий. Другими словами, если Promise позволяют работать со значением, которого у вас еще нет, так, будто оно у вас уже есть, то FRP позволяет работать со значением, меняющимся во времени, так, будто оно не меняется.
Вот что это дает по сравнению с обратными вызовами:
1) Поток событий (Event stream) и значение меняющаяся во времени (Property / Behavior) становятся объектами первого класса. Это значит что их можно передавать в функции и возвращать из функций.
Например, можно создать объект содержащий клики на кнопку (поток событий), и дальше делать с этим объектом всё, что можно делать с обычной переменной — передавать в функцию, возвращать из функции, сохранять как свойство другого обекта и т.д. Или можно создать объект отражающий текущий размер окна браузера (значение меняющаяся во времени).
Это позволяет гораздо лучше разделять ответственности в коде, разделять его на модули, и писать более гибкий, короткий и управляемый код.
К примеру можно написать функцию, возвращающую поток перетаскиваний (drag). В качестве параметров она будет принимать 3 потока — начало перетаскивания, движение, конец перетаскивания. Дальше можно передать в эту функцию: либо потоки для соответствующих событий мыши (mousedown, mousemove, mouseup), либо для touch событий (touchstart, touchmove, touchend). Сама же функция не будет ничего знать об источниках событий, а будет работать только с абстрактными потоками. Пример реализации на Bacon.
2) Явный state
Второе большое преимущество FRP это явное управление состоянием. Как известно, state — один из самых главных источников сложности программ, поэтому грамотное управление им позволяет писать более надежные и простые в поддержке программы. Отличный доклад от Рича Хикки о сложности (complexity) «Simple Made Easy».
FRP позволяет писать бОльшую часть кода на «чистых функциях» и управлять потоком данных (dataflow) явно (с помощью потоков событий), а состояния хранить тоже явно в Property.
Kefir.js
Сейчас есть две основные FRP библиотеки для JavaScript, это Bacon.js и RxJS. Как мне кажется, Bacon более близок духу функционального программирования, а RxJS это что-то из мира ООП. У Rx очень тяжелая для восприятия документация — её во-первых много, а во-вторых она написана в очень формальном стиле (как автогенерируемая документация из исходного кода). Т.е. Rx труднее изучать и труднее им пользоваться. Но Rx более быстрый и потребляет меньше памяти.
Последнее обстоятельство иногда бывает ахиллесовой пятой Bacon. Впервые я заметил проблему, когда попытался написать аналог scrollMonitor на Bacon. Получился очень хороший API со всей мощью FRP, но когда я запустил этот стресс тест, всё просто зависло. Как оказалось, Bacon потребляет кучу памяти и частые сборки мусора вызывают фризы. Это может быть актуально при большом количестве потоков или на мобильных устройствах. Я считаю что в библиотеке должен быть больший запас производительности, чтобы меньше думать об этом при написании кода приложения!
Kefir.js — новая FRP билиотека, над которой я работаю последние несколько месяцев. API Kefir очень похож на API Bacon, но в Kefir я уделяю много внимания производительности и потреблению памяти. Сейчас Kefir примерно в 5-10 раз быстрее Bacon, и в 1-2 раза быстрее Rx, примерно тоже и с памятью.
Сравнение производительности Kefir и Bacon в живом тесте. Также есть результаты синтетических тестов памяти. Еще есть синтетические тесты производительности, вот результаты некоторых из них:
stream.map(id)
----------------------------------------------------------------
Kefir x 7,692,055 ops/sec ±1.62% (33 runs sampled)
Bacon x 703,734 ops/sec ±1.63% (34 runs sampled)
RxJS x 2,303,480 ops/sec ±1.70% (34 runs sampled)
-----------------------
Kefir 1.00 Bacon 0.09 RxJS 0.30
stream.map(id) with multiple listeners
----------------------------------------------------------------
Kefir x 4,185,280 ops/sec ±0.89% (34 runs sampled)
Bacon x 421,695 ops/sec ±0.79% (33 runs sampled)
RxJS x 604,156 ops/sec ±1.21% (31 runs sampled)
-----------------------
Kefir 1.00 Bacon 0.10 RxJS 0.14
stream.flatMap (x) -> Lib.once(x)
----------------------------------------------------------------
Kefir x 1,073,871 ops/sec ±1.14% (32 runs sampled)
Bacon x 57,474 ops/sec ±4.45% (28 runs sampled)
-----------------------
Kefir 1.00 Bacon 0.05
stream.combine(Lib.constant(1), fn)
----------------------------------------------------------------
Kefir x 2,413,356 ops/sec ±1.14% (34 runs sampled)
Bacon x 220,898 ops/sec ±1.41% (34 runs sampled)
-----------------------
Kefir 1.00 Bacon 0.09
stream.skipDuplicates()
----------------------------------------------------------------
Kefir x 7,009,320 ops/sec ±1.49% (33 runs sampled)
Bacon x 684,319 ops/sec ±1.55% (34 runs sampled)
RxJS x 401,798 ops/sec ±1.48% (31 runs sampled)
-----------------------
Kefir 1.00 Bacon 0.10 RxJS 0.06
Также я стараюсь делать Kefir максимально простым для изучения, примерно как Underscore или LoDash. Поэтому и документация очень похожа на документацию Underscore. Цель — сделать документацию лучше чем и в Rx и в Bacon.
Еще одна цель Kefir — это переосмысление API Bacon. Bacon долго развивался и, из за необходимости поддерживать обратную совместимость, API в некоторых местах стал немного корявым. В Kefir есть возможность написать всё с чистого листа, и я стараюсь этой возможностью пользоваться.
Текущее состояние
Сейчас Kefir находится в состоянии разработки, но уже много всего есть, и им можно пользоваться. Документация тоже еще не полная, но я надеюсь скоро ее дописать и поддерживать в полном состоянии при добавлении новых фич.
По сравнению с Bacon сейчас в Kefir не хватает:
- Ошибок как событий, есть только значения
- Части методов / комбинаторов: zip, combineTemplate, when, update, различных методов для буферов, и некоторых других
- Атомарных событий (в Rx, кстати, тоже нет)
Вот и всё что я хотел пока рассказать про Kefir. Я не описывал подробно саму библиотеку, т.к. Kefir очень похож на Bacon, и если вы знакомы с последним, то без труда освоите первый. А если нет, то можно изучать Kefir по туториалам Bacon, поглядывая в документацию кефира :-)
github.com/pozadi/kefir — проект на GitHub
pozadi.github.io/kefir — документация
Автор: Pozadi