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

в 23:11, , рубрики: FRP, javascript, kefir.js, rx, rxjs, Программирование, реактивное программирование, функциональное программирование

Наверняка многие уже слышали о подходе 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

Источник

* - обязательные к заполнению поля