Comments:"Flowdock » Bacon.js Makes Functional Reactive Programming Sizzle"
URL:http://blog.flowdock.com/2013/01/22/functional-reactive-programming-with-bacon-js/
Like most other JavaScript developers, we’ve been in a callback hell. Functional Reactive Programming (FRP) is one, and in our opinion rather powerful, tool to improve the structure of your web app. This blog post introduces FRP concepts and benefits with Bacon.js, an FRP library for both browsers and server-side JS. We’ve been using Bacon.js in the new Flowdock.
Reactive programming is different from event-driven programming in the sense that it focuses on data flows and propagation of change instead of handling single events. For example, in an imperative world, there could be a statement that sums up two variables and produces a third variable. If either of the variables change, you wouldn’t expect the value of the result variable to change. If you’re using reactive building blocks, the result variable would always be the sum of the two variables. In this sense, reactive programming is almost like a spreadsheet.
FRP implements reactive programming with functional building blocks. Referential transparency is strictly enforced, which prohibits the use of variables. An alternative way has to be used to handle time-varying values. The main concepts in FRP are properties (not to be confused with JavaScript properties) and event streams. There are different names for these in different implementations. ‘Signal’ and ‘event’ are commonly used, but we’ll stick with Bacon.js nomenclature.
EventStreams are sources of events. These can be mouse clicks, keyboard events or any other input. The power of EventStreams is that they are composable. It is possible to use similar tools that are used to handle arrays for event streams: you can filter events, map event values to other values or reduce events to a property.
Property is used as an abstraction for a time-varying value. Properties are similar to EventStreams, but they also have the notion of current value. Most of the functionality is shared with EventStreams.
Streams and Properties in Bacon
Let’s try out this FRP thing by figuring out if the user is active on a web page. This is something we’ve had to do in Flowdock to determine what kind of notifications we should play. For example, we don’t want to bother users with sound notifications when they are participating actively in the current discussion.
The easiest thing that could work is to use focus
and blur
events. A pretty standard approach to this, using jQuery would be
var focused =true;
$(window).on('blur',function(){ focused =false;});
$(window).on('focus',function(){ focused =true;});
Not that bad, but how about the FRP approach then?
varblur= $(window).asEventStream('blur').map(function(){
returnfalse;
});
varfocus= $(window).asEventStream('focus').map(function(){
returntrue;
});
var focused =focus.merge(blur).toProperty(true);
Two Bacon.js EventStreams are defined: blur that emits false
s and focus that emits true
s. Then the streams are combined using using merge
and converted to a Property that captures the value of focus. true
is defined as initial value. If you want to inspect the value, a listener to the Property can be bound using onValue
focused.onValue(function(focus){ console.log('focus',focus);});
There’s one gotcha: Bacon.js is lazy. Before the stream or property has any subscribers, it won’t do anything. The current value of the property won’t update and event listeners aren’t bound to the DOM. This can be very useful since it means creating streams causes zero overhead.
Handling state with switching streams
The focus/blur idea to handle activity state is usually not good enough. The focus and blur events aren’t always reliable and we won’t have any idea if the user has left the computer three hours ago.
How about using keyboard and mouse activity then? Let’s say that users are active when they have moved the mouse pointer or pressed some key within the last 30 seconds.
For comparison, this could be implemented in plain old jQuery as follows
var timeout =null;
function setInactive(){
active =false;
}
$(window).on('keydown mousemove',function(){
active =true;
clearTimeout(timeout);
timeout = setTimeout(setInactive,30000);
})
A Bacon EventStream of user activities is a good start.
var activity = $(window).asEventStream('mousemove keydown')
The stream only contains events of the positive case. Negative case requires you to detect when there is no activity. Here’s where stream switching becomes useful.
var active = activity.flatMapLatest(function(event){
return Bacon.once(true).merge(Bacon.later(30000,false));
}).toProperty(true);
Here stream switching is used to implement state in a functional world. In Bacon, flatMap
maps each event in the stream to a new stream of events. flatMapLatest
works like flatMap
, except that only events from the latest spawned stream are included. So, the value is true for 30 seconds after the last user interaction.
This isn’t the only, or even the simplest, way to implement such an activity property. Bacon throttle
could be used to only emit events after a quiet period.
activity.map(function(){returntrue;}).merge(
activity.throttle(30000).map(function(){returnfalse;})
).toProperty(true);
If this is compared to the event-driven solution, you’ll see that the FRP solution looks more desirable. There are less moving parts and the code is more focused on the logic rather than implementation details.
Composing Properties
The activity based solution might be more resilient than using window blur and focus events, but blur is still useful. Since focus is typically reliable, maybe it could be improved by combining the two solutions: users are actively viewing the page when the window is focused and they have moved their mouse in the past 30 seconds. Luckily, composing properties in Bacon is extremely easy.
focused.combine(active,function(focus, active){
returnfocus&& active;
})
Even better, there are methods to combine properties with basic boolean operators.
The jQuery implementation is left as an exercise for the reader.
Decoupling apps with Bacon
There are other data sources that we could use for user activity monitoring. The Page Visibility API is a more reliable alternative for focus
and blur
. If this API is available in the browser, we can use an implementation based on that instead.
Bacon makes it easy to switch out parts of the implementation when constructing EventStreams and Properties. Let’s say that we’ve implemented a Bacon property from Page Visibility state as visibility
.
if(document.hidden=== undefined){
return visibility;
else{
return focused;
}
}
function activity(focus){
var stream = $(window).asEventStream('mousemove keydown');
return stream.flatMapLatest(function(event){
return Bacon.once(true).merge(Bacon.later(30000,false));
}).toProperty(true).and(focus);
}
activity(windowFocus());
Decoupling the stream and application logic also makes it easier to test components that depend on the activity. You can give the state as argument to other components and don’t need to write complex objects to encapsulate the state.
On the Road to a Bypass Surgery
At Flowdock, we’ve been using Bacon to handle all kinds of things: autocompleting UI components with asynchronous results, synchronising Backbone models over Socket.IO and handling keyboard shortcuts.
This blog post didn’t cover much of the Bacon.js API. The Bacon.js readme is the most comprehensive source of documentation. The Bacon.js wiki has also excellent visualisations of the few important methods and their implications.