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

4 comments:

  1. I am curious to know how does Bacon.js compare with the new fangled Facebook React?

    ReplyDelete
    Replies
    1. I haven't any practical experience with React so far, so cannot say much. Nevertheless, React is an UI library while Bacon.js is an FRP library, so they are not really solving the same problem. I'm sure you can use both in the same application, if that's your cup of tea. Maybe use React for UI components and route events between them and the backend using FRP.

      Delete
    2. I don't think React offers any novel way to manage intricate interaction state, so Bacon would still be useful at the UI with React. Since React generates synthetic events (separate from the DOM), Bacon would probably need to hook into those events. Also, after flowing through Bacon's event streams or properties, the eventual UI updates would need to end with React pseudo-DOM updates. I think.

      Delete
  2. Awesome news about glitch elimination! Congrats.

    ReplyDelete