素人がプログラミングを勉強していたブログ

プログラミング、セキュリティ、英語、Webなどのブログ since 2008

連絡先: twitter: @javascripter にどうぞ。

Google Chromeに入ったジェネレータとPromiseで非同期処理に革命が起きた

Google Chrome Canary(正確にはV8)に、ついにGenerators(yield)が入った。これを上手に使うと、エラー処理を含む非同期コードを同期的に書くことができるようになり、見通しが極めて良くなるので、ここで紹介する。

ここで紹介するものはいずれNode.jsでも使用できるようになるので、Webとの互換性を気にする必要のないNode.jsでは近いうちに活用できるようになると思う。

下のコードを動かすためには、最新のGoogle Chrome Canaryで、chrome://flagsからexperimental javascriptを有効にしておく必要がある。

ES6 HarmonyのGenerator構文について

functionではなくfunction*というキーワードを使うと、yieldキーワードが使えるようになる。

function* range(begin, end) {
  for (var i = begin; i < end; i++) {
    yield i;
  }
}

こうして作成したジェネレータは、for of構文を使って、

for (var n of range(0, 5)) {
  console.log(n);
}

として使うことができるが、今のところfor of構文は実装されていないようなので、まだ使えない。 手動でジェネレータを動かすこともでき、next/send/throwメソッドを使って、

var g = range(0, 2);
console.log(g.next());
console.log(g.next());
console.log(g.next());

console.log(g.next()); // Error: Generator has already finished

このように動かすこともできる。これは実装済み。send/throwは引数を一つ受け付け、それぞれ、yieldが返す値を指定するとき、yieldに例外を発生させたいときに使う。 詳しい仕様は http://wiki.ecmascript.org/doku.php?id=harmony:generators に載っているが、まあPythonのジェネレータとほとんど同じと思うと良いだろう。

非同期処理/Promiseとの組み合わせ

ジェネレータのすごいところは、関数を途中で止められるところである。この特性と、Promiseをを組み合わせると、非同期処理を同期的に書くことができる。 Promiseそのものの詳細については http://javascripter.hatenablog.com/entry/2012/12/30/232842 に書いた。

ここではPromiseの実装としてjQuery.Deferredを使うが、好きなものを使うと良いだろう。 はてブで指摘されたのでPromiseを修正 id:teppeis ++

// new Promise(function (resolve, reject) { resolve('hi'); }) -> promise
function Promise(setup) {
  var d = jQuery.Deferred();
  var p = d.promise();
  setup(d.resolve, d.reject);
  return p;
}

まず、後に定義するasync関数を使うと何ができるかを先に示す。 一番簡単な例だと、

function wait(ms) { return new Promise(function (resolve, reject) { setTimeout(resolve, ms); }); }

async(function* () {
  console.log('hi');

  yield wait(1000);

  console.log('this will appear after 1 sec');  
});

まず、waitを同期的に書けたのがわかる。これはジェネレータを使わないと

console.log('hi');
wait(1000).then(function () { console.log('this will appear after 1 sec'); });

である。

Promiseの結果を取得することもでき、非同期XMLHttpRequestは下記のように書ける。

function get(url) {
  return new Promise(function (resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url, false);
    req.onload = function () {
      resolve(req);
    };
    req.onerror = function () {
      reject(req);
    };
    req.send(null);
  });
}

async(function* () {

  var res = yield get(location.href);
  var len = res.responseText.length;
  console.log('length is', len);

});

これはジェネレータ抜きだと

get(location.href).then(function (res) {
  var len = res.responseText.length;
  console.log('length is', len);
});

に対応する。 then(resolve, reject)のreject部はどう書けばいいのだろうか?同期処理と同じように

async(function* () {
  try {
    yield get(location.href);
  } catch (ex) {
    alert('could not fetch ' + location.href);
  }
});

と書けばよい。asyncはプロミスを返すので

async(function* () {
  yield get(location.href);
}).then(function () {}, function (ex) {
  // エラー処理はここで
});

とすることもできるし、実際のところ、yield promiseとするだけで完全に同期処理のように扱える。

var len = async(function* () {
 return (yield get(location.href)).responseText.length;
});

async(function* () {
  return (yield len) + 1;
}).then(function (val) {
  console.log(val);
}, function () {
  console.log('たぶんgetでエラーが発生した感じがする');
});

などの組み合わせも、期待通り動く。 さて、肝心のasync関数についてだが、

function isPromise(maybePromise) {
  return maybePromise && typeof maybePromise.then === 'function';
}

function isStopIteration(maybeStopIteration) {
  // ugly, but works well
  return maybeStopIteration && (
    typeof StopIteration === 'function' ?
      maybeStopIteration instanceof StopIteration :
      maybeStopIteration.message === 'Generator has already finished');
}


function async(thunk) {
  return new Promise(function (resolve, reject) {
    var thread = thunk();
    function proceed(method, result) {
      var returnValue;
      try {
        returnValue = thread[method](result);
      } catch (ex) {
        if (isStopIteration(ex)) {
          resolve(result);
        } else {
          reject(ex);
        }
        return;
      }

      if (isPromise(returnValue)) {
        returnValue.then(function (result) {
          proceed('send', result);
        }, function (result) {
          proceed('throw', result);
        });
      } else {
        // silently proceed
        proceed('send', returnValue);
      }
    }
    proceed('send', null);
  });
}

たったこれだけである。 ジェネレータを使い同期的に書いたコードもasyncが返す時にはPromiseになり、Promise自体を拡張する必要がないこと、同期/非同期をまぜてほぼ同じように書けること、NodeのDomainのようなこともasyncで囲うだけでできることを考えると、非常に筋がいいと思う。

参考

https://github.com/mozilla/task.js 少しoverkillな気がするが、同じコンセプトのもの。一年前から更新されてないし、どうなったのかな?という感じは多少する。

https://github.com/kriskowal/q Q.asyncが同じことをやろうとしているが、StopIterationがないのでGoogle Chromeでは動いていない。いずれ対応すると思われる。

コードをうごくように修正した。ちなみに、Firefoxはずいぶん前からGeneratorを実装していて、ES6の構文とは多少異なり独自仕様だが、task.jsはFirefoxで動くように作られているので、興味があったら動かしてみると良いだろう。