Pull to refresh

Kefir.js — новая библиотека для функционального реактивного программирования (FRP) в JavaScript

Reading time 4 min
Views 22K
Наверняка многие уже слышали о подходе 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 — документация
Only registered users can participate in poll. Log in, please.
Слышали ли вы раньше об FRP?
56.63% Да 252
43.37% Нет, узнал(а) из этого поста 193
445 users voted. 66 users abstained.
Only registered users can participate in poll. Log in, please.
Если пользовались RxJS или Bacon, то что вам больше нравится?
46.67% Bacon 42
53.33% RxJS 48
90 users voted. 336 users abstained.
Tags:
Hubs:
+26
Comments 19
Comments Comments 19

Articles