跳转至内容

Haskell/库/随机数

来自维基教科书,开放书籍,开放世界

随机数示例

[编辑 | 编辑源代码]

随机数有许多用途。

示例: 十个随机整数

import System.Random

main = do
   gen <- newStdGen
   let ns = randoms gen :: [Int]
   print $ take 10 ns

IO 动作 newStdGen 创建一个新的 StdGen 伪随机数生成器状态。此 StdGen 可以传递给需要生成伪随机数的函数。

(还存在一个全局随机数生成器,它在系统依赖的方式中自动初始化。此生成器在 IO 单元中维护,可以使用 getStdGen 访问。这可能是一个库问题,因为实际上您唯一真正需要的是 newStdGen。)

或者,可以使用 mkStdGen

示例: 使用 mkStdGen 生成十个随机浮点数

import System.Random

randomList :: (Random a) => Int -> [a]
randomList seed = randoms (mkStdGen seed)

main :: IO ()
main = do print $ take 10 (randomList 42 :: [Float])

运行此脚本将产生如下输出

[0.110407025,0.8453985,0.3077821,0.78138804,0.5242582,0.5196911,0.20084688,0.4794773,0.3240164,6.1566383e-2]

示例: 对列表进行乱序排列(不完美地)

import Data.List ( sortBy )
import Data.Ord ( comparing )
import System.Random ( Random, RandomGen, randoms, newStdGen )

main :: IO ()
main =
 do gen <- newStdGen
    interact $ unlines . unsort gen . lines

unsort :: (RandomGen g) => g -> [x] -> [x]
unsort g es = map snd . sortBy (comparing fst) $ zip rs es
  where rs = randoms g :: [Integer]

随机数生成比 randoms 更复杂。例如,您可以使用 random(没有 's')生成单个随机数以及一个新的 StdGen,用于生成下一个随机数。此外,randomR 和 randomRs 会接受一个参数来指定范围。请参见下文了解更多想法。

标准随机数生成器

[编辑 | 编辑源代码]

Haskell 标准随机数函数和类型在 System.Random 模块中定义。随机数的定义 很难理解,因为它使用类使其更通用。

来自标准

 ---------------- The RandomGen class ------------------------
 
 class RandomGen g where
   genRange :: g -> (Int, Int)
   next     :: g -> (Int, g)
   split    :: g -> (g, g)
 
 ---------------- A standard instance of RandomGen -----------
 data StdGen = ... -- Abstract

这基本上引入了 StdGen,即标准随机数生成器“对象”。它是 RandomGen 类的实例,该类指定其他伪随机数生成器库需要实现的操作才能与 System.Random 库一起使用。

给定 r :: StdGen,您可以说

(x, r2) = next r

这将为您提供一个随机整数 x 和一个新的 StdGen r2。next 函数在 RandomGen 类中定义,您可以将其应用于 StdGen 类型的东西,因为 StdGen 是 RandomGen 类的实例,如下所示。

来自标准

instance RandomGen StdGen where ...
instance Read      StdGen where ...
instance Show      StdGen where ...

这也表示您可以将 StdGen 转换为字符串,反之亦然。(点不是 Haskell 语法;它们只是表示标准没有定义这些实例的实现。)

来自标准

mkStdGen :: Int -> StdGen

将种子 Int 传递给 mkStdGen 函数,您将获得一个生成器。

作为一门函数式编程语言,Haskell 使用 next 返回一个新的随机数生成器。在使用可变变量的语言中,随机数生成器例程具有隐藏的副作用,即更新生成器状态以备下次调用。Haskell 不会这样做。如果您想在 Haskell 中生成三个随机数,您需要说类似的话

let
   (x1, r2) = next r
   (x2, r3) = next r2
   (x3, r4) = next r3

随机值 (x1, x2, x3) 本身是随机整数。要获取某个范围内的值,例如 (0,999),应该有一个基于此的库例程,实际上确实有

来自标准

---------------- The Random class ---------------------------
class Random a where
   randomR :: RandomGen g => (a, a) -> g -> (a, g)
   random  :: RandomGen g => g -> (a, g)

   randomRs :: RandomGen g => (a, a) -> g -> [a]
   randoms  :: RandomGen g => g -> [a]

   randomRIO :: (a,a) -> IO a
   randomIO  :: IO a

请记住,StdGen 是 RandomGen 类型(除非您自己编写随机数生成器)的唯一实例。因此,您可以将 StdGen 替换为上面类型中的 'g',并得到以下结果

   randomR :: (a, a) -> StdGen -> (a, StdGen)
   random  :: StdGen -> (a, StdGen)

   randomRs :: (a, a) -> StdGen -> [a]
   randoms  :: StdGen -> [a]

但请记住,这全部都在 *另一个* 类声明“Random”中。这全部表示:Random 的任何实例都可以使用这些函数。Standard 中 Random 的实例是

instance Random Integer where ...
instance Random Float   where ...
instance Random Double  where ...
instance Random Bool    where ...
instance Random Char    where ...

因此,对于这些类型中的任何一个,您都可以获得一个随机范围。您可以使用以下命令获取一个随机整数

(x1, r2) = randomR (0,999) r

您可以使用以下命令获取一个随机大写字符

(c2, r3) = randomR ('A', 'Z') r2

您甚至可以使用以下命令获取一个随机位

(b3, r4) = randomR (False, True) r3

