代码之家  ›  专栏  ›  技术社区  ›  Rafael S. Calsaverini

状态单元、随机数序列和单元码

  •  16
  • Rafael S. Calsaverini  · 技术社区  · 16 年前

    我试图掌握状态单子,为此我想编写一个单子代码,使用线性同余生成器生成随机数序列(可能不太好,但我的目的只是学习状态单子,而不是构建一个好的RNG库)。

    Bool (为了简单起见):

    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 
    

    不要担心数字,这只是种子的更新规则(根据数字配方),应该生成一个伪随机序列 Int

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

    好的,我可以通过使用状态单子来避免这个样板:

    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个伪随机数的列表 布尔

    我可以阅读我所做的(主要基于以下示例: http://www.haskell.org/pipermail/beginners/2008-September/000275.html

    有人能帮我解决一些疑问吗?

    1-我曾尝试对nextVal函数进行去语法处理,以了解它的功能,但我做不到。我可以猜它提取当前状态,更新它,然后将状态传递给下一个计算,但这只是基于阅读这个dosugar,就像它是英语一样。

    如何真正将此函数分解为原始函数>>=和逐步返回函数?

    put get 函数可以。我可以猜到,他们“打包”和“解包”的状态。但对我来说,做糖背后的机制仍然是难以捉摸的。

    好的,关于这个代码的任何其他一般性评论都是非常受欢迎的。我有时觉得Haskell可以创建一个有效的代码,并做我期望它做的事情,但我不能像我习惯于使用命令式程序那样“遵循评估”。

    3 回复  |  直到 16 年前
        1
  •  31
  •   C. A. McCann Ravikant Cherukuri    16 年前

    首先,State有两个类型参数: 以及 计算的最终结果 . 我们将使用 stateData result 分别作为它们的类型变量。如果你仔细想想,这是有道理的;基于状态的计算的定义特征是,它在生成输出时修改状态。

    不太明显的是,类型构造函数采用 功能 从状态到修改状态和结果,如下所示:

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

    因此,虽然monad被称为“State”,但由monad包装的实际值是 基于状态的计算

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

    runState (State f) = f
    

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

    如果你看国家的定义,我们可以看到 状态数据 Int ,以及 Bool ,因此数据构造函数包装的函数必须具有 Int -> (Bool, Int) . 现在,想象一个无状态版本的 len2State String -> Bool . 那么,您将如何将这样一个函数转换为一个返回适合状态包装器的值的函数呢?

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

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

    但这有点愚蠢和多余。让我们概括一下转换,这样我们就可以传入结果值,并将任何内容转换为类似状态的函数。

    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 -> (result, stateData) 结果 值在状态计算之外,所以我们这里的结果是 ()

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

    那很容易!现在,让我们实际上 用那个州的数据。让我们重写一下 len2State 从上面的内容,我们将比较字符串长度和当前状态值。

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

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

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

    现在,有趣的部分:在 chainStates 转换 useState 这将返回状态数据作为其结果,这样ChainState就可以将其传递给那些不知道我们正在使用的技巧的函数。此外,我们将使用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)
    

    当然,我们不能忘记,状态实际上是一个单子,它包装了类似于状态的函数,使我们远离它们,因此我们所构建的漂亮函数中没有一个能帮助我们处理真实的事情。还是会?令人震惊的是,事实证明,真实状态的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)
    

    声明:

    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)
    
        2
  •  8
  •   Norman Ramsey    16 年前

    首先,您的示例过于复杂,因为它不需要存储 val 在单子状态下;只有种子是持久状态。第二,我认为如果您不使用标准的state monad,而是自己用它们的类型重新实现所有state monad及其操作,您的运气会更好。我想这样你会学到更多。以下是一些让您开始学习的声明:

    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'
    

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

        3
  •  5
  •   jberryman    16 年前

    你有两个很好的回答。当我使用状态单子时,我会做些什么 State s a 具有 s -> (s,a) (毕竟,事实就是这样)。

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

    你可以看到bind只是一种特殊的函数组合操作符,比如 (.)

    我写了一篇关于国家单子的博客/教程 here . 它可能不是特别好,但通过写它帮助我更好地探索事物。

    推荐文章