ジェネレーターを DSL のように使って関数モナドと State モナドを模倣してみました。記述をそれっぽく見せることに重点を置いたため、bind や return を正確に実装したわけではありません。
この記事は次の記事の Python 版です。同じことが出来るはずなので確認したくなりました。
結果的に、ジェネレーターの return
の仕様の変遷や、デコレーターが有用なことなどが分かりました。
実装
実装を並べると、関数モナドと State モナドの差分が分かりやすいです。
関数モナド |
State モナド |
def function_monad(g):
def f(state):
it = g()
value = None
try:
while True:
value = it.send(value)(state)
except StopIteration as e:
return e.value
return f
|
def state_monad(g):
def f(state):
it = g()
value = None
try:
while True:
value, state = it.send(value)(state)
except StopIteration as e:
return (e.value, state)
return f
|
関数モナドでは state
は固定で value
のみが更新されますが、State モナドでは両者をセットで扱い更新されます。
これを踏まえてState モナドで使う get
と put
を見れば、挙動が分かりやすいと思います。
get = lambda state: (state, state)
put = lambda newState: lambda oldState: (None, newState)
サンプル
以前書いた次の記事から引用しました。
関数モナド
test
に対する引数が、yield
の右のラムダ式に引数として与えられます。
Haskell |
Python |
JavaScript |
test = do
a <- (+ 1)
b <- (* 2)
return (a, b)
main = do
print (test 3)
print (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))
実行結果
|
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));
実行結果
|
オンラインで実行 (Repl.it)
State モナド
State モナドはコンテキストとして状態を持っており、呼び出す際に初期値を与えます。状態の取得は get
、更新は put
で行います。
Haskell |
Python |
JavaScript |
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)
実行結果
|
@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)
|
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]);
実行結果
|
オンラインで実行 (Repl.it)
リストモナド
同じ方式でリストモナドを実装できないか考えたのですが、多重ループとなる場合に変数の値を変えて同じコードを何度も実行する必要があり、実現する方法が思いつかずに断念しました。
ジェネレーターの中で普通に for
で多重ループを書けば同じことはできます。
Haskell |
Python |
JavaScript |
test = do
x <- [1, 2]
y <- [3, 4]
[x, y]
main =
print 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)
実行結果
|
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);
実行結果
|
オンラインで実行 (Repl.it)
リスト内包表記で書くとフラット化が必要になります。
>>> [[x, y] for y in [3, 4] for x in [1, 2]]
[[1, 3], [2, 3], [1, 4], [2, 4]]
>>> sum([[x, y] for y in [3, 4] for x in [1, 2]], [])
[1, 3, 2, 3, 1, 4, 2, 4]
【参考】Python 3 で flatten する方法いろいろ
return
ジェネレーターで値付きの return
が使えるようになったのは割と最近のことのようです。
これは組み込み例外 — Python 3.7.3 ドキュメント に記述があり、PEP 479 -- Change StopIteration handling inside generators がデフォルトで有効化されているためです。
以前のバージョンではエラーになっていたようです。
SyntaxError: 'return' with argument inside generator
デコレーター
今回はデコレーターが大活躍しています。少し前に次の記事で知りました。
Python用語集に、以下の記載があります。
decorator
(デコレータ) 別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod() と staticmethod() です。
デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:
def f(...):
...
f = staticmethod(f)
@staticmethod
def f(...):
...
※ 併記した JavaScript では関数でジェネレーターを包む様子を直接表記しています。
参考
Haskell のモナドは次の記事を参考にしてください。