-
Notifications
You must be signed in to change notification settings - Fork 4
Difference from Promise
After reading introduction to Action
, now you must have one question, let's use the example from FAQ section:
new Action(function(cb){
readFile('fileA', function(err, data){
if (err){
cb(err);
}else{
cb(data);
}
});
})
.go(processOne)
.next(processTwo)
.go()
Above code will throw an Error
, because go
will not return a new Action
, thus it will break the chain, but what if we want to fire an Action
and return a new Action
so we can continue adding callbacks to the chain? Let's meet the Action.freeze
:
Action.freeze = function(action) {
var callbacks, data, pending;
// a state flag to mark if the action have returned
pending = true;
// a variable to hold the action data when action returned
data = void 0;
// during pending stage, all callbacks are saved to an Array
callbacks = [];
// Let's fire the missle
action._go(function(_data) {
var cb, j, len;
// when this callback are reached
// we mark the pending flag false save the data, and send data to callbacks
if (pending) {
data = _data;
pending = false;
for (j = 0, len = callbacks.length; j < len; j++) {
cb = callbacks[j];
cb(_data);
}
// release previous callbacks reference
return callbacks = void 0;
}
});
// Return a new `Action` immediately
return new Action(function(cb) {
if (pending) {
// during pending stage, we can't give cb the data they want
// so we save them, wait action returned
return callbacks.push(cb);
} else {
// after pending stage, we already have the data
// we directly give it to cb
return cb(data);
}
});
};
Action.freeze
fire the Action
it received, create an new Action
with two state, during pending stage, if this new Action
are fired, we save the callbacks without feeding them value, until pending finish and we get the value, then we feed the value to previous callbacks we saved, after pending finished, any further callbacks will be feed the same value.
In another word, Action.freeze
add Promise
semantics to an Action
.
fileAFreezed = Action.freeze(new Action(function(cb){
readFile('fileA', function(err, data){
if (err){
cb(err);
}else{
cb(data);
}
});
}))
// at this point, 'fileA' is already been reading
// before reading is complete, processA will be saved in an Array
fileAFreezed
.next(processA)
.go()
...
// once `fileA` are read, any further callbacks will get the same fileA immediately
fileAFreezed
.next(processB)
.go()
Why not add Action.prototype.freeze()
so we can chain it, you may ask. The answer is, this's where i think Promise
went wrong, most of the time we don't need to be that stict, creating internal state and arrays just to throw them away after using, use Action.freeze
will
-
Encourage user write more lazy code.
-
Make a memorized
Action
explicit.
From Action.freeze
, we get a big picture how Action
and Promise
differ, let's check another example from FAQ;
Action.retry = function(times, action) {
var a;
return a = action.guard(function(e) {
if (times-- !== 0) {
// see how we reuse a here
return a;
} else {
return new Error('RETRY_ERROR: Retry limit reached');
}
});
};
Action.retry(3, new Action(function(cb){
readFile('fileA', function(err, data){
if (err){
cb(err);
}else{
cb(data);
}
});
}))
.next(processA)
.guard(function(e){
if (e.message.indexOf('RETRY_ERROR') === 0)
console.log('Retry read fileA failed after 3 times');
})
.go()
The code above will retry readFile
fileA
at most 3 times, the retry
function recursively return Action
a
to perform the same action. Now consider how to do it using Promise
:
Promise.retry = function(times, promiseFn) {
var p = promiseFn()
while(times--){
// every time we failed, we use promiseFn to create a new Promise
p = p.catch(promiseFn);
}
return p.catch(function(e) {
throw new Error('RETRY_ERROR: Retry limit reached');
// or use Promise.reject
});
};
Promise.retry(3, function(){
return new Promise(function(resolve, reject){
readFile('fileA', function(err, data){
if (err){
reject(err);
}else{
resolve(data);
}
});
})
}
.then(processA)
.catch(function(e){
if (e.message.indexOf('RETRY_ERROR'))
console.log('Retry read fileA failed after 3 times');
})
Now it's clear, you can't retry a Promise
, since a Promise
just resolve once, you have no way to reuse it, so we use a PromiseFn()
to make a new Promise
everytime. This means every retry you have to creating a internal state, array, etc and throw them away when they finish, instead of focusing on the value produced in the future by action like Promise
, Action
focus on the action itself, then
provided a way to save callbacks before action finishes, while next
provided a way to compose a callback with previous continuation and produced a new continuation waiting for next callback.
That's why i seperate Action.freeze
, most of the time, you don't need such a strict behavior, all you want is building a callback chain with proper error handling. So with Action
:
-
You can run the
Action
when you want to, it won't be scheduled to nextTick. -
You can run any times you want, you can even return
Action
itself inside itsnext
to do recursive action. -
With
Action.freeze
, you get both of the world,Action
is strictly powerful thanPromise
because we can implementPromise
semantics on top ofAction
, but we can't do other way around.
Let's use a simple Promise
without reject
as an example from Q
var isPromise = function (value) {
return value && typeof value.then === "function";
};
var defer = function () {
var pending = [], value;
return {
resolve: function (_value) {
if (pending) {
value = ref(_value); // values wrapped in a promise
for (var i = 0, ii = pending.length; i < ii; i++) {
var callback = pending[i];
value.then(callback); // then called instead
}
pending = undefined;
}
},
promise: {
then: function (_callback) {
var result = defer();
// callback is wrapped so that its return
// value is captured and used to resolve the promise
// that "then" returns
var callback = function (value) {
result.resolve(_callback(value));
};
if (pending) {
pending.push(callback);
} else {
value.then(callback);
}
return result.promise;
}
}
};
};
var ref = function (value) {
if (value && typeof value.then === "function")
return value;
return {
then: function (callback) {
return ref(callback(value));
}
};
};
Without reject
, every time you create a Promise
, you create a internal varible to hold the resolve result, and an Array to hold callbacks.
That's the same cost when you call Action.freeze
.
When creating an Action
, you save the reference of the go
, that's all, creating Action
are cheaper.
Skip this part if it puzzles you.
In some FP languages continuation are used to express complex control structure, but they are not used to perform implicit parallel computations, consider following haskell values:
one :: Int
one = 1
oneCPS :: (Int -> a) -> a
oneCPS f = f 1
oneCPS (+1)
-- 2
Everytime we make something with type (a -> r) -> r
from something with type a
, we make a CPS transfrom, async function in javascript is exactly the same:
var file = readfileSync('data.txt')
var fileCPS = function(cb){
readfileSync('data.txt', cb);
}
fileCPS(function(data){
console.log(data);
})
In haskell use ConT
monad to wrap function with (a -> r) -> r
type, note how fmap
and >>=
works for ConT
:
instance Functor (ContT r m) where
fmap f m = ContT $ \ c -> runContT m (c . f)
instance Monad (ContT r m) where
return x = ContT ($ x)
m >>= k = ContT $ \ c -> runContT m (\ x -> runContT (k x) c)
It's exactly how Action.prototype.next
deal with a nest Action
:
Action.prototype._next = function(cb) {
var self = this;
return new Action(function(_cb) {
return self.action(function(data) {
var _data = cb(data);
if (_data instanceof Action) {
return _data._go(_cb);
} else {
return _cb(_data);
}
});
});
};
We just use instanceof
to dynamicly decide we want a fmap
or >>=
.
Since haskell use light weight thead to deal with parallel IO, the code oneCPS (+1)
above will not return before (+1)
finish, but consider above javascript functions:
fileCPS(function(data){
console.log(data);
})
fileCPS(...)
returned immediately, without running console.log
at all, and we only know console.log
will run sometime later, this is how parallel IO works in javascript, by keep a threading pool in background, a function doesn't have to return when finish.
Certainly this's behavior is simple to understand at first, it create a lot of more problems, while i borrow a lot idea from haskell, some combinators are especially tricky, for example, the Action.freeze
actually are js version of call/cc, since the (a -> r) -> r
function in javascript are async, Action.freeze
got a different semantics.
What's that semantics? Oh, it's just Promise
.