【问题标题】:State Monad, sequences of random numbers and monadic code状态单子,随机数序列和单子代码
【发布时间】:2010-12-29 17:10:50
【问题描述】:

我正在尝试掌握状态单子,为此我想编写一个单子代码,该代码将使用线性同余生成器生成一系列随机数(可能不好,但我的目的只是学习状态Monad,没有建立一个好的 RNG 库)。

生成器就是这样(为简单起见,我想生成一个Bools的序列):

type Seed = Int

random :: Seed -> (Bool, Seed)
random seed = let (a, c, m) = (1664525, 1013904223, 2^32)  -- some params for the LCG
                  seed' = (a*seed + c) `mod` m
              in  (even seed', seed')   -- return True/False if seed' is even/odd 

不要担心数字,这只是种子的更新规则(根据数字食谱)应该生成Ints 的伪随机序列。现在,如果我想按顺序生成随机数,我会这样做:

rand3Bools :: Seed -> ([Bool], Seed)
rand3Bools seed0  = let (b1, seed1) = random seed0
                        (b2, seed2) = random seed1
                        (b3, seed3) = random seed2
                    in  ([b1,b2,b3], seed3)

好的,所以我可以通过使用 State Monad 来避免这个样板:

import Control.Monad.State

data Random {seed :: Seed, value :: Bool}

nextVal = do 
   Random seed val <- get 
   let seed' = updateSeed seed
       val'  = even seed'
   put (Random seed' val')
   return val'

updateSeed seed = let (a,b,m) = (1664525, 1013904223, 2^32) in (a*seed + c) `mod` m

最后:

getNRandSt n = replicateM n nextVal 

getNRand :: Int -> Seed -> [Bool]
getNRand   n seed = evalState (getNRandStates n) (Random seed True)

好的,这很好用,并为每个给定的种子给我一个 n 伪随机 Bools 的列表。但是……

