ES6のジェネレータはHaskellのdo記法ほど強力ではないという話

curiosity-driven.org
postd.cc

この記事では、ES6のジェネレータを使って、Haskellのdo記法を模倣したdoM関数を定義し、ジェネレータを使ってモナドを取り扱えることを示している。しかし “The same routine can be used with other monads like the Continuation monad” という記述に反し、実はdoMと一緒に使えるモナドと、使えないモナドがある。

話を簡単にするため、以下ではECMAScriptのプロトタイプチェーン機能を使わないでプログラムを書くことにし、doMには、モナドを表現するオブジェクトを、引数として別途渡すことにする。

function doM(m, gen) {
  function step(x) {
    var y = gen.next(x);
    if (y.done) {
      return y.value;
    }
    return m.bind(y.value, step);
  }
  return step();
}

具体的に、ふたつのモナドOMGMを定義する。OMはOptionMonadの略で、GMはGeneratorMonadの略だ。

var OM = {
  unit: function(x) {
    return {tag: "Some", value: x};
  },
  bind: function(n, k) {
    if (n.tag === "None") {
      return {tag: "None"};
    }
    return k(n.value);
  },
};

var GM = {
  unit: function*(x) {
    yield x;
  },
  bind: function*(g, k) {
    for (var i of g) {
      yield* k(i);
    }
  },
};

このとき、OMに対してはdoMを適用できる。

var x = doM(OM, function*() {
  var a = yield OM.unit(2);
  var b = yield OM.unit(3);
  return OM.unit(a * b);
}());

console.log(x); // ==> Object { tag: "Some", value: 6 }

var y = doM(OM, function*() {
  var a = yield OM.unit(2);
  var b = yield {tag: "None"};
  return OM.unit(a * b);
}());

console.log(y); // ==> Object { tag: "None" }

一方で、GMに対してdoMを使うと、例外が出てしまう。

var z = doM(GM, function*() {
  var a = yield function*(){ yield 2; yield 3; yield 4; }();
  var b = yield function*(){ yield 5; yield 6; }();
  return GeneratorMonad.unit(a * b);
}());

try {
  for(var i of z) {
   console.log(i);
  }
} catch (e) {
  console.log(e); // ==> TypeError: k(...) is undefined
}

なぜかといえば、GM.bindは第2引数k複数回呼び出す可能性があるからで、一方、doM内で定義されている関数stepは、ジェネレータの呼出しに副作用を使うことを前提としていて、複数回呼び出されることを考慮しない作りになっているという点が問題となっている。この問題を修正するには、途中まで実行したジェネレータを複製することができる必要があるが、ES6のジェネレータにそのような機能はないようなので、結局ジェネレータを使ってどのようなモナドにも対応したdoMを実装することはできないということになる。

比較用に、doMを使わない場合のコードも載せておく。

var w = GM.bind(function*(){ yield 2; yield 3; yield 4; }(), a =>
        GM.bind(function*(){ yield 5; yield 6; }(), b =>
        GM.unit(a * b)));

for(var i of w) {
  console.log(i);
}