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.

\$\endgroup\$
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' ) )

\$\endgroup\$
  • \$\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\$ – Kos Apr 25 '14 at 7:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.