9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Pythonでもジェネレーターで関数モナドとStateモナドを模倣してみた

Last updated at Posted at 2020-02-10

ジェネレーターを 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 モナドで使う getput を見れば、挙動が分かりやすいと思います。

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)

実行結果

(4,6)
(6,10)

@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))

実行結果

(4, 6)
(6, 10)

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));

実行結果

[4,6]
[6,10]

オンラインで実行 (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)

実行結果

(3,6)
(5,10)
(3,4)
(5,6)

@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]);

実行結果

[3,6]
[5,10]
[3,4]
[5,6]

オンラインで実行 (Repl.it)

リストモナド

同じ方式でリストモナドを実装できないか考えたのですが、多重ループとなる場合に変数の値を変えて同じコードを何度も実行する必要があり、実現する方法が思いつかずに断念しました。

ジェネレーターの中で普通に for で多重ループを書けば同じことはできます。

Haskell Python JavaScript

test = do
x <- [1, 2]
y <- [3, 4]
[x, y]
main =
print test

実行結果

[1,3,1,4,2,3,2,4]

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)

実行結果

[1, 3, 1, 4, 2, 3, 2, 4]

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);

実行結果

[1,3,1,4,2,3,2,4]

オンラインで実行 (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 のモナドは次の記事を参考にしてください。

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?