- PVSM.RU - https://www.pvsm.ru -

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

Наверняка многие уже слышали о подходе FRP для организации асинхронного кода. На хабре уже писали об FRP (Реактивное программирование в Haskell [1], FRP на Bacon.js [2]) и есть хорошие доклады на эту тему (Программировние UI с помощью FRP и Bacon.js [3], Functional Reactive Programming & ClojureScript [4], О Bacon.js от Juha Paananen — автора бекона [5])

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

Вот что это дает по сравнению с обратными вызовами:

1) Поток событий (Event stream) и значение меняющаяся во времени (Property / Behavior) становятся объектами первого класса [6]. Это значит что их можно передавать в функции и возвращать из функций.

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

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

К примеру можно написать функцию, возвращающую поток перетаскиваний (drag). В качестве параметров она будет принимать 3 потока — начало перетаскивания, движение, конец перетаскивания. Дальше можно передать в эту функцию: либо потоки для соответствующих событий мыши (mousedown, mousemove, mouseup), либо для touch событий (touchstart, touchmove, touchend). Сама же функция не будет ничего знать об источниках событий, а будет работать только с абстрактными потоками. Пример реализации на Bacon [7].

2) Явный state

Второе большое преимущество FRP это явное управление состоянием. Как известно, state — один из самых главных источников сложности программ, поэтому грамотное управление им позволяет писать более надежные и простые в поддержке программы. Отличный доклад от Рича Хикки о сложности (complexity) «Simple Made Easy» [8].

FRP позволяет писать бОльшую часть кода на «чистых функциях» [9] и управлять потоком данных (dataflow) явно (с помощью потоков событий), а состояния хранить тоже явно в Property.

Kefir.js

Сейчас есть две основные FRP библиотеки для JavaScript, это Bacon.js [10] и RxJS [11]. Как мне кажется, Bacon более близок духу функционального программирования, а RxJS это что-то из мира ООП. У Rx очень тяжелая для восприятия документация — её во-первых много, а во-вторых она написана в очень формальном стиле (как автогенерируемая документация из исходного кода). Т.е. Rx труднее изучать и труднее им пользоваться. Но Rx более быстрый и потребляет меньше памяти.

Последнее обстоятельство иногда бывает ахиллесовой пятой Bacon. Впервые я заметил проблему, когда попытался написать аналог scrollMonitor [12] на Bacon. Получился очень хороший API со всей мощью FRP, но когда я запустил этот стресс тест [13], всё просто зависло. Как оказалось, Bacon потребляет кучу памяти и частые сборки мусора вызывают фризы. Это может быть актуально при большом количестве потоков или на мобильных устройствах. Я считаю что в библиотеке должен быть больший запас производительности, чтобы меньше думать об этом при написании кода приложения!

Kefir.js [14] — новая FRP билиотека, над которой я работаю последние несколько месяцев. API Kefir очень похож на API Bacon, но в Kefir я уделяю много внимания производительности и потреблению памяти. Сейчас Kefir примерно в 5-10 раз быстрее Bacon, и в 1-2 раза быстрее Rx, примерно тоже и с памятью.

Сравнение производительности Kefir и Bacon в живом тесте [15]. Также есть результаты синтетических тестов памяти [16]. Еще есть синтетические тесты производительности, вот результаты некоторых из них:

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. Поэтому и документация [14] очень похожа на документацию Underscore. Цель — сделать документацию лучше чем и в Rx и в Bacon.

Еще одна цель Kefir — это переосмысление API Bacon. Bacon долго развивался и, из за необходимости поддерживать обратную совместимость, API в некоторых местах стал немного корявым. В Kefir есть возможность написать всё с чистого листа, и я стараюсь этой возможностью пользоваться.

Текущее состояние

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

По сравнению с Bacon сейчас в Kefir не хватает:

  • Ошибок как событий, есть только значения
  • Части методов / комбинаторов: zip, combineTemplate, when, update, различных методов для буферов, и некоторых других
  • Атомарных событий [17] (в Rx, кстати, тоже нет)

Вот и всё что я хотел пока рассказать про Kefir. Я не описывал подробно саму библиотеку, т.к. Kefir очень похож на Bacon, и если вы знакомы с последним, то без труда освоите первый. А если нет, то можно изучать Kefir по туториалам Bacon, поглядывая в документацию кефира :-)

github.com/pozadi/kefir [18] — проект на GitHub
pozadi.github.io/kefir [19] — документация

Автор: Pozadi

Источник [20]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/69978

Ссылки в тексте:

[1] Реактивное программирование в Haskell: http://habrahabr.ru/post/140719/

[2] FRP на Bacon.js: http://habrahabr.ru/post/198656/

[3] Программировние UI с помощью FRP и Bacon.js: http://www.youtube.com/watch?v=orP_1l_SkiA

[4] Functional Reactive Programming & ClojureScript: http://www.youtube.com/watch?v=R4sTvHXkToQ

[5] О Bacon.js от Juha Paananen — автора бекона: http://www.ustream.tv/recorded/29299079

[6] объектами первого класса: https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82_%D0%BF%D0%B5%D1%80%D0%B2%D0%BE%D0%B3%D0%BE_%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B0

[7] Пример реализации на Bacon: https://github.com/pozadi/bacon-reusable-parts/blob/master/src/bacon-drags.coffee

[8] Отличный доклад от Рича Хикки о сложности (complexity) «Simple Made Easy»: http://www.infoq.com/presentations/Simple-Made-Easy

[9] «чистых функциях»: https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D1%82%D0%BE%D1%82%D0%B0_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8

[10] Bacon.js: http://baconjs.github.io/

[11] RxJS: https://github.com/Reactive-Extensions/RxJS

[12] scrollMonitor: https://github.com/sakabako/scrollMonitor

[13] этот стресс тест: http://sakabako.github.io/scrollMonitor/demos/stress.html

[14] Kefir.js: http://pozadi.github.io/kefir/

[15] Сравнение производительности Kefir и Bacon в живом тесте: http://pozadi.github.io/kefir/demos/tree.html

[16] результаты синтетических тестов памяти: https://github.com/pozadi/kefir/blob/master/test/perf/memory-results.txt

[17] Атомарных событий: https://github.com/baconjs/bacon.js/#atomic-updates

[18] github.com/pozadi/kefir: https://github.com/pozadi/kefir

[19] pozadi.github.io/kefir: http://pozadi.github.io/kefir

[20] Источник: http://habrahabr.ru/post/237495/