ES6のジェネレータはHaskellのdo記法ほど強力ではないという話
この記事では、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(); }
具体的に、ふたつのモナドOM
とGM
を定義する。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); }