Inspired by this question on Stack Overflow, I've attempted to code such animation, mostly to get some more practice with async, promises and Q.js:
function addOutput(s) {
$('<div>').text(s).appendTo(wnd);
//return Q.defer().promise;
return Q.delay(100).then(function() { return addPrompt(); });
}
function addInput(s) {
var l = $('.prompt:last');
return addLettersRecursive(l, s);
}
function addPrompt() {
var prompt = "kos@codepen % ";
var l = $('<div>').text(prompt).addClass('prompt').appendTo(wnd);
return Q.delay(900);
}
function addLettersRecursive(container, s) {
container.append(s.charAt(0)); // dangerous :(
var row_complete = Q.defer();
Q.delay(100).then(function() {
if (s.length <= 1) {
Q.delay(300).then(function() {
row_complete.resolve();
});
}
addLettersRecursive(container, s.substr(1)).then(function() {
row_complete.resolve();
})
});
return row_complete.promise;
}
// Usage
addPrompt(">>> ")
.then(function() { return addInput("whoami"); })
.then(function() { return addOutput("kos"); })
.then(function() { return addInput("uname -a"); })
.then(function() { return addOutput("Javascript in codepen.io, powered by Q.js"); })
.then(function() { return addInput("raz dwa"); })
.then(function() { return addOutput("zsh: command not found: raz"); })
.then(function() { return addInput("trzy cztery"); })
.then(function() { return addOutput("zsh: command not found: trzy"); })
.done();
I'm not really satisfied by this implementation, though. How can I simplify it? Here are the specific concerns I have:
- I implemented
addInput
in terms of a recursive helper functionaddLettersRecursive
which still is overly complicated according to my gut feeling. - There's this
.then(function() { return function_that_returns_promise(args); });
pattern all over, which seems like a very verbose way of chaining behaviour.
What I have tried:
I attempted to unwind the recursion by stacking the promises one on top of another in a loop, which was a bit tricky to implement because of the selective closure involved. There's some improvement but still looks messy:
function addInput(s) { var container = $('.prompt:last'); var d = Q.delay(0); for (var i=0; i<s.length; ++i) { d = d.then(function(i) { return function() { // pass i by value container.append(s.charAt(i)); return Q.delay(100); }}(i)); } return d.then(function() { return Q.delay(300); }); }
I'm not sure if this kind of pattern is typical to promise-driven code, or I'm just doing something sub-optimally. One thing that came to my mind is this substitution:
/* before */ .then(function() { return addInput("whoami"); }) /* after */ .then(addInput.bind(null, "whoami")) /* before */ return d.then(function() { return Q.delay(300); }); /* after */ return d.then(Q.delay.bind(Q, 300));
Is it a good track?
Generally I haven't seen much "real life" uses of promises yet except the book cases, so if anything else looks out of the ordinary or sub-optimal, please raise it.
1 Answer 1
This stuff is hard to get right, at this point good promise driven code seems more like an art than engineering.
From a CodeReview perspective, your code is fairly easy to follow, except for one thing which got me stumped for a little while, row_complete
should really be named char_complete
, and then suddenly addLettersRecursive
will make more sense.
There are at least 3 additional ways you can deal with .then(function() { return function_that_returns_promise(args); });
Change your functions ( like
addInput
) to return a function, like this:function addInput(s) { return function addInputWrapper(){ var l = $('.prompt:last'); return addLettersRecursive(l, s); } }
Then you can call simply
.then( addInput( 'whoami' ) )
Create a generic wrapper function like this:
function wrap( f ){ var args = Array.prototype.slice.call(arguments,1); return function wrapper(){ return f.apply( this, args ); } }
Then you can call
.then( wrap( addInput, 'whoami' ) )
Generate a
lambdafy
function like this :function lambdafy( f ) { var lambda = function(){ var args = Array.prototype.slice.call(arguments); return function(){ f.apply( this , args ); } } return lambda; }
Then you can
addInput = lambdafy( addInput )
and.then( addInput( 'whoami' ) )
-
\$\begingroup\$ Thanks! 1 crossed my mind but probably isn't conventional, for instance
Q.delay()
returns the promise immediately. 2 looks very similar toFunction.bind
in usage, and I think I'll stick with this approach for now. \$\endgroup\$Kos– Kos2014年04月25日 07:38:47 +00:00Commented Apr 25, 2014 at 7:38
Explore related questions
See similar questions with these tags.