【问题标题】:Haskell random numbersHaskell 随机数
【发布时间】:2016-05-28 09:02:40
【问题描述】:

我正在尝试在 Haskell 中生成一个随机数样本

import System.Random

getSample n = take n $ randoms g where
    g = newStdGen

但我似乎并没有以正确的方式使用newStdGen。我错过了什么?

【问题讨论】:

    标签: haskell


    【解决方案1】:

    首先,您可能不想使用newStdGen。最大的问题是每次运行程序时都会得到不同的种子,因此没有可重现的结果。在我看来,mkStdGen 是一个更好的选择,因为它需要你给它一个种子。这意味着您每次都会得到相同的(伪)随机数序列。如果您想要不同的序列,只需更改种子即可。

    newStdGen 的第二个问题是,由于它不纯,你最终会进入 IO monad,这可能有点不方便。

    sample :: Int -> IO [Int]
    sample n = do
      gen <- newStdGen
      return $ take n $ randoms gen
    

    您可以使用 do-notation 来“提取”这些值,然后对它们求和:

    main :: IO ()
    main = do
      xs <- sample 10
      s = sum xs
      print s
    

    或者您可以将函数“映射”到结果上(但请注意,在某些时候您可能需要提取值):

    main :: IO ()
    main = do
      s <- fmap sum $ sample 10
      print s
    

    fmap 函数是map 的通用版本。就像map 将函数应用于列表中的值一样,fmap 可以将函数应用于IO 中的值。

    这个sample 函数的另一个问题是,如果我们再次调用它,它会从一个新的种子开始,而不是继续之前的(伪)随机序列。同样,这使得重现结果变得不可能。为了解决这个问题,我们需要传入种子并返回一个新种子。不幸的是,randoms 不会为我们返回下一个种子,所以我们必须使用 random 从头开始​​编写。

    sample :: Int -> StdGen -> ([Int],StdGen)
    sample n seed1 = case n of
      0 -> ([],seed1)
      k -> let (rs,seed2) = sample (k-1) seed1
               (r, seed3) = random seed2
            in ((r:rs),seed3)
    

    我们现在的主要功能是

    main :: IO ()
    main = do
      let seed1 = mkStdGen 123456
          (xs,seed2) = sample 10 seed1
          s = sum xs
          (ys,seed3) = sample 10 seed2
          t = sum ys
      print s
      print t
    

    我知道这似乎只是为了使用随机数而做的大量工作,但这样做的好处是值得的。我们可以使用单个种子生成所有随机性,从而保证结果可以重现。

    当然,这是 Haskell,我们可以利用 Monads 摆脱种子值的所有手动线程。这是一种稍微高级一点的方法,但非常值得学习,因为 monad 在 Haskell 代码中无处不在。

    我们需要这些导入:

    import System.Random
    import Control.Monad
    import Control.Applicative
    

    然后我们将创建一个新类型,它表示将种子转化为值和下一个种子的动作。

    newtype Rand a = Rand { runRand :: StdGen -> (a,StdGen) }
    

    我们需要 FunctorApplicative 实例,否则 GHC 会报错,但我们可以避免在此示例中实现它们。

    instance Functor Rand
    instance Applicative Rand
    

    现在是 Monad 实例。这就是魔法发生的地方。 &gt;&gt;= 函数(称为 bind)是我们指定如何通过计算线程化种子值的地方。

    instance Monad Rand where
      return x = Rand ( \seed -> (x,seed) )
      ra >>= f = Rand ( \s1 -> let (a,s2) = runRand ra s1
                               in  runRand (f a) s2 )
    
    newRand :: Rand Int
    newRand = Rand ( \seed -> random seed )
    

    现在我们的sample 函数非常简单!我们可以利用Control.Monad 中的replicateM,它重复给定的操作并将结果累积到一个列表中。所有具有种子价值的有趣业务都在幕后处理

    sample :: Int -> Rand [Int]
    sample n = replicateM n newRand
    
    main :: IO ()
    main = do
      let seed1 = mkStdGen 124567
          (xs,seed2) = runRand (sample 10) seed1
          s = sum xs
      print s
    

    如果我们需要多次生成随机值,我们甚至可以留在 Rand monad 中。

    main :: IO ()
    main = do
      let seed1 = mkStdGen 124567
          (xs,seed2) = flip runRand seed1 $ do
            x <- newRand
            bs <- sample 5
            cs <- sample 10
            return $ x : (bs ++ cs)
          s = sum xs
      print s
    

    我希望这会有所帮助!

    【讨论】:

    • 谢谢!这是一件让我感到困惑的事情:我从一个需要随机数列表作为输入的纯函数开始(基本上是在做一个sum $ map someFunction randomNumbers)。但是,上面生成的随机数是一元的(这是一个词吗?),似乎我不能把它们放在我的纯函数中。在 Haskell 中解决此问题的正确方法是什么?
    • 我认为没有一种“正确”的方法可以做到这一点。我在上面概述了我的个人偏好。您可能对解决当前问题的“最方便”的方法更感兴趣。在这种情况下,尝试fmap sum $ map someFunction randomNumbers,或使用mkStdGen 123456 代替newStdGen。 (123456 可以是任意整数)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-01-15
    • 1970-01-01
    • 2015-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-25
    相关资源
    最近更新 更多