Functional reactive programming

Let's build another kind of application that works in much the same way; one that uses functional programming to react to changes in state. But, this time, the application won't be able to rely on event listeners.

Imagine for a moment that you work for a news media company and your boss tells you to create a web application that tracks government election results on Election Day. Data is continuously flowing in as local precincts turn in their results, so the results to display on the page are very reactive. But we also need to track the results by each region, so there will be multiple objects to track.

Rather than creating a big object-oriented hierarchy to model the interface, we can describe it declaratively as immutable data. We can transform it with chains of pure and semi-pure functions whose only ultimate side effects are updating whatever bits of state absolutely must be held onto (ideally, not many).

And we'll use the Bacon.js library, which will allow us to quickly develop Functional Reactive Programming (FRP) applications. The application will only be used one day out of the year (Election Day), and our boss thinks it should take a proportional amount of time. With functional programming and a library such as Bacon.js, we'll get it done in half the time.

But first, we're going to need some objects to represent the voting regions, such as states, provinces, districts, and so on.

function Region(name, percent, parties){
  // mutable properties:
  this.name = name;
  this.percent = percent; // % of precincts reported
  this.parties = parties; // political parties

  // return an HTML representation
  this.render = function(){
    var lis = this.parties.map(function(p){
      return '<li>' + p.name + ': ' + p.votes + '</li>';
    });
    var output = '<h2>' + this.name + '</h2>';
    output += '<ul>' + lis.join('') + '</ul>'; 
    output += 'Percent reported: ' + this.percent; 
    return output;
  }
}
function getRegions(data) {
  return JSON.parse(data).map(function(obj){
    return new Region(obj.name, obj.percent, obj.parties);
  });
}
var url = 'http://api.server.com/election-data?format=json';
var data = jQuery.ajax(url);
var regions = getRegions(data);
app.container.innerHTML = regions.map(function(r){
  return r.render();
}).join(''),

While the above would be sufficient for just displaying a static list of election results, we need a way to update the regions dynamically. It's time to cook up some Bacon and FRP.

Reactivity

Bacon has a function, Bacon.fromPoll(), that lets us create an event stream, where the event is just a function that is called on the given interval. And the stream.subscribe() function lets us subscribe a handler function to the stream. Because it's lazy, the stream will not actually do anything without a subscriber.

var eventStream = Bacon.fromPoll(10000, function(){
  return Bacon.Next;
});
var subscriber = eventStream.subscribe(function(){
  var url = 'http://api.server.com/election-data?format=json';
  var data = jQuery.ajax(url);
  var newRegions = getRegions(data);	
  container.innerHTML = newRegions.map(function(r){
    return r.render();
  }).join(''),
});

By essentially putting it in a loop that runs every 10 seconds, we could get the job done. But this method would hammer-ping the network and is incredibly inefficient. That would not be very functional. Instead, let's dig a little deeper into the Bacon.js library.

In Bacon, there are EventStreams and Properties parameters. Properties can be thought of as "magic" variables that change over time in response to events. They're not really magic because they still rely on a stream of events. The Property changes over time in relation to its EventStream.

The Bacon.js library has another trick up its sleeve. The Bacon.fromPromise() function is a way to emit events into a stream by using promises. And as of jQuery version 1.5.0, jQuery AJAX implements the promises interface. So all we need to do is write an AJAX search function that emits events when the asynchronous call is complete. Every time the promise is resolved, it calls the EvenStream's subscribers.

var url = 'http://api.server.com/election-data?format=json';
var eventStream = Bacon.fromPromise(jQuery.ajax(url));
var subscriber = eventStream.onValue(function(data){
  newRegions = getRegions(data);
  container.innerHTML = newRegions.map(function(r){
    return r.render();
  }).join(''),
}

A promise can be thought of as an eventual value; with the Bacon.js library, we can lazily wait on the eventual values.

Putting it all together

Now that we have the reactivity covered, we can finally play with some code.

We can modify the subscriber with chains of pure functions to do things such as adding up a total and filtering out unwanted results, and we do it all within onclick() handler functions for buttons that we create.

// create the eventStream out side of the functions
var eventStream = Bacon.onPromise(jQuery.ajax(url));
var subscribe = null;
var url = 'http://api.server.com/election-data?format=json';

// our un-modified subscriber
$('button#showAll').click(function() {
  var subscriber = eventStream.onValue(function(data) {
    var newRegions = getRegions(data).map(function(r) {
      return new Region(r.name, r.percent, r.parties);
    });
    container.innerHTML = newRegions.map(function(r) {
      return r.render();
    }).join(''),
  });
});

// a button for showing the total votes
$('button#showTotal').click(function() {
  var subscriber = eventStream.onValue(function(data) {
    var emptyRegion = new Region('empty', 0, [{
      name: 'Republican', votes: 0
    }, {
      name: 'Democrat', votes: 0
    }]);
    var totalRegions = getRegions(data).reduce(function(r1, r2) {
      newParties = r1.parties.map(function(x, i) {
      return {
        name: r1.parties[i].name,
        votes: r1.parties[i].votes + r2.parties[i].votes
      };
    });
    newRegion = new Region('Total', (r1.percent + r2.percent) / 2, newParties);
    return newRegion;
    }, emptyRegion);
    container.innerHTML = totalRegions.render();
  });
});

// a button for only displaying regions that are reporting > 50%
$('button#showMostlyReported').click(function() {
  var subscriber = eventStream.onValue(function(data) {
    var newRegions = getRegions(data).map(function(r) {
      if (r.percent > 50) return r;
      else return null;
    }).filter(function(r) {return r != null;});
    container.innerHTML = newRegions.map(function(r) {
      return r.render();
    }).join(''),
  });
});

The beauty of this is that, when users click between the buttons, the event stream doesn't change but the subscriber does, which makes it all work smoothly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.137.7