2
\$\begingroup\$

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:

(Live demo)

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:

  1. I implemented addInput in terms of a recursive helper function addLettersRecursive which still is overly complicated according to my gut feeling.
  2. 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:

  1. 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); });
    }
    
  2. 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.

asked Apr 23, 2014 at 11:32
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

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); });

  1. 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' ) )

  2. 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' ) )

  3. 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' ) )

answered Apr 23, 2014 at 12:53
\$\endgroup\$
1
  • \$\begingroup\$ Thanks! 1 crossed my mind but probably isn't conventional, for instance Q.delay() returns the promise immediately. 2 looks very similar to Function.bind in usage, and I think I'll stick with this approach for now. \$\endgroup\$ Commented Apr 25, 2014 at 7:38

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.