Saturday, December 21, 2013

Bacon.js 0.7.0 is out!

This is the stuff I've been slowly working on in my spare time. I'm so glad I finally got this version released, since it's been in a kind of a 99% ready state for quite a long time, and I absolutely hate to have any "almost ready" code rotting in a codebase. Gotta ship it! 
And the timing is of course perfect too, so you can consider this my $holiday_occurring_in_late_december present to you.

Glitch-free FRP #272

The main goal of the 0.7.0 release is to make FRP with Bacon.js essentially glitch-free. And this is the most invasive change into Bacon.js internals for quite a while. Also something that I many times thought impossible to solve with the brain capacity available to me.
Let's start with the problem that I'm trying to solve here. A glitch in FRP is a temporary violation of an invariant that, with bacon.js at least, occurs when simultaneous events are involved. And just to be precise, my definition for simultaneous events is events that have the same root event. So, for instance, a stream a and its descendant a.map((x) -> x * 2) produce simultaneous events by this definition, while a = Bacon.later(1) and b = Bacon.later(1) do not.
The exec summary of this improvement is that 0.7.0 is more reliable.
But if you're intrested in the details, read on! For instance, if you use Bacon.js 0.6.x and run this
a = Bacon.sequentially(10, [1, 2])
b = a.map((x) -> x * 2)
c = Bacon.combineAsArray([a, b])
c.log()
You'll get
[ 1, 2 ]
[ 2, 2 ]
[ 2, 4 ]
where the pair [2,2] is a glitch that occurs because c is updated first by the changed value of a and the the changed value ofb. In glitch-free FRP you should get just
[ 1, 2 ]
[ 2, 4 ]
Since 0.4.0, Bacon.js has featured "atomic updates", but that has beed quite limited, because they only apply to a graph of Properties. The above example fails in pre-0.7.0 because of this limitation. In this example, the problem could be worked-around by making a a property as in
a = Bacon.sequentially(10, [1, 2]).toProperty()
But there are more complex cases that are harder to work-around. Like if you have a complex graph of Properties that you combine using combineWith(a, b, f), the pre-0.7.0 system doesn't guarantee that your combinator function f won't be called for some invalid intermediate pairs of values when the sources a and b occur simultaneously.
And then there are takeUntil and skipUntil. Previously the functioning of these combinators has depended on subscription order. For example, the correct functioning of a.takeUntil(b) in case of simultaneous events from sources a and b has depended on that the event from b gets in first. And it did in most cases but not always. There's a new test case (look for the word "evil") that fails for pre-0.7.0 versions.
How did I solve this? Simply using a dependency-graph. Now all Observables have a list of "deps". For instance, if you run
Bacon = require("../src/Bacon")

a = Bacon.constant("a")
b = Bacon.constant("b")
c = Bacon.combineAsArray([a, b])

console.log("deps of c:", c.deps().map((s) -> s.toString()))
You'll get
deps of c: [ 'Bacon.constant(a)', 'Bacon.constant(b)' ]
So now, using this new dependency information, it's possible to hold the output of any combined Observable until all of its deps are ready. And that's how it works. Well, except when you add a new subscriber while bacon.js is currently flushing events. To make that work flawlessly too, I had to shave another yak. But it's shaved now and all is well.

Observable.toString #265

As you might have spotted above, there's now a rather nice toString method for all Observables. The output of this method resembles very closely to the code that was used to create the Observable. For instance
Bacon.combineTemplate({name:Bacon.constant("Bacon"), version: Bacon.constant("0.7")}).toString()
will output
Bacon.combineTemplate({name:Bacon.constant(Bacon),version:Bacon.constant(0.7)})'
Also, I've added a name method as @rassie suggested in #273 and implemented .inspect() as an alias to .toString() so that Node.js will show your Observables in a nice manner in the console.
So now you can do this in Node.js.
> B.once("1").name("one")
one
> B.once(1).name("stuff").take(1)
stuff.take(1)

Support for analysis/debugging/profiling tools #273

