Haskell中State monad的传递引起了混乱

4
在Haskell中,状态是单子传递并存储状态。在下面的两个例子中,都使用>>传递State单子,并通过函数内联和还原进行了密切验证,确保状态确实传递到了下一步。
然而,这似乎不太直观。那么,这是否意味着当我想传递State单子时,只需要>>(或者>>=和lambda表达式\s -> a,其中s不是a的自由变量)?有人可以提供一个直观的解释吗,而不必去简化函数吗?
-- the first example
tick :: State Int Int 
tick = get >>= \n ->
   put (n+1) >>
   return n

-- the second example
type GameValue = Int 
type GameState = (Bool, Int)

playGame' :: String -> State GameState GameValue 
playGame' []      = get >>= \(on, score) -> return score
playGame' (x: xs) = get >>= \(on, score) ->
    case x of
        'a' | on -> put (on, score+1)
        'b' | on -> put (on, score-1)
        'c'      -> put (not on, score)
        _        -> put (on, score) 
    >> playGame xs 

非常感谢!
1个回答

5
理解状态等同于 s -> (a, s)。因此,任何在单子操作中“封装”的值都是通过对某个状态 s 进行转换产生的结果(一个生成a的具有状态的计算)。
在两个具有状态的计算之间传递状态。
f :: a -> State s b
g :: b -> State s c

对应于使用>=>进行组合

f >=> g

或者使用 >>=

\a -> f a >>= g

这里的结果是

a -> State s c

这是一种有状态的操作,以某种方式转换了一些基础状态s,并允许访问一些a,最终产生了一些c。整个转换过程可以依赖于a,而值c可以依赖于某些状态s。这正是表达有状态计算所需的。这种机制作为单子的表达方式的巧妙之处(也是唯一的目的)在于你不必费心传递状态。但要理解它是如何完成的,请参考对>>=的定义(请参见hackage),暂时忽略它是一个变换器而不是最终的单子。
m >>= k  = StateT $ \ s -> do
    ~(a, s') <- runStateT m s
    runStateT (k a) s'

你可以忽略使用StateTrunStateT的包装和解包,这里m的形式为s -> (a, s)k的形式为a -> (s -> (b, s)),你希望产生一个有状态的转换s -> (b, s)。因此结果将是s的一个函数,要产生b,你可以使用k,但首先需要a,如何产生a?你可以取m并将其应用于状态s,你从第一个单子操作m得到一个修改后的状态s',然后将该状态传递给(k a)(它的类型为s -> (b, s))。就在这里,状态s通过m变成了s',然后传递给k变成一些最终的s''
作为这个机制的用户,对于你来说,这些都是隐藏的,这就是monad的好处。如果你想让状态沿着某个计算发展,你可以从小步骤构建你的计算,将它们表示为“State”操作,并让“do”符号或绑定(“>>=”)进行链接/传递。
">>="和">>"之间唯一的区别在于你是否关心非状态结果。
a >> b

实际上等同于

a >>= \_ -> b

所以无论动作a输出什么值,你都会将其丢弃(只保留修改后的状态),并继续(传递状态)执行另一个动作b


关于您的示例
tick :: State Int Int 
tick = get >>= \n ->
    put (n+1) >>
    return n

你可以用do语法重写它:

tick = do
    n <- get
    put (n + 1)
    return n

虽然第一种写法可能更明确地显示了传递的内容,但第二种写法很好地说明了你不必关心它。

  1. 首先,获取当前状态并公开它(在简化的设置中为get :: s -> (s, s)),<-表示您关心该值,并且不想将其丢弃,底层状态也会在没有更改的情况下传递(这就是get的工作方式)。

  2. 然后,put :: s -> (s -> ((), s)),在删除不必要的括号后等效于put :: s -> s -> ((), s),接受一个值来替换当前状态(第一个参数),并生成一个状态操作,其结果是无趣的值(),您可以丢弃它(因为您不使用<-或者因为您使用>>而不是>>=)。由于put,底层状态已更改为n + 1,因此会传递。

  3. return对底层状态不起作用,它只返回其参数。

总之,tick 从一些初始值 s 开始,内部更新它为 s+1 并在旁边输出 s
另一个示例的工作方式完全相同,只是在那里使用 >> 来丢弃 put 产生的 ()。但状态一直传递。

在阅读后,我意识到我只是没有理解它被丢弃的价值,但状态仍然会传递。 - Quentin Liu
很高兴能够帮助,如果答案符合您的要求,请考虑接受它以关闭问题(这适用于stackoverflow上的所有问题),这有助于跟踪未解决的问题,并帮助其他人在提出自己的问题之前找到答案。 - jakubdaniel
谢谢这个提示,哈哈。 - Quentin Liu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接