17

我试图掌握状态单子,为此我想编写一个单子代码,该代码将使用线性同余生成器生成一系列随机数(可能不好,但我的目的只是学习状态单子,而不是建立一个好的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)

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

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

任何人都可以帮助我解决一些疑问吗?

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

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

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

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

4

3 回答 3

32

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

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

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

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

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

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

runState (State f) = f

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

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

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

好吧,很明显,转换后的函数将需要第二个参数,一个Int代表状态值的参数。它还需要返回一个状态值 another 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 -> (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'

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

现在,有趣的部分:在chainStates和之间convert,我们应该几乎可以将无状态函数的任意组合变成启用状态的函数!我们现在唯一需要的是一个替换,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 定义它的好方法。因此,总结一下,我们可以使用真实状态重写最后一个示例:

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)
于 2009-12-24T08:28:21.190 回答
8

首先,您的示例过于复杂,因为它不需要将 存储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'

为了使嵌套结构更清晰一点,我对缩进采取了主要的自由。

于 2009-12-24T03:46:08.330 回答
5

你有几个很好的回应。我在使用 State monad 时所做的事情在我的脑海中被替换State s as -> (s,a)(毕竟,这就是它的本质)。

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

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

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

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

于 2009-12-25T19:43:15.520 回答