对 Haskell 中 State monad 的传递感到困惑

confusion over the passing of State monad in Haskell

在 Haskell 中,状态是 monad 被传递来提取和存储状态。并且在下面的两个例子中,都使用 >> 传递了 State monad,并且通过仔细验证(通过函数内联和归约)确认状态确实传递到了下一步。

但这似乎不是很直观。那么这是否意味着当我想传递 State monad 时我只需要 >> (或 >>= 和 lambda 表达式 \s -> a 其中 sa)?任何人都可以在不费心减少功能的情况下对这个事实提供一个直观的解释吗?

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

非常感谢!

真正归结为了解状态与 s -> (a, s) 同构。因此,单子操作中的任何值 "wrapped" 都是对某些状态应用转换的结果 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。这正是您想要表达有状态计算的内容。整洁的事情(以及将此机制表示为 monad 的唯一目的)是您不必为传递状态而烦恼。但是要理解它是如何完成的,请参考hackage>>=的定义),暂时忽略它是一个transformer而不是final monad)。

m >>= k  = StateT $ \ s -> do
    ~(a, s') <- runStateT m s
    runStateT (k a) s'

你可以忽略使用StateTrunStateT的包装和展开,这里ms -> (a, s)的形式,k是[=的形式36=],并且您希望生成状态转换 s -> (b, s)。所以结果将是 s 的函数,要产生 b 你可以使用 k 但你首先需要 a,你如何产生 a ?你可以将 m 应用到状态 s,你从第一个 monadic 动作 m 得到修改后的状态 s',然后将该状态传递给 (k a)(类型为 s -> (b, s))。正是在这里,状态 s 已经通过 m 成为 s' 并被传递到 k 成为最终的 s''.

对于使用此机制的您来说,这仍然是隐藏的,这就是 monad 的妙处。如果你想让一个状态随着一些计算而发展,你可以从你表达为 State-actions 的小步骤构建你的计算,然后让 do-notation 或 bind (>>=) 来做chaining/passing.

>>=>> 之间的唯一区别是您关心或不关心非状态结果。

a >> b

实际上等同于

a >>= \_ -> b

因此,操作 a 输出的任何值都将被丢弃(仅保留修改后的状态)并继续(传递状态)其他操作 b


关于你的例子

tick :: State Int Int 
tick = get >>= \n ->
    put (n+1) >>
    return n

你可以用do-notation重写为

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

虽然第一种编写方式可能更明确地说明了传递的内容,但第二种方式很好地展示了您不必关心它。

  1. 首先get current 状态并公开它(简化设置中的get :: s -> (s, s)),<-表示您确实关心该值并且不想将其丢弃,底层状态也会在后台传递而不会发生变化(这就是 get 的工作方式)。

  2. Then put :: s -> (s -> ((), s)),等同于将不必要的括号删除到 put :: s -> s -> ((), s) 之后,取一个值用(第一个参数)替换当前状态,并生成一个有状态的结果是您丢弃的无趣值 () 的操作(因为您不使用 <- 或因为您使用 >> 而不是 >>=)。由于 put,基础状态已更改为 n + 1,因此它被传递。

  3. return 对底层状态没有任何作用,它只是 returns 它的参数。

总而言之,tick 从一些初始值 s 开始,它在内部将其更新为 s+1 并在侧面输出 s

另一个例子的工作方式完全相同,>> 只是用来丢弃 put 产生的 ()。但是状态一直在传递。