ジェネレーターを DSL のように使って関数モナドと State モナドを模倣してみました。記述をそれっぽく見せることに重点を置いたため、bind や return を正確に実装したわけではありません。
関数モナド(のようなもの)
See the Pen
Function Monad by 七誌 (@7shi)
on CodePen.
State モナド(のようなもの)
See the Pen
State Monad by 七誌 (@7shi)
on CodePen.
この記事には Python 版があります。
実装
実装を並べると、関数モナドと State モナドの差分が分かりやすいです。
関数モナド |
State モナド |
function functionMonad(g) {
return state => {
let it = g(), result, value;
while (!(result = it.next(value)).done) {
value = result.value(state);
}
return result.value;
};
}
|
function stateMonad(g) {
return state => {
let it = g(), result, value;
while (!(result = it.next(value)).done) {
[value, state] = result.value(state);
}
return [result.value, state];
};
}
|
関数モナドでは state
は固定で value
のみが更新されますが、State モナドでは両者をセットで扱い更新されます。
これを踏まえてState モナドで使う get
と put
を見れば、挙動が分かりやすいと思います。
let get = state => [state, state];
let put = newState => oldState => [, newState];
サンプル
以前書いた次の記事から引用しました。
Haskell と JavaScript と Python を並べて比較します。
関数モナド
test
に対する引数が、yield
の右のラムダ式に引数として与えられます。
Haskell |
JavaScript |
Python |
test = do
a <- (+ 1)
b <- (* 2)
return (a, b)
main = do
print (test 3)
print (test 5)
|
let test = functionMonad(
function*() {
let a = yield x => x + 1;
let b = yield x => x * 2;
return [a, b];
});
log(test(3));
log(test(5));
|
@function_monad
def test():
a = yield lambda x: x + 1
b = yield lambda x: x * 2
return (a, b)
print(test(3))
print(test(5))
|
実行結果
|
実行結果
|
実行結果
|
State モナド
State モナドはコンテキストとして状態を持っており、呼び出す際に初期値を与えます。状態の取得は get
、更新は put
で行います。
Haskell |
JavaScript |
Python |
test = do
a <- get
put (a * 2)
b <- get
return (a, b)
postInc = do
x <- get
put (x + 1)
return x
test2 = do
a <- postInc
b <- postInc
return (a, b)
main = do
print (evalState test 3)
print (evalState test 5)
print (evalState test2 3)
print (evalState test2 5)
|
let test = stateMonad(
function*() {
let a = yield get;
yield put(a * 2);
let b = yield get;
return [a, b];
});
let postInc = stateMonad(
function*() {
let x = yield get;
yield put(x + 1);
return x;
});
let test2 = stateMonad(
function*() {
let a = yield postInc;
let b = yield postInc;
return [a, b];
});
log(test (3)[0]);
log(test (5)[0]);
log(test2(3)[0]);
log(test2(5)[0]);
|
@state_monad
def test():
a = yield get
yield put(a * 2)
b = yield get
return (a, b)
@state_monad
def postInc():
x = yield get
yield put(x + 1)
return x
@state_monad
def test2():
a = yield postInc
b = yield postInc
return (a, b)
print(test (3)[0])
print(test (5)[0])
print(test2(3)[0])
print(test2(5)[0])
|
実行結果
|
実行結果
|
実行結果
(3, 6)
(5, 10)
(3, 4)
(5, 6)
|
リストモナド
同じ方式でリストモナドを実装しようとすると、多重ループとなる場合に変数の値を変えて同じコードを何度も実行する必要があます。
現状では毎回やり直すか、ジェネレーターを強引に CPS 変換するくらいしか方法がなさそうです。
ジェネレーターの中で普通に for
で多重ループを書けば同じことはできます。
Haskell |
JavaScript |
Python |
test = do
x <- [1, 2]
y <- [3, 4]
[x, y]
main =
print test
|
function listMonad(g) {
return Array.from(g());
}
let test = listMonad(
function*() {
for (let x of [1, 2]) {
for (let y of [3, 4]) {
yield x; yield y
}
}
});
log(test);
|
def list_monad(g):
return list(g())
@list_monad
def test():
for x in [1, 2]:
for y in [3, 4]:
yield x; yield y
print(test)
|
実行結果
|
実行結果
|
実行結果
|
for
による方法は次の記事を参照してください。
flatMap
リストモナドの挙動は .flatMap
で再現できます。
> [1, 2].flatMap(x => [3, 4].flatMap(y => [x, y]))
[ 1, 3, 1, 4, 2, 3, 2, 4 ]
参考
出力の log()
は次の実装を使っています。
Haskell のモナドは次の記事を参考にしてください。
次の記事に触発されました。
ジェネレーターを DSL として使う発想は、co が async/await を模倣していたのにヒントを得ました。