haskell-jp / beginners #25 at 2024-07-22 13:11:02 +0900

モナドでの <- の挙動について質問させてください。

State s モナドを例にとります。
newtype State s a = State { runState :: s -> (a,s) }

instance Monad (State s) where
    return a = State $ \s -> (a, s)

    (State h) >>= f = State $ \s ->
        let
            (a, newState) = h s
            (State g) = f a
        in
            g newState

pop = State $ (\(a:s) -> (a,s))

上記の定義に対して以下のようなプログラムを実行します。
steps :: State [Int] Int
steps = do
    a <- pop
    return a

f = runState steps [1..5]

結果は以下の通りです。(hugs)
Main> :set +t
Main> :l b.hs
Main> f
(1,[2,3,4,5]) :: (Int,[Int])

動作自体は理解できたのですが、`a <- pop` により値 (Int) が束縛される理由がわかりません。

Maybe モナドで考えて a <- Just a の場合は Just が外れて a が取り出されている。と理解しやすいです。
また、`(a, s) <- pop` であった場合は Monad (State s) の定義と矛盾するような気がするので、違うのかと考えています。

モナド値 State s a の場合、`a` の位置にあるものが <- により取り出される。
のだと思いますが、そういうものと考えてしまって良いものでしょうか ?
結論から言うと「モナド値 State s a の場合、`a` の位置にあるものが <- により取り出される。」という認識で問題ないです。

件の do 記法による定義を脱糖すると
steps = pop >>= \a -> return a
となります。
で、 State>>= の定義を見ると、右辺に渡した関数 f 、この場合 \a -> return a の引数として渡しているのは、左辺にある State h というパターンマッチで取り出された h の結果 (a, newState)a なのでタプルの一つ目の要素 a です。
これを pop に当てはめると
pop = State $ (\(a:s) -> (a,s))
という定義なので、結果である (a,s)a<- によって束縛される、という説明でよいでしょうか?
@igrep
理屈から言えば、そーなんだろうな。と漠然とは考えられたのですが、Maybe モナドのように値が単独で存在せず、タプルの中の値になってしまっていたので、なんでこんな風に動作するんだろう
と、考えてしまいました。

タプルの中の値であっても、`a <-` により、束縛されている値が取得できる。という認識が間違っていなくて安心しました。

丁寧に解説までしていただき、ありがとうございました。
すでに分かっていたら余計なお世話かもしれませんが、
まず一回Stateを使わずにタプルの2つ目で現在の状態を返すように書いてみて、それをStateを使ったものに書き換えると私はしっくり理解できました。
a <- と書くから「取り出している」と感じてしまうけれど、実際には \a -> なので一個の値を受け付ける関数が生成されているわけで、「後半部分が値を一個ぶんだけ受け取れるように取り計らってくれる(のがモナド)」という感じな気がしています。
@
ありがとうございます。

Stateを使わずにタプルの2つ目で現在の状態を返すように書いてみて
について、やってみようとしましたが出来ませんでした :smiling_face_with_tear:

もし、面倒でなければ可能であれば簡単に具体例を書いていただけると助かります
@ai-ou

例えばタプルの2つ目の変数で現在のスタックの状態を表そうとすると以下のような方法が考えられます。

popT :: [Int] -> (Int, [Int])
popT (a : s) = (a, s)

pushT :: Int -> [Int] -> ((), [Int])
pushT a s = ((), a : s)

stepsT :: [Int] -> ((), [Int])
stepsT world0 =
  let (a, world1) = popT world0
      ((), world2) = pushT (a * 2) world1
  in ((), world2)

これは一つの関数を実行するたびにworld引数を書き換えないといけないので面倒くさいですし、ミスしやすいという考えもあります。

そこでそちらが書いていたようにStateを定義してやるとスッキリと収まり各関数もStateに渡すだけです。

newtype State s a
  = State
  { runState :: s -> (a, s)
  }

instance Functor (State s) where
  fmap f (State h) = State $ \s ->
    let (a, newState) = h s
    in (f a, newState)

instance Applicative (State s) where
  pure a = State (a, )

  (State f) <*> (State g) = State $ \s ->
    let (h, newState) = f s
        (a, newerState) = g newState
    in (h a, newerState)

instance Monad (State s) where
  (State h) >>= f = State $ \s ->
    let (a, newState) = h s
        (State g) = f a
    in g newState

pop :: State [Int] Int
pop = State popT

push :: Int -> State [Int] ()
push = State . pushT

steps :: State [Int] ()
steps = do
  a <- pop
  push (a * 2)

f :: ((), [Int])
f = runState steps [1 .. 5]

main :: IO ()
main = print f

steps がだいぶスッキリした見た目になりました。
worldの更新は内部で >>= で行っていれば同じになりますし、 runState すると型が元の形式と同じになります。
私はこのステップを踏むとすんなり理解できたので、参考になると幸いです。
@
最初のコードに合わせてサンプルを作成していただき、理解しやすいです。
すぐには飲み込めませんが、いただいたコードを熟読して理解に努めます。

丁寧にご回答いただきありがとうございました。