我可以阅读我所做的(主要基于此示例:http://www.haskell.org/pipermail/beginners/2008-September/000275.html)并将其复制以做其他事情。但我认为我无法理解 do-notation 和 monadic 函数(如 replicateM)背后真正发生的事情。

谁能帮我解决这些疑惑?

1 - 我试图对 nextVal 函数进行脱糖以了解它的作用,但我做不到。我猜它会提取当前状态,更新它,然后将状态提前传递给下一个计算,但这只是基于阅读这个 do-sugar 就好像它是英语一样。

我如何真正将这个函数脱糖到原来的 >>= 并逐步返回函数?

2 - 我无法理解 putget 函数的作用。我可以猜到他们“打包”和“解包”状态。但糖背后的机制对我来说仍然难以捉摸。

好吧,非常欢迎对这段代码提出任何其他一般性评论。我有时会迷恋 Haskell,因为我可以创建一个可以运行的代码并按照我的预期去做,但我不能像我习惯于使用命令式程序那样“遵循评估”。

【问题讨论】:

    标签: haskell monads state-monad do-notation


    【解决方案1】:

    首先,您的示例过于复杂,因为它不需要将val 存储在状态单子中;只有种子是持久状态。其次,我认为如果你不使用标准的状态单子,而是自己重新实现所有的状态单子及其操作,以及它们的类型,你会有更好的运气。我想你会通过这种方式学到更多。这里有一些声明可以帮助您入门:

    data MyState s a = MyState (s -> (s, b))
    
    get :: Mystate s s
    put :: s -> Mystate s ()
    

    然后你就可以自己写连接词了:

    unit :: a -> Mystate s a
    bind :: Mystate s a -> (a -> Mystate s b) -> Mystate s b
    

    终于

    data Seed = Seed Int
    nextVal :: Mystate Seed Bool
    

    至于脱糖的麻烦,您使用的do 表示法非常复杂。 但是脱糖是一个一次的机械过程。据我所知,你的代码应该像这样去糖(回到你原来的类型和代码,我不同意):

     nextVal = get >>= \ Random seed val ->
                          let seed' = updateSeed seed
                              val'  = even seed'
                          in  put (Random seed' val') >>= \ _ -> return val'
    

    为了使嵌套结构更清晰,我对缩进做了很大的改动。

    【讨论】:

      【解决方案2】:

      State monad 乍一看确实有点令人困惑。让我们按照 Norman Ramsey 的建议进行操作,并逐步了解如何从头开始实施。警告,这很长!

      首先,State 有两个类型参数:包含的状态数据的类型计算的最终结果的类型。我们将在这里分别使用stateDataresult 作为它们的类型变量。如果您考虑一下,这是有道理的;基于状态的计算的定义特征是它在产生输出的同时修改状态。

      不太明显的是,类型构造函数将 函数 从状态转换为修改后的状态和结果,如下所示:

      newtype State stateData result = State (stateData -> (result, stateData))
      

      因此,虽然 monad 被称为“状态”,但被 monad 包裹的实际值是基于状态的计算的值,而不是包含状态的实际值。

      记住这一点,我们不应该惊讶地发现用于在 State monad 中执行计算的函数 runState 实际上只不过是包装函数本身的访问器,可以这样定义:

      runState (State f) = f
      

      那么当你定义一个返回状态值的函数时,这意味着什么?让我们暂时忽略 State 是一个 monad 的事实,只看底层的类型。首先,考虑这个函数(它实际上对状态没有任何作用):

      len2State :: String -> State Int Bool
      len2State s = return ((length s) == 2)
      

      如果看State的定义,我们可以看到这里stateData的类型是Int,而result的类型是Bool,所以数据构造函数包装的函数必须有类型Int -&gt; (Bool, Int)。现在,想象一下len2State 的无状态版本——显然,它的类型为String -&gt; Bool。那么如何将这样的函数转换为返回适合状态包装器的值的函数呢?

      嗯,很明显,转换后的函数需要接受第二个参数,Int 代表状态值。它还需要返回一个状态值,另一个Int。因为我们实际上并没有对这个函数中的状态做任何事情,所以让我们做一件显而易见的事情——直接传递那个 int。这是一个有状态的函数,根据无状态版本定义:

      len2 :: String -> Bool
      len2 s = ((length s) == 2)
      
      len2State :: String -> (Int -> (Bool, Int))
      len2State s i = (len2' s, i)
      

      但这有点愚蠢和多余。让我们对转换进行概括,以便我们可以传入结果值,并将任何内容转换为类似 State 的函数。

      convert :: Bool -> (Int -> (Bool, Int))
      convert r d = (r, d)
      
      len2 s = ((length s) == 2)
      
      len2State :: String -> (Int -> (Bool, Int))
      len2State s = convert (len2 s)
      

      如果我们想要一个改变状态的函数怎么办?显然我们不能用convert 构建一个,因为我们写它是为了传递状态。让我们保持简单,并编写一个函数来用新值覆盖状态。它需要什么样的类型?它需要一个Int 来表示新的状态值,当然还必须返回一个函数stateData -&gt; (result, stateData),因为这正是我们的状态包装器所需要的。覆盖状态值在状态计算之外并没有真正有意义的result 值,所以我们这里的结果将只是(),在Haskell 中表示“无值”的零元素元​​组。

      overwriteState :: Int -> (Int -> ((), Int))
      overwriteState newState _ = ((), newState)
      

      这很容易!现在,让我们实际做一些事情 使用该状态数据。让我们将上面的len2State 改写成更合理的形式:我们将字符串长度与当前状态值进行比较。

      lenState :: String -> (Int -> (Bool, Int))
      lenState s i = ((length s) == i, i)
      

      我们可以像以前那样将其概括为转换器和无状态函数吗?没那么容易。我们的len 函数需要将状态作为参数,但我们不希望它“了解”状态。确实很尴尬。但是,我们可以编写一个快速的辅助函数来为我们处理所有事情:我们将给它一个需要使用状态值的函数,它会将值传入,然后将所有内容打包成一个状态形函数离开len 没有比这更明智的了。

      useState :: (Int -> Bool) -> Int -> (Bool, Int)
      useState f d = (f d, d)
      
      len :: String -> Int -> Bool
      len s i = (length s) == i
      
      lenState :: String -> (Int -> (Bool, Int))
      lenState s = useState (len s)
      

      现在,棘手的部分——如果我们想将这些函数串在一起怎么办?假设我们想在一个字符串上使用lenState,然后如果结果为假,则将状态值加倍,然后再次检查字符串,如果任何一个检查成功,最后返回真。我们拥有完成这项任务所需的所有部分,但全部写出来会很痛苦。我们可以创建一个函数,自动将两个函数链接在一起,每个函数都返回类似状态的函数吗?确定的事!我们只需要确保它接受两件事作为参数:第一个函数返回的 State 函数,以及一个将前一个函数的 result 类型作为参数的函数。让我们看看结果如何:

      chainStates :: (Int -> (result1, Int)) -> (result1 -> (Int -> (result2, Int))) -> (Int -> (result2, Int))
      chainStates prev f d = let (r, d') = prev d
                             in f r d'
      

      所有这一切都是将第一个状态函数应用于一些状态数据,然后将第二个函数应用于结果和修改后的状态数据。很简单吧?

      现在,有趣的部分是:在chainStatesconvert 之间,我们几乎可以将无状态函数的任意组合变成启用状态的函数!我们现在唯一需要的是替换useState,它返回状态数据作为结果,以便chainStates 可以将它传递给对我们正在使用的技巧一无所知的函数。此外,我们将使用 lambdas 来接受前面函数的结果并给它们临时名称。好的,让我们实现这一点:

      extractState :: Int -> (Int, Int)
      extractState d = (d, d)
      
      chained :: String -> (Int -> (Bool, Int))
      chained str = chainStates  extractState         $ \state1 ->
                    let check1 = (len str state1) in
                    chainStates (overwriteState (
                        if check1 
                        then state1 
                        else state1 * 2))             $ \ _ ->
                    chainStates  extractState         $ \state2 ->
                    let check2 = (len str state2) in
                    convert (check1 || check2)
      

      并尝试一下:

      > chained "abcd" 2
      (True, 4)
      > chained "abcd" 3
      (False, 6)
      > chained "abcd" 4
      (True, 4)
      > chained "abcdef" 5
      (False, 10)
      

      当然,我们不能忘记 State 实际上是一个 monad,它封装了类似 State 的函数并让我们远离它们,所以我们构建的任何漂亮函数都无法帮助我们处理真实的事情。或者他们会?令人震惊的是,事实证明,真正的 State monad 提供了所有相同的功能,但名称不同:

      runState (State s) = s
      return r = State (convert r)
      (>>=) s f = State (\d -> let (r, d') = (runState s) d in
                               runState (f r) d')
      get = State extractState
      put d = State (overwriteState d)
      

      请注意,>>= 几乎与chainStates 相同,但没有使用chainStates 定义它的好方法。因此,总结一下,我们可以使用 real 状态重写最后一个示例:

      chained str = get                               >>= \state1 ->
                    let check1 = (len str state1) in
                    put (if check1 
                        then state1 else state1 * 2)  >>= \ _ ->
                    get                               >>= \state2 ->
                    let check2 = (len str state2) in
                    return (check1 || check2)
      

      或者,所有都用等效的 do 表示法:

      chained str = do
              state1 <- get
              let check1 = len str state1
              _ <- put (if check1 then state1 else state1 * 2)
              state2 <- get
              let check2 = (len str state2)
              return (check1 || check2)
      

      【讨论】:

      • 谢谢你,写作也很愉快,虽然它可能需要更多的工作。我担心人们可能会因为篇幅太长而被推迟,但我想不会……
      【解决方案3】:

      您收到了一些很棒的回复。我在使用 State monad 时所做的就是将 State s a 替换为 s -&gt; (s,a)(毕竟,这就是它的本质)。

      然后你会得到一个如下所示的绑定类型:

      (>>=) :: (s -> (s,a)) ->
               (a -> s -> (s,b)) ->
               (s -> (s,b))
      

      您会看到 bind 只是一种特殊的函数组合运算符,例如 (.)

      我写了一篇关于状态单子here 的博客/教程。它可能不是特别好,但通过编写它帮助我更好地理解了一些事情。

      【讨论】:

      • 指向您博客的超链接似乎已损坏;您有更新的内容想与我们分享吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多