モナドについて考えた記録
Promiseってなんとなくモナドっぽいなと思って、ジェネレータ使ってHaskellのdoを再現できないかなあというのがはじまり。結論からいうと、できない。
そもそもPromiseがモナドであるかを考える。
return :: Monad m => a -> m a
値を受け取ってfulfilledされたPromiseを返すものだ Promise.fulfill / Promise.resolveがこれにあたる。
Promise.mreturn = Promise.fulfill || Promise.resolve;
>>= :: Monad m => m a -> (a -> m b) -> m b
bindとも呼ばれたりする。この関数に、値を受け取ってモナドを返す関数を渡すと、その値が結果になる。
Promise.prototype.mbind = Promise.prototype.then;
つまり、Promise#thenである。
モナド則
- return a >>= f === f a
- m >>= return === m
- (m >>= f) >>= g === m >>= (\x -> f x >>= g)
これらを満たしていればモナドであると言える。 return a >>= f === f a は
Promise.mreturn(1).then(function (v) { console.log(v); }); // 1 (function (v) { console.log(v); })(1);
であり、満たしている
m >>= return === m これは (>>=) m returnで
var m = Promise.mreturn(1); m.then(Promise.mreturn) .then(function (v) { console.log(v); });
m.then(function (v) { console.log(v); }); // 1
であり、同一なので満たす。
(m >>= f) >>= g === m >>= (\x -> f x >>= g)は
var m = Promise.mreturn(2); function f(v) { return Promise.mreturn(v * 3); } function g(v) { return Promise.mreturn(v + 2); } var m = Promise.mreturn(1); m.mbind(f).mbind(g) // (m >>= f) >>= gの部分 .then(console.log.bind(console)); // 5
m.mbind(function (x) { return f(x).mbind(g); }) // m >>= (\x -> f x >>= g)の部分 .then(console.log.bind(console)); // 5
となり、一致するので満たしている。
よって、Promiseはモナドである。
拡張
Promise.do(function* () { var a = yield Proimse.mreturn(1); var b = yield Promise.mreturn(2); return a + b; }).then(console.log.bind(console)); // 3
この記法をモナド全般に拡張できないのか?というのが今回のテーマなのだが、結論から言うと、できない。
一見、yield時に得たモナドをmbind(function (v) { iter.next(v) // 再帰的に終わるまで繰り返す }) で正しそうに見えるが、実際は、モナド全般に一般化するとmbind(f)でfは複数回呼ばれる可能性があるので、動かない。
わかりづらいので、想定通りに動かないモナドの例でいくと リストモナドを
Array.prototype.mbind = function (f) { return [].concat.apply([], [].concat(this.map(f))); } Array.mreturn = function (x) { return [x]; };
のように定義すると、
[1, 2].mbind(function (x) { return [3, 4].mbind(function (y) { return Array.mreturn([x, y]); }); }); // [[1,3],[1,4],[2,3],[2,4]]
このようなことができる。しかし
Array.do(function* () { var x = yield [1, 2]; var y = yield [1, 2]; return [x, y]; });
とすると、イテレータの状態(=yield時の継続)をコピーできないので、x = 1、y = 1の後に、x = 2に戻す部分が実装できない。
うーん
ということで何らかのaltJSでdo <-をmreturnとmbindにdesugarするようにしたらいろいろなことを統一的にできるようになっていいのではないか? と思うのだが、ES6などの方向性とマッチしていないので(当然、リスト内包はMonadのsyntax sugarではない)、Monadなどのパラダイムを活用するのは難しいように思う