8
3

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 1 year has passed since last update.

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

Last updated at Posted at 2020-02-10

ジェネレーターを 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 モナドで使う 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)
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))
実行結果
(4,6)
(6,10)
実行結果
[4,6]
[6,10]
実行結果
(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)
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)
実行結果
[3,6]
[5,10]
[3,4]
[5,6]
実行結果
(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)
実行結果
[1,3,1,4,2,3,2,4]
実行結果
[1,3,1,4,2,3,2,4]
実行結果
[1, 3, 1, 4, 2, 3, 2, 4]

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 を模倣していたのにヒントを得ました。

8
3
1

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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?