到目前为止一切都很好,但将随机数状态像这样贯穿整个程序很痛苦,容易出错,并且通常会破坏程序的简洁明了的函数属性。

一种部分解决方案是 RandomGen 类中的“split”函数。它接受一个生成器,并返回两个生成器。这让你可以说类似的话

(r1, r2) = split r
x = foo r1

在这种情况下,我们将 r1 传递到函数 foo 中,该函数使用它进行随机操作并返回结果“x”。然后我们可以将“r2”作为接下来要进行的操作的随机数生成器。如果没有“split”,我们将不得不写

(x, r2) = foo r1

但是这也很笨拙。我们可以通过将所有内容都放在 IO 单元中来快速且脏的方式来做到这一点。因此,我们获得了与任何其他语言相同的标准全局随机数生成器。

来自标准

---------------- The global random generator ---------------- 
newStdGen    :: IO StdGen 
setStdGen    :: StdGen -> IO ()
getStdGen    :: IO StdGen
getStdRandom :: (StdGen -> (a, StdGen)) -> IO a

我们可以写

foo :: IO Int
foo = do
   r1 <- getStdGen
   let (x, r2) = randomR (0,999) r1
   setStdGen r2
   return x

这将获取全局生成器,使用它,然后更新它(否则每个随机数都将相同)。但是每次使用它都要获取和更新全局生成器很麻烦,因此更常见的是使用 getStdRandom。该函数的参数是一个函数。将该函数的类型与 'random' 和 'randomR' 的类型进行比较。它们都非常适合。要在 IO 单元中获得一个随机整数,您可以说

x <- getStdRandom $ randomR (1,999)

'randomR (1,999) 的类型为 StdGen -> (Int, StdGen),因此它可以直接作为 getStdRandom 所需的参数。

使用 QuickCheck 生成随机数据

[编辑 | 编辑源代码]

只能通过 IO 单元进行随机数有点麻烦。您会发现代码中的某个函数需要一个随机数,突然之间您必须将一半的程序重写为 IO 动作而不是简洁的纯函数,或者您需要一个 StdGen 参数来将其传递到所有更高级别的函数中。我们更希望有些更纯净的东西。

回想一下 状态单元 章节,模式像

let
   (x1, r2) = next r
   (x2, r3) = next r2
   (x3, r4) = next r3

可以使用“do”表示法实现

   do -- Not real Haskell
      x1 <- random
      x2 <- random
      x3 <- random

当然,您可以在 IO 单元中做到这一点,但如果随机数有自己的专门用于随机计算的单元,那会更好。碰巧的是,这样的单元确实存在。它位于 Test.QuickCheck 模块中,被称为 Gen

Gen 位于 Test.QuickCheck 中的原因是历史性的:它是 QuickCheck 的发明地。QuickCheck 的目的是生成随机单元测试来验证代码的属性。(顺便说一句,QuickCheck 效果非常好,大多数 Haskell 开发人员使用它进行测试)。有关更多详细信息,请参见 HaskellWiki 上的 QuickCheck 简介。本教程将集中介绍如何使用 Gen 单元生成随机数据。

要使用 QuickCheck 模块,您需要安装 QuickCheck 包。安装完成后,只需将

import Test.QuickCheck

放在您的源文件中。

Gen 单元可以看作是随机计算的单元。除了生成随机数外,它还提供了一个函数库,这些函数可以将复杂的值从简单值构建起来。

因此,让我们从一个返回 0 到 999 之间的三个随机整数的例程开始

randomTriple :: Gen (Integer, Integer, Integer)
randomTriple = do
   x1 <- choose (0,999)
   x2 <- choose (0,999)
   x3 <- choose (0,999)
   return (x1, x2, x3)

choose 是 QuickCheck 中的函数之一。它等效于 randomR

choose :: Random a => (a, a) -> Gen a

换句话说,对于任何类型为“a”的类型,该类型是“Random”的实例(见上文),choose 将范围映射到生成器。

一旦您拥有 Gen 动作,您就必须执行它。

unGen 动作执行操作并返回随机结果

unGen :: Gen a -> StdGen -> Int -> a

三个参数是

  1. 生成器动作。
  2. 一个随机数生成器。
  3. 结果的“大小”。这在上面的示例中没有使用,但是如果您生成一个具有可变数量元素的数据结构(例如列表),那么此参数将允许您将某个预期大小传递到生成器中。我们将在后面的示例中看到。

例如,这会生成三个任意数字

let
   triple = unGen randomTriple (mkStdGen 1) 1

但是这些数字将始终相同,因为使用了相同的种子值!如果您想要 *不同的* 数字,则必须使用不同的 StdGen 参数。

大多数编程语言中的一种常见模式涉及随机数生成器在两种操作方案之间进行选择

-- Not Haskell code
r := random (0,1)
if r == 1 then foo else bar

QuickCheck 提供了一种更具声明性的方式来做同样的事情。如果 foobar 都是返回相同类型的生成器,那么我们可以说

oneof [foo, bar]

这有相同的机会返回 foo 或 "bar 如果您想要不同的几率,那么您可以说类似的话

frequency [ (30, foo), (70, bar) ]

oneof 接受一个简单的 Gen 动作列表,并随机选择其中之一。frequency 做类似的事情,但每个项目的概率由关联的权重给出。

oneof :: [Gen a] -> Gen a
frequency :: [(Int, Gen a)] -> Gen a
华夏公益教科书