What is FRP

User’s demands increase over time, expecting dynamic behavior millisecond response times in apps wihch grow increasingly complex. The architecture which supports these apps is outlined in the Reactive Manifesto, but is (reductively) responsive, resilient, elastic, and message driven.

As code moves toward async, I’ve fallen in love with Promises. However, promises are meant for representing the outcome of an action or a value that will be available in the future - but only one value.

Functional Reactive Programming (FRP) scales the ideas of promises to working with streams, recurring events, and dynamic values. FRP simply provides an abstraction that makes working with these streams easier, more composable, etc. - in a sense like promises, they provide an abstraction and allow us to move away from callbacks.

FRP libraries simpify composing asynchronous and event-based programs, often through the use of Observables and Schedulers. Streams are things like WebSockets, twitter feeds, health tracking data, etc. They are discrete values / events that vary over continuous time, and reactions to they may change the system over time.

Observables are basically just asynchronous immutable arrays. FRP libraries are the Underscore.js of events. Streams can be combined, split, piped, and generally manipulated in all kinds of ways. Properties can be bound, sampled, combined, transformed, or whatever.

FRP Libraries

There are multiple libraries around FRP for javascript, but two that are prominent are Bacon.js and RxJS.

The two are fairly similar. Bacon was written out of frustration with RxJS, but the two have come a long way since. To compare the two, here’s an example of a search bar which queries wikipedia and displays the results as a list:

// Bacon.js Version, from http://sitr.us/2013/05/22/functional-reactive-programming-in-javascript.html
// jsFiddle: http://jsfiddle.net/hallettj/SqrNT/

function searchWikipedia(term) {
    var url = '//en.wikipedia.org/w/api.php?callback=?';
    return $.getJSON(url, {
        action: 'opensearch',
        format: 'json',
        search: term
    })
    .then(function(data) {
        return { query: data[0], results: data[1] };
    });
}

var suggestions = distinct.flatMapLatest(function(value) {
    return Bacon.fromPromise(searchWikipedia(value));
});

suggestions.onValue(function(data) {
    var $results = $('#results').empty();
    data.results.forEach(function(s) {
        $('<li>').text(s).appendTo($results);
    });
});
// RxJS, adapted from their site
// jsFiddle: http://jsfiddle.net/mattpodwysocki/AL8Mj/

(function (global, undefined) {

    // Search Wikipedia for a given term
    function searchWikipedia(term) {
        var cleanTerm = global.encodeURIComponent(term);
        var url = 'http://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=' + cleanTerm + '&callback=JSONPCallback';
        return Rx.DOM.Request.jsonpRequestCold(url);
    }

    function clearChildren(element) {
        while (element.firstChild) {
            element.removeChild(element.firstChild);
        }
    }

    function main() {
        var input = document.querySelector('#textInput'),
            results = document.querySelector('#results');

        // Get all distinct throttled key up events from the input
        var keyup = Rx.DOM.fromEvent(input, 'keyup')
            .map(function (e) {
                return e.target.value; // Project the text from the input
            })
            .filter(function (text) {
                return text.length > 2; // Only if the text is longer than 2 characters
            })
            .throttle(750 /* Pause for 750ms */ )
            .distinctUntilChanged(); // Only if the value has changed

        // Search wikipedia
        var searcher = keyup
            .map(function (text) { return searchWikipedia(text); })
            .switchLatest(); // Ensure no out of order results

        var subscription = searcher.subscribe(
            function (data) {
                // Append the results
                clearChildren(results);

                var res = data[1];

                var i, len, li;
                for (i = 0, len = res.length; i < len; i++) {
                    li = document.createElement('li');
                    li.innerHTML = res[i];
                    results.appendChild(li);
                }
            },
            function (error) {
                // Handle any errors
                clearChildren(results);

                var li = document.createElement('li');
                li.innerHTML = 'Error: ' + error;
                results.appendChild(li);
            }
        );
    }

    main();

}(window));

Stay tuned for combining FRP with React, Facebook’s view library, or maybe a service worker (that may come later when they do more than just caching, which is the current state of affairs), or something else…

Resources

Chris Meiklejon gave a good intro at NodePDX 2013