5

I'm writing some node.js to interact with sensors over a serial port connection. The code for reading the sensor is asynchronous, naturally. In my control code, though, I need to read a sensor, do something based on the value, read again, do something else, etc. To do this, I'm using code like the following self-contained test:

var main = new Main();
main.next();

function* Main()
{
  var reading = yield readSensor(this.next.bind(this));
  console.log(reading);

  var reading = yield readSensor(this.next.bind(this));
  console.log(reading);
}

function readSensor(callback)
{
  // simulate asynchrounous callback from reading sensor
  setTimeout(function sensorCallback() { callback('foo'); }, 100);
}

So, my sequential control code is in a generator which yields to readSensor() when it needs to get a reading. When the sensor reading is done, it calls the callback, and control returns to the main code. I'm doing it this way because I may need to read from various sensors in different orders depending on previous readings. So, here's the questionable part: I pass this.next.bind(this) as a callback to the asynchronous read function. The code seems to work when generators are enabled (--harmony_generators), but I am wondering if there are pitfalls here that I am missing. I'm relatively new to JS, so don't be afraid to point out the obvious :)

tyapo
  • 120
  • 2
  • 8
  • 1
    I didn't think you could call an ES6-generator like a constructor. Maybe it's a v8 bug? Let me check this. – Bergi Mar 10 '15 at 21:28
  • @Bergi I looked at the latest ES6 draft spec, but couldn't decide if this was supported or not. I think it should be; it would be a lightweight, relatively clean way to deal with the nested callback issue. Of course, even if you can't construct generators as objects so that they have a `this`, you could send them their "self" with an initial `main.next(main);` call, received with an initial `yield`. But, for now, the Q.async library seems to do what I'm after. – tyapo Mar 11 '15 at 12:12
  • I've studied the ES6 draft in detail now and can confirm my suspicion. See my answer :-) – Bergi Mar 11 '15 at 13:13

2 Answers2

2

I haven't studied ES6 generators in depth, but having a generator pass its own .next to another function as a callback doesn't sit well with me. If anything, it could create a situation where readSensor fails and you have no way to handle the failure, ending up in a deadlock.

I suggest modifying or wrapping readSensor to return a promise, and then using the technique outlined in this article.

That would allow you to write code like this (verified working in Node v0.12.0):

var Promise = require('q');

var main = async(function* () {
    var reading = yield readSensor();
    console.log(reading);

    reading = yield readSensor();
    console.log(reading);
});

main();

function readSensor() {
    return Promise.delay(2000).thenResolve(Math.random() * 100);
}



/***********************************************************
 * From here down,                                         *
 * boilerplate  async() function from article linked above *
 ***********************************************************/

function async(makeGenerator){
  return function () {
    var generator = makeGenerator.apply(this, arguments);

    function handle(result){
      // result => { done: [Boolean], value: [Object] }
      if (result.done) return Promise.resolve(result.value);

      return Promise.resolve(result.value).then(function (res){
        return handle(generator.next(res));
      }, function (err){
        return handle(generator.throw(err));
      });
    }

    try {
      return handle(generator.next());
    } catch (ex) {
      return Promise.reject(ex);
    }
  }
}

As loganfsmyth notes below, Q already provides a Q.async() method that provides the functionality of this async() function, and possibly other promise libraries do as well.

JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • Completely out of the main question, what's the purpose of `*` in this context? Having a function referenced by memory address? I've never saw that in JS. – Vadorequest Mar 10 '15 at 21:23
  • 2
    @Vadorequest It denotates a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*). – JLRishe Mar 10 '15 at 21:24
  • Thanks, I've read about ES6 features a few months ago, completely forgot that though! It's relatively new. – Vadorequest Mar 10 '15 at 21:27
  • Thanks. I looked at Q.async briefly while researching the issue, but was scared off by the warnings about the examples. Of course, the same warnings would apply to anything using generators at this point, I guess. I will probably adopt this method. I guess as a long-time C(++) programmer, I'm just shocked at how I need to use a library to get pretty basic control flow. Then again, moving from JS to C(++), I could imagine being shocked that I needed to deal with semaphores, mutexes, and the like just to keep my code from clobbering itself. – tyapo Mar 11 '15 at 11:52
  • @tyapo Running parallel asynchronous operations on a single thread with synchronous code-like syntax isn't really what I'd call "pretty basic control flow". It's true that you have to jump through some hoops in order to do things that are a matter of course in languages that do this stuff with blocking code, but the net result is much better. The `async()` function reproduced in its entirety above is all you need to get this magic async/await syntax with generator functions, and you should be able to combine that with any promise library or presumably built-in promises once they're ready. – JLRishe Mar 11 '15 at 12:20
  • @JLRishe Don't get me wrong, I'm not knocking the language here, just getting used to it. Your answer is exactly what I need to solve my problem. Thanks. – tyapo Mar 11 '15 at 12:43
1

So, here's the questionable part: I pass this.next.bind(this) as a callback to the asynchronous read function. The code seems to work when generators are enabled

No, that doesn't work. Generators cannot be constructed using new like you do. From the spec:

If the generator was invoked using [[Call]], the this binding will have already been initialized in the normal manner. If the generator was invoked using [[Construct]], the this bind is not initialized and any references to this within the FunctionBody will produce a ReferenceError exception.

Generator functions be invoked with new (see §9.2.3, with derived for [[ConstructorKind]]), but they do not construct an instance.

When the sensor reading is done, […] control returns to the main code.

That's a clever idea indeed. It has been explored before, see Understanding code flow with yield/generators or this article. Many libraries support this, especially combined with promises.

I suggest you use one of these libraries, your current code isn't really stable (will break with full ES6 support) and also seems to lacks error handling.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375