These are some experimental features that I included in 0.7.0 for preview.
I'm talking about improvements that will enable the implementation of development tools that can, for instance, visualize the event network and event flow going on. The improvements include
observable.deps()
Returns an array of dependencies that the Observable has. For instance, for a.map(function() {}), the deps would return [a]. This method returns the "visible" dependencies only, skipping internal details. This method is thus suitable for visualization tools.
Internally, many combinator functions depend on other combinators to create intermediate Observables that the result will actually depend on. The deps method will skip these internal dependencies.
observable.toString()
Returns a nice textual presentation of the stream. Covered in #265.
observable.internalDeps()
Returns the true dependencies of the observable, including the intermediate "hidden" Observables. This method is for Bacon.js internal purposes but could be useful for debugging/analysis tools as well.
observable.desc()
Returns a structured version of what toString returns. The structured description is an object that contains the fields context,method and args. For example, for Bacon.fromArray([1,2,3]).desc() you'd get
{ context: Bacon, method: "fromArray", args: [[1,2,3]] }
Bacon.spy(f)
Adds your function as a "spy" that will get notified on all new Observables. This will allow a visualization/analytis tool to spy on all Bacon activity. Current implementation just calls your function, but maybe, it should allow you to wrap all created Observables in your own wrapper to make spying even easier? This would enable AOFRP (aspect oriented functional reactive programming) lol.
All of this stuff is included in 0.7.0 but only as experimental features subject to change later. The problem is that spy alone is not enough for visualizing the event network. To observe the lifecycle and events passing through all the Observables, we need to add a way to observe without forcing a subscription. Currently, all you can do is to call subscribe to watch all the events, but that will also prevent the stream from being disposed when it has no "real" subscribers. Nasty details to be be found in #273.
So, even though my intention was to "officially" include #273 into 0.7.0 too, it proved a bit more involved than it seemed at first, so we'll get back to it later.
Anyways, enjoy the new version of Bacon.js and please let me know what you think!
Cheers!
@raimohanska

Wednesday, December 4, 2013

Wrapping Things in Bacon

I've got a lot of questions along the lines of "can I integrate Bacon with X". And the answer is, of course, Yes You Can. Assuming X is something with a Javascript API that is. In fact, for many values of X, there are ready-made solutions.
  • JQuery events are supported out-of-the box
  • With Bacon.JQuery you get more, including AJAX and two-way bindings
  • Node.js style callbacks, with (error, result) parameters, are supported with Bacon.fromNodeCallback
  • General unary callbacks are supported with Bacon.fromCallback
  • There's probably a plenty of wrappers I haven't listed here. Let's put them on the Wiki, shall we?
In case X doesn't fall into any of these categories, you may have to roll your own. And that's not hard either. UsingBacon.fromBinder, you should be able to plug into any data source quite easily. In this blog posting, I'll show some examples of just that.
You might want to take a look at Bacon.js readme for documentation and reference.

Example 1: Timer

Let's start with a simple example. Suppose you want to create a stream that produces timestamp events each second. Easy!
Using Bacon.interval, you'll get a stream that constantly produces a value. Then you can use map to convert the values into timestamps.
Bacon.interval(1000).map(function() { return new Date().getTime() })
Using Bacon.fromPoll, you can have Bacon call your function each 1000 milliseconds, and produce the current timestamp on each call.
Bacon.fromPoll(1000, function() { return new Date().getTime() })
So, clearly Using Bacon.fromBinder is an overkill here, but if you want to learn to roll your own streams, this might be a nice example:
var timer = Bacon.fromBinder(function(sink) {
    var id = setInterval(function() {
        sink(new Date().getTime())
    }, 1000)
    return function() {
        clearInterval(id)
    }
})
timer.take(5).onValue(function(value) {
    $("#events").append($("<li>").text(value))
})
So,
  • you call Bacon.fromBinder and you provide your own "subscribe" function
  • there there you register to your underlying data source. In the example, setInterval.
  • when you get data from your data source, you push it to the provided "sink" function. In the example, you push the current timestamp
  • from your "subscribe" function you return another function that cleans up. In this example, you'll call clearInterval

Example 2: Hammer.js

Hammer.js is a library for handling multi-touch gesture events. Just to prove my point, I created a fiddle where I introduce a "hammerStream" function that wraps any Hammer.js event into an EventStream:
function hammerStream(element, event) {
    return Bacon.fromBinder(function(sink) {
        function push() {
            sink("hammer time!")
        }
        Hammer(element).on(event, push)
        return function() {
            Hammer(element).off(event, push)
        }
    })
}
It's exactly the same thing as with the above example. In my "subscribe" function, I register an event handler to Hammer.js. In this event handler I push a value "hammer time!" to the stream. I return a function that will de-register the hammer event handler.

More examples

You're not probably surprised at the fact that all the included wrappers and generators (including $.asEventStream,Bacon.fromNodeCallbackBacon.intervalBacon.fromPoll etc) are implemented on top of Bacon.fromBinder. So, for more examples, just dive into the Bacon.js codebase itself.