Help us understand the problem. What is going on with this article?

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

ジェネレーターを DSL のように使って関数モナドと State モナドを模倣してみました。記述をそれっぽく見せることに重点を置いたため、bind や return を正確に実装したわけではありません。

関数モナド(のようなもの)

See the Pen Function Monad by 七誌 (@7shi) on CodePen.

State モナド(のようなもの)

See the Pen State Monad by 七誌 (@7shi) on CodePen.

この記事には Python 版があります。

また、ジェネレーターを強引に CPS 変換することも試してみました。

実装

実装を並べると、関数モナドと 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 モナドで使う getput を見れば、挙動が分かりやすいと思います。

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)
実行結果
(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]
@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)

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)
実行結果
(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]
@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 変換するくらいしか方法がなさそうです。

※ もしイテレーターがコピーできれば実現できそうです。

【参考】How would we copy an iterator?

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

Haskell JavaScript Python




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



main =
  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]
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]

for による方法は次の記事に書きました。

リストモナドの挙動は .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 を模倣していたのにヒントを得ました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした