-
Notifications
You must be signed in to change notification settings - Fork 0
AboutBoxons
Boxons track the outcome of asynchronous function calls, either nodejs callbacks or promises. Boxons make callbacks and promises easier to use and easier to mix together.
To track the outcome of a nodejs style callback, use a boxon where a callback was previously used: var b = Boxon( cb ); do_async( b );
instead of do_async( cb );
. Once a boxon is created and provided to some asynchronous function, that boxon will remember what parameters were used to call it. These parameters are the "outcome" of the async function, pretty much like "returned value xor exception" is the outcome of a sync function.
To track the outcome of a promise, create a boxon from that promise: var b = Boxon.cast( a_promise );
.
The outcome gets delivered either when the boxon is called by the nodejs style async function or when the promise gets resolved or rejected.
Once the outcome is delivered, use var outcome = b();
to retrieve it. This will throw an exception if the outcome was an error. If retrieval is attempted before the outcome was delivered, the outcome is forever set to undefined
. To avoid that, use b( function(){ ... } );
to attach code to run when the outcome is delivered.
npm install l8
ls node_modules/l8/lib/boxon.js
# tests, using mocha
mocha test/boxon.js -R spec
Boxons are defined in file l8/lib/boxons.js, about 200 LOC. There are no external dependencies. To use boxons, please HTML include script boxon.js or var Boxon = require( "l8/lib/boxon.js" );
.
var read = Boxon(); fs.readFile( "test.txt", read );
// or var read = Boxon( fs, fs.readFile, "test.txt" );
// or var read = Boxon.cast( fs.readFile, "test.txt" );
read( function( err, content ){
// ...
});
Using b( cb )
an outcome delivery callback can be attached to a boxon at any time: when a boxon is created, before a boxon is delivered or after a boxon was delivered. Contrary to promises, call to the callback is synchronous whenever possible. This provides a speed advantage but dealing with two cases, immediate call or deferred call, can be error prone.
Advanced: using b( cb, ctx );
the ctx
parameter defines what this
will be when cb
is called ; it defaults to the boxon itself when not provided. When ctx
is provided, it is still possible to retrieve the current boxon using Boxon.current
.
If one or more callbacks are attached to a boxon after that boxon was delivered, they are called immediately, using the boxon's delivered outcome (it was memorized).
To specify the async function either call directly that function with the boxon as last parameter, ie asyncRead( p1, p2, a_boxon )
or tell the boxon about what function to call using syntax a_boxon( target, function [, ...arguments] )
. In that latter case, the specified function will be applied on the specified target using the specified arguments plus the boxon itself. Note: if the function raises an exception right away, instead of invoking the callback with an error, that exception is re-raised immediately too. Note: please use syntax var b = Boxon.cast( function, [, ...arguments] );
when the target is null. Anything matching b( null, [...args] );
would actually set the boxon's outcome and b( null, function [, ...arguments] );
does match that pattern.
var read_test = Boxon();
fs.readFile( "test.txt", read_test );
... much later maybe ...
try{
var content = read_test();
...
}catch( err ){
...
}
To retrieve the outcome of a boxon, simply call the boxon with no parameters, var result = a_boxon();
.
If no error outcome was delivered, it is the normal outcome that is returned, as an array in cases where multiple results were delivered. If an error outcome was delivered, an exception is thrown.
If outcome retrieval is attempted before the boxon was delivered, undefined
is returned and from now on this will be the delivered outcome (functional style). To avoid that and retrieve the outcome only after it is eventually delivered, please use a_boxon( function( err, result ){ ... } );
.
var r1 = Boxon();
var r2 = Boxon();
var r3 = Boxon( null, "third" );
var with_all = Boxon.all( [ r1, r2, r3, "forth" ] );
with_all( function( err, a ){
if( !a ){
console.log( "Timeout" );
return;
});
console.log( r1(), r2() );
// => first, second, third, forth
console.log( a[0](), a[1]() );
// => first, second, third, forth
});
setTimeout( with_all, 1000 );
r2( null, "second" );
r1( null, "first" );
Using Boxon.all( an_array )
one can collect the outcome of multiple boxons into a new boxon. As a convenience the array can contain boxons, thenables or plain values. The outcome of the new boxon is an array of delivered boxons. Note: if the array is empty, an empty array is immediately delivered.
var r1 = Boxon();
var r2 = Boxon();
var to = Boxon.cast( setTimeout, 1000 );
Boxon.race( [ to, r1, r2 ] )( function( _, winner ){
if( winner === to ){
console.log( "Timeout!" );
}else{
console.log( winner() );
}
// => second
));
r2( null, "second" );
r1( null, "first" );
With Boxon.race( an_array )
, the outcome of the new boxon is the first delivered boxon. Note: if the array is empty, no outcome is delivered.
var b = Boxon.cast( a_promise ); // Create a boxon from a promise
b( function( err, rslt ){ ... } ); // Attach a callback to it
xx && b( new Error( "Bang" ) ); // Maybe set the outcome, an error
setTimeout( function(){
try{
console.log( "Success: " + b() ); // Retrieve the outcome
}catch( err ){
console.log( "Error: " + err );
// => outputs Bang maybe or the promise rejection reason
}
}, 1000 );
With promises, there are two different callbacks to handle, one for normal results, one for errors. Using a boxon, a promise can be handled using a single callback with a nodejs style signature f( err [, ...result] )
.
var MyBoxon = Boxon.scope( Promise ); // Any ECMAScript 6 compatible Promise factory
var b = MyBoxon();
b.then( function( m ){ console.log( m + " Hello" ); } );
b.then( function( m ){ console.log( m + " World" ); } );
b( null, "Boxon" ); // null => no error
// => outputs Boxon Hello and then Boxon World
When in need of some of the extra power of promises (chaining, multiple callbacks, asynchronous delivery, exception handling... ), simply upgrade boxons using your favorite ECMAScript 6 compatible Promise factory: var MyBoxon = Boxon.scope( Promise );
. Boxons created using the new boxon factory will have .then()
method that they delegate to a promise.
Advanced: Despite the fact that the promise delegate is created "on the fly" the first time .then()
is called, there is however a small overhead with promise enhanced boxons. If ultimate speed is required, avoid promises and consider using "plain" boxons: PlainBoxon = Boxon.scope().plain()
. A "plain" boxon hides the fact that it is a boxon, it does not contain a .boxon
property, that makes it slightly lighter. As a consequence for the lack of .boxon
duck typing cannot detect the boxon anymore. Note: this can be a desirable feature in cases where boxons need to be processed as plain values using Boxon.all()
or similar methods that normally dereferences boxons in order to get their outcome. This solution to avoid dereferencing also works with plain boxons created to track promises.
Advanced: the callback to retrieve the outcome of a boxon is called synchronously if the outcome was delivered before the callback is attached and called asynchronously if the outcome is delivered after the callback was attached. The existence of two cases (mix mode) is error prone and when designing an async API one should consider specifying how callbacks are called, either always sync or always async. Fortunately there is a solution to make an always async callback boxon. It involves using the always async .then()
method of promises. Basically: create a boxon from a promise created from a mix mode boxon: var always_async_b = Boxon.cast( Promise.cast( a_boxon ) );
.
var content1 = Boxon(), content2 = Boxon();
content1( fs, fs.readFile, "test1.txt" )( function( err ){
err ? content2() : content2( fs, fs.readFile, "test2.txt" );
});
content2( function(){
try{
console.log( content1() + content2() );
}catch( err ){
console.log( "Error", err );
}
});
versus
try{
var content1 = fs.readFileSync( "test1.txt" );
var content2 = fs.readFileSync( "test2.txt" );
console.log( content1 + content2 );
}catch( err ){
console.log( "Error", err );
}
This style is the closer you can get to a synchronous solution using boxons and nodejs style async functions. Almost the same number of lines but some unfortunate noise. Maybe not as bad as Callback Hell but you can get a somehow better solution using promises with boxons.
var content1 = Boxon(), content2 = Boxon();
content1( fs, fs.readFile, "test1.txt" )
.then( function(){ return
content2( fs, fs.readFile, "test2.txt" ); })
.then( function(){
console.log( content1() + content2() );
}).catch( function( err ){
console.log( "Error", err );
});
versus, without boxons:
var content1, content2;
Promise( function(s,f){
fs.readFile( "test1.txt", function(e,r){e?f(e):s(r)} )
}).then( function( ok ){ content1 = ok;
return Promise( function(s,f){
fs.readFile( "test2.txt", function(e,r){e?f(e):s(r)} )
}).then( function( ok ){ content2 = ok;
console.log( content1 + content2 );
}).catch( function( err ){
console.log( "Error", err );
});
Using promises and boxons, this is the closer you can get to sync code. It's not perfect but it's shorter/simplier than the solution without boxons. There is a simplier solution, using generators, where available.
co( function*(){
try{
var content1 = yield Boxon( fs, fs.readFile, "test1.txt" );
var content2 = yield Boxon( fs, fs.readFile; "test2.txt" );
console.log( content1 + content2 );
}catch( err ){
console.log( "Error", err );
}
})();
Because boxons are "thunks", using co generator based solution leads to code that is very similar to the sync code.
To make a boxon from a "thunk", please use var b = Boxon.co( a_thunk );
. To make a "thunk" from a boxon, you don't need to do anything because boxons are thunks already.
function Moxon(){
var queue = [];
return Boxon( function( err, rslt ){
if( err && err.Boxon )return queue.push( err );
for( var cb, ii = 0 ; cb = queue[ ii++ ] ; ) cb.on.apply( cb.context, arguments );
}));
}
...
var q = Moxon();
q( cb1 );
q( cb2 );
q( null, "Fire!" );
Only one callback can be attached before a boxon is delivered. If more than one callback is attached before the boxon is delivered, the first attached callback is called with an error: { Boxon: b, on: ff, context: cc }
. This mechanism makes it possible to manage multiple callbacks if so desired (as promises do when p.then(...)
is called multiple times for example) without any overhead when you don't need that feature.
Once a boxon is delivered, multiple callbacks are possible because callbacks are called synchronously, hence they don't need to be queued.
Please note that if a boxon is called to deliver an outcome multiple times, only the first time matters: subsequent calls are ignored. As a result, the outcome, once set/delivered, will never change (much like promises do). This is similar to variables in pure functional languages where a variable can be assigned a value once only. This has for consequence the fact that callbacks are called at most once only.
As a convenience a slightly more complete Boxon.Moxon()
is in the API. Except for the handling of multiple callbacks, it works exactly like Boxon()
.
This specification defines the minimal interop protocol for boxon friendly modules. It makes it possible to mix boxons and promises produced by different implementations using Boxon.cast()
. Please note that the minimum requirements for .boxon()
are very similar to the ones for .then()
in the context of Promise.cast()
.
-
a "boxon" is something with a callable
.boxon
property. -
to deliver a boxon, call that property with either an error or null (or anything falsy) and optional results. If the implementation refuses to deliver the boxon, it may raise an exception.
-
to retrieve the delivered outcome, call the property with a nodejs style callback: if the outcome was already delivered, that callback may be called immediately, else it will be called when the outcome gets delivered. Restrictions: 1) the outcome shall never change once set. 2) support for multiple pending callbacks is implementation dependant, ie don't depend on it.
-
a "boxon factory" is an object that provides a
.cast( boxon )
function that imports a boxon created by some other factory and returns a new boxon (unless the original boxon comes from the same implementation, in which case that boxon can be returned as is). It must also be able to import a thenable like ECMAScript 6Promise.cast( thenable )
does and convert it into a boxon. -
a "boxon friendly promise factory" is an ECMAScript 6 Promise compatible factory that provides a
.cast( x )
function that in addition to a thenable can also import a boxon created by some other factory, it then returns a new promise.
This is the minimal specification to enable interop. l8's implementation of boxons provides additional services, other implementations may provide different services.
There is a tension in the javascript community about what scheme to use to handle asynchronous calls. Some argue that callbacks are ok and promises are overkill. Some argue that callbacks are error prone and promises make them manageable. Others argue that neither are good enough and promote generators. Fibers are nice too sometimes, but threads are better, but threads race and deadlock... It's complicated.
Callbacks are good
- fast
- memory efficient
- f( err [, ...result ) is becoming ubiquitous, de facto standard
- easy access to previous outcomes in enclosing closures
but
- callback hell!
- procedural
- not try/catch friendly
Promises are nice
- functional
- chainable
- multiple listeners
- exception handling and propagation
but
- asynchronous delivery is somehow expensive
- so is memory
- f( err ) and f( result ) splits the outcome in two
- access to previous outcomes in chain is not always easy
- not try/catch friendly
- requires promisification of f( err [, ...r] ) async functions
Boxons are promise friendly callbacks
- functional
- small memory overhead
- synchronous fast delivery
- f( err [, ...result ] ) de facto standard compliant
- direct interop with existing async functions
- easy resolution: a_boxon( err ) or a_boxon( null, r... )
- easy access to previous outcomes: a_boxon()
- try/catch friendly to some extend
- can track a promise as well as a callback
- can turn callbacks into promises easely
- embrace & extend promises when provided a Promise factory
- visionmedia's co thunk compatible
but promises or co() style generators are still usefull in some cases because
- immediate sync xor deferred async delivery is error prone
- Callback Hell, boxons are not chainable
- scope of try/catch is limited to a single step, no propagation
The name "boxon" is the concatenation of "box" and "on" because the thing, initially empty, may be filled to contain something, as does a "box", and a callback can be attached "on" that event. "outcome" might have been a better name but overloading an existing name is often problematic.
If the conciseness of boxons pleases you, you may want to have a look at their slighly heavier Parole cousin. It combines features of boxons, promises, steps and much more.
Until an API Reference page for boxons is created, please read boxon.js, it's barely 200 lines of code.