FRP with RxJS

Aliaksei Bahachuk 

Reactive Extensions

Reactive Extensions is a library to compose async and event-based programs using observable collections and query operators.

Rx is everywhere

Java

JavaScript

.Net

Ruby

Swift

C++

Clojure

Scala

What is the most asynchronous?

User

More async data streams

  • XHR requests

  • WebSocket

  • Promises

  • Fibers

  • Events

  • Dom Inputs

  • Web Worker

  • Web RTC 

Stream creators

  • Rx.jquery

  • Rx.Observable.create

  • Rx.Observable.fromEvent

  • Rx.Observable.fromCallback

  • Rx.Observable.fromNodeCallback

  • Rx.Observable.fromPromise

Stream operators

RxJS operators

RxJS operators: map

resStream

RxJS operators: delay

resStream

RxJS operators: debounce

resStream

RxJS operators: merge

RxJS operators: combineLatest

RxJS operators: zip

Let's create a morse-code decoder!

Morse-code decoder

click...

click...

click.............

click...

Morse-code decoder

click...

click...

click.............

click...

Morse-code decoder

  • Dom events (keydown, keyup)

  • Start signal/End signal

  • Dot/Dash/Whitespace

  • Letter

  • Word/Sentence

Morse-code decoder

Morse-code decoder

  • "." = ~400ms

  • "_" = 3 * "."

  • between codes = "."

  • between letters = 3 * "." 

  • between words = 7 * "."

Morse-code decoder

const keyUps = Rx.Observable
                 .fromEvent(document, 'keyup');

const keyDowns = Rx.Observable
                   .fromEvent(document, 'keydown');

Morse-code decoder

const spaceKeyUps = keyUps.filter((data) => data.keyCode === 32);

const spaceKeyDowns = keyDowns.filter((data) => data.keyCode === 32);

Morse-code decoder

const signalStartsRaw = spaceKeyDowns.map(() => "start");
const signalEndsRaw = spaceKeyUps.map(() => "end");

const signalStartsEnds = Rx.Observable
                            .merge(signalStartsRaw, signalEndsRaw)
                            .distinctUntilChanged();

const signalStarts = signalStartsEnds
                            .filter((ev) => ev === "start");
const signalEnds = signalStartsEnds
                            .filter((ev) => ev === "end");

Morse-code decoder

const signalStarts = signalStartsEnds
                           .filter((ev) => ev === "start")
                           .timestamp();
const signalEnds = signalStartsEnds
                           .filter((ev) => ev === "end")
                           .timestamp();

const spanStream = signalStarts.flatMap((start) => {
    return signalEnds.map((end) => end.timestamp - start.timestamp)
                     .first();
});

Morse-code decoder

Morse-code decoder

const SPAN = 400;

const dotsStream = spanStream
                        .filter((v) => v <= SPAN)
                        .map(() => ".");

const lineStream = spanStream
                        .filter((v) => v > SPAN)
                        .map(() => "-");

Morse-code decoder

const dotsAndLines = Rx.Observable.merge(dotsStream, lineStream);

// [['.', '.', '-'], ['-', '.', '-'] ... ] 
const letterCodes = dotsAndLines.buffer(letterWhitespaces); 

 // ['A', 'B' ...]
const lettersStream = letterCodes
                        .map((codes) => morse.decode(codes.join("")));

Morse-code decoder

Morse-code decoder

What else?

Component bindings

Component bindings

click...

click...

click.............

click...

click

category.

comp.

bundle.

package.

Component bindings

Component bindings

RxJS with other frameworks

RxJS with Angular - rx.angular.js

angular.module('example', ['rx'])
  .controller('AppCtrl', function($scope, $http, rx) {

    function searchWikipedia (term) {
      return rx.Observable
        .fromPromise($http({}))
        .map(function(response){ return response.data[1]; });             
    }

    $scope.search = '';
    $scope.results = [];

    $scope.$createObservableFunction('click')
      .map(function () { return $scope.search; })
      .flatMapLatest(searchWikipedia)
      .subscribe(function(results) {
        $scope.results = results;
      });
  });

RxJS with Angular2 - inside

RxJS with React - rx-react

RxJS with others

Alternative?

Bacon

Kefir

Highland

Tests

stream1.combine(stream2, ->) (1000 samples)
----------------------------------------------------------------
Kefir   w/o subscr. 0.44 KiB   w/ subscr. +0.80 KiB   sum 1.23 KiB
Bacon   w/o subscr. 5.42 KiB   w/ subscr. +6.15 KiB   sum 11.57 KiB
Rx      w/o subscr. 0.43 KiB   w/ subscr. +2.84 KiB   sum 3.26 KiB
-----------------------
Kefir 1.00 1.00 1.00    Bacon 12.45 7.71 9.39    Rx 0.98 3.56 2.65

.filter(->) (1000 samples)
----------------------------------------------------------------
Kefir   w/o subscr. 0.31 KiB   w/ subscr. +0.46 KiB   sum 0.77 KiB
Bacon   w/o subscr. 1.82 KiB   w/ subscr. +2.49 KiB   sum 4.31 KiB
Rx      w/o subscr. 0.37 KiB   w/ subscr. +1.44 KiB   sum 1.81 KiB
-----------------------
Kefir 1.00 1.00 1.00    Bacon 5.91 5.39 5.60    Rx 1.21 3.11 2.35

.map(->) (1000 samples)
----------------------------------------------------------------
Kefir   w/o subscr. 0.30 KiB   w/ subscr. +0.47 KiB   sum 0.77 KiB
Bacon   w/o subscr. 1.81 KiB   w/ subscr. +2.49 KiB   sum 4.30 KiB
Rx      w/o subscr. 0.37 KiB   w/ subscr. +1.43 KiB   sum 1.81 KiB
-----------------------
Kefir 1.00 1.00 1.00    Bacon 6.07 5.31 5.60    Rx 1.24 3.06 2.35

I want more hardcore!

Javelin and ClosureScript

(defc test-results
  {:scores [74 51 97 88 89 91 72 77 69 72 45 63]
   :proctor "Mr. Smith"
   :subject "Organic Chemistry"
   :sequence "CHM2049"})

(defc= test-results-with-mean
  (let [scores (:scores test-results)
        mean   (/ (reduce + scores) (count scores))
        grade  (cond (<= 90 mean) :A
                     (<= 80 mean) :B
                     (<= 70 mean) :C
                     (<= 60 mean) :D
                     :else        :F)]
    (assoc test-results :mean mean :grade grade)))

Cycle.js

  • RxJS

  • Side effects/Logic

  • Composition

  • Streams,  streams, streams, streams

  • hJSX/JSX 

Questions?

RxJs, Bacon, Kefir...

By Aliaksei Bahachuk

RxJs, Bacon, Kefir...

An overview of RxJs, Bacon, Kefir

  • 2,119