Skip to content

StateTモナド

Stateモナドの続きです。

コンソールに出力したい…けど

よく考えると doSomething :: forall a. Show a => a -> Unit に渡したところで何一つ面白いことがありません。
副作用がない PureScript において Unit を返す関数は結局なにもしてくれないのです。

面白いこと…例えば logShow :: forall a. Show a => a -> Effect Unit に渡してコンソールに出力とかしてみたいですね。

というわけで前回最後のコードdoSomethinglogShow に置き換えてみます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
transit :: State Int Unit
transit = do
  modify (_ + 1)
  b <- gets even
  _ <- lift $ logShow b  -- 現在の状態が偶数かどうかを表示する
  modify (_ + 2)
  s <- get
  _ <- lift $ logShow s  -- 現在の状態を表示する
  put 3
  modify (_ + 4)

実際に試す

…何も出力されませんね。

実は _ <- lift $ logShow b_ <-作用を捨てちゃっているからなんです。

logShow が返す Effect unit 型の値は、コンソールに何かを出力するという作用を表すただの値です。
この値は main 関数から処理系に渡すことで実際の作用が発生します。

つまり _ <- で捨てたりせずなんとかして最後まで受け渡す必要があります。

まずは力技でなんとかする

do記法はもともとただの糖衣構文であり脱糖した後は bind で結合したただのクロージャになっています。
つまり a <- で一度名前に束縛した値は以降どこでも参照できるのです。

これを利用して作用を捨てずに拾って返すようにしてみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
transit :: State Int (Effect Unit)
transit = do
  modify (_ + 1)
  b <- gets even
  e <- lift $ logShow b  -- 現在の状態が偶数かどうかを表示する
  modify (_ + 2)
  s <- get
  f <- lift $ logShow s  -- 現在の状態を表示する
  put 3
  modify (_ + 4)
  pure do                -- 作用を1つに繋げて出力する
    e
    f

できました!

あとで状態だけでなく出力も取り出す必要があるので execState の代わりにこんな関数を用意すると便利そうです。

1
runState (State m) = m  -- 初期状態を渡して最後の状態と出力を得る

実際に試す

かっこいい方法を考える

しかしクロージャじゃないとダメというのも不便なときがありそうです。
ではいっそ Int -> Effect (Tuple a Int) を繋げるようにしたらどうでしょう?

一旦モナドは忘れてまずはこんな感じで定義してみます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type StateT s a = s -> Effect (Tuple a s)

bind' m f = \s -> m s >>= \(Tuple v s') -> f v s'
infixl 1 bind' as >>-

modify f = \s -> pure $ Tuple unit  (f s)
gets   f = \s -> pure $ Tuple (f s) s
get      = \s -> pure $ Tuple s     s
put    s = \_ -> pure $ Tuple unit  s
lift   m = \s -> m >>= \x -> pure $ Tuple x s

execStateT m s = snd <$> m s

こうすれば特別に意識しなくても作用を状態遷移中に書けそうです。

1
2
3
4
5
6
7
8
9
transit :: StateT Int Unit
transit   = modify (_ + 1)          -- 状態を +1 する
  >>- \_ -> gets   even             -- 現在の状態が偶数か調べる
  >>- \b -> lift   (logShow b)      -- 調べた結果を表示する
  >>- \_ -> modify (_ + 2)          -- 状態を(以下略)
  >>- \_ -> get                     -- 状態を取得する
  >>- \s -> lift   (logShow s)      -- 現在の状態を表示する
  >>- \_ -> put    3                -- 状態を 3 にする
  >>- \_ -> modify (_ + 4)          -- (以下略)

実際に試す

Effect 以外でも使えるようにする

4.で作成した StateT は別に Effect じゃなくても purebind があれば使えそうです。
つまり Monad ならなんでもよさそうです。

なので Effectm に置き換えてみます。

1
type StateT s m a = s -> m (Tuple a s)

これだけで Effect 以外でも OK になります。

試しに Aff でやってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
transit :: StateT Int Aff Unit
transit   = modify (_ + 1)          -- 状態を +1 する
  >>- \_ -> gets   even             -- 現在の状態が偶数か調べる
  >>- \b -> lift   (logShow b)      -- 調べた結果を表示する
  >>- \_ -> lift   (delaySec 2.0)   -- 2秒待つ
  >>- \_ -> modify (_ + 2)          -- 状態を(以下略)
  >>- \_ -> get                     -- 状態を取得する
  >>- \s -> lift   (logShow s)      -- 現在の状態を表示する
  >>- \_ -> lift   (delaySec 2.0)   -- 2秒待つ
  >>- \_ -> put    3                -- 状態を 3 にする
  >>- \_ -> modify (_ + 4)          -- (以下略)

実際に試す

注意

Try PureScript! では Aff 使うと render =<< withConsole が効かないので F12 を押すなどしてコンソール出力を直接確認する必要があります。

StateT も Monad のインスタンスにする

後は前回と同じです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 新しい型にする
newtype StateT s m a = StateT (s -> m (Tuple a s))

-- Monad のインスタンスにする
instance functorStateT :: Functor m => Functor (StateT s m) where
  map f (StateT a) = StateT (\s -> map (\(Tuple b s') -> Tuple (f b) s') (a s))

instance applicativeStateT :: Monad m => Applicative (StateT s m) where
  pure a = StateT \s -> pure $ Tuple a s

instance applyStateT :: Monad m => Apply (StateT s m) where
  apply = ap

instance bindStateT :: Monad m => Bind (StateT s m) where
  bind (StateT x) f = StateT \s ->
    x s >>= \(Tuple v s') -> case f v of StateT st -> st s'

instance monadStateT :: Monad m => Monad (StateT s m)

-- いろいろな関数を用意する
modify f = StateT \s -> pure $ Tuple unit  (f s)
gets   f = StateT \s -> pure $ Tuple (f s) s
get      = StateT \s -> pure $ Tuple s     s
put    s = StateT \_ -> pure $ Tuple unit  s
lift   m = StateT \s -> m >>= \x -> pure $ Tuple x s

execStateT (StateT m) s = snd <$> m s

実際に試す

注意

Try PureScript! では Aff 使うと render =<< withConsole が効かないので F12 を押すなどしてコンソール出力を直接確認する必要があります。

まとめ

ある型コンストラクタが別の型コンストラクタを引数になっていて、両方とも Monad のインスタンスで、さらに相互に変換する仕組みがあれば、それがモナド変換子です。

判ってしまえば難しいことは何もないのですが…初めはいくら説明されてもなかなか脳が受け付けません。
いろいろ足掻いているうちにある日突然脳が、これか! と納得するのです。

なので実際のところ2017年の自分がこの記事を読んですぐに判るかというと…あやしいですね(笑)


Last update: September 20, 2023