【问题标题】:How can I initialize state in a hidden way in Haskell (like the PRNG does)?如何在 Haskell 中以隐藏的方式初始化状态(就像 PRNG 一样)?
【发布时间】:2009-07-13 18:46:58
【问题描述】:

我浏览了一些关于 State monad 的教程,我想我明白了。

比如this nice tutorial

import Data.Word

type LCGState = Word32

lcg :: LCGState -> (Integer, LCGState)
lcg s0 = (output, s1) 
  where s1 = 1103515245 * s0 + 12345
        output = fromIntegral s1 * 2^16 `div` 2^32


getRandom :: State LCGState Integer
getRandom = get >>= \s0 -> let (x,s1) = lcg s0
                           in put s1 >> return x

好的,所以我可以使用 getRandom:

*Main> runState getRandom 0
(0,12345)
*Main> runState getRandom 0
(0,12345)
*Main> runState getRandom 1              
(16838,1103527590)

但是每次调用时我仍然需要将种子传递给 PRNG。我知道 Haskell 实现中可用的 PRNG 不需要:

Prelude> :module Random
Prelude Random> randomRIO (1,6 :: Int)
(...) -- GHC prints some stuff here
6
Prelude Random> randomRIO (1,6 :: Int)
1

所以我可能误解了 State monad,因为我在大多数教程中都能看到 似乎不是“持久”状态,而只是线程状态的一种便捷方式。

所以...我怎样才能拥有自动初始化的状态(可能来自某些 使用时间和其他不太可预测的数据的函数),例如 Random 模块 有吗?

非常感谢!

【问题讨论】:

    标签: haskell monads state-monad


    【解决方案1】:

    randomRIO 使用 IO 单子。这似乎在解释器中工作得很好,因为解释器也可以在 IO monad 中工作。这就是您在示例中看到的内容;你实际上不能在代码的顶层做到这一点——你必须像所有的单子一样把它放在一个 do-expression 中。

    在一般代码中,您应该避免使用 IO monad,因为一旦您的代码使用了 IO monad,它就会永远与外部状态相关联——您无法摆脱它(即,如果您有使用 IO monad 的代码,任何调用它的代码也必须使用 IO monad;没有安全的方法可以“摆脱”它)。所以IO monad 应该只用于诸如访问外部环境之类的事情,这是绝对需要的事情。

    对于像本地独立状态这样的事情,你不应该使用 IO monad。您可以使用前面提到的State monad,也可以使用ST monad。 ST monad 包含许多与 IO monad 相同的特性;即有STRef 可变单元格,类似于IORef。与 IO 相比,ST 的好处在于,当你完成后,你可以在 ST monad 上调用 runST 以从 monad 中获取计算结果,而这是 IO 无法做到的。

    至于“隐藏”状态,这只是 Haskell 中用于 monad 的 do 表达式语法的一部分。如果您认为需要显式传递状态,那么您没有正确使用 monad 语法。

    这是在 IO Monad 中使用 IORef 的代码:

    import Data.IORef
    foo :: IO Int -- this is stuck in the IO monad forever
    foo = do x <- newIORef 1
             modifyIORef x (+ 2)
             readIORef x
    -- foo is an IO computation that returns 3
    

    这是使用 ST monad 的代码:

    import Control.Monad.ST
    import Data.STRef
    bar :: Int
    bar = runST (do x <- newSTRef 1
                    modifySTRef x (+ 2)
                    readSTRef x)
    -- bar == 3
    

    代码的简洁性本质上是一样的;除了在后一种情况下,我们可以从 monad 中获取值,而在前一种情况下,我们不能不将其放入另一个 IO 计算中。

    【讨论】:

    • 谢谢!这很有帮助。关于你的评论,“在前者我们不能不把它放在另一个 IO 计算中”——我想这就是为什么当你结合几个 monad(使用 monad 转换器)时 IO monad 需要成为最里面的原因?
    • 现在我想起来了,PRNG 必然会使用 IO monad,对吧?它需要熵,所以它需要来自“某处”的数据。它实际上需要至少一个引用不透明的函数(否则它就没有用处,例如,对于加密代码)! :-)
    【解决方案2】:
    secretStateValue :: IORef SomeType
    secretStateValue = unsafePerformIO $ newIORef initialState
    {-# NOINLINE secretStateValue #-}
    

    现在在 IO monad 中使用普通的readIORef and writeIORef 访问您的 secretStateValue。

    【讨论】:

    • 说到这里,Random 使用atomicModifyIORef 而不是readIORef + writeIORef 对;这样做似乎是个好主意。
    • 确实,如果要从多个线程访问使用 atomicModifyIORef 是一个更好的主意。
    【解决方案3】:

    所以我可能误解了 State monad,因为我在大多数教程中看到的似乎不是“持久”状态,而只是线程状态的一种便捷方式。

    state monad 正是关于通过某个范围的线程状态。

    如果您想要顶级状态,那是语言之外的(并且您必须使用全局可变变量)。请注意这可能会使您的代码的线程安全变得复杂——该状态是如何初始化的?什么时候?以及通过哪个线程?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-05-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多