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
所需的参数。
只能通过 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
三个参数是
- 生成器动作。
- 一个随机数生成器。
- 结果的“大小”。这在上面的示例中没有使用,但是如果您生成一个具有可变数量元素的数据结构(例如列表),那么此参数将允许您将某个预期大小传递到生成器中。我们将在后面的示例中看到。
例如,这会生成三个任意数字
let
triple = unGen randomTriple (mkStdGen 1) 1
但是这些数字将始终相同,因为使用了相同的种子值!如果您想要 *不同的* 数字,则必须使用不同的 StdGen
参数。
大多数编程语言中的一种常见模式涉及随机数生成器在两种操作方案之间进行选择
-- Not Haskell code r := random (0,1) if r == 1 then foo else bar
QuickCheck
提供了一种更具声明性的方式来做同样的事情。如果 foo
和 bar
都是返回相同类型的生成器,那么我们可以说
oneof [foo, bar]
这有相同的机会返回 foo
或 "bar
如果您想要不同的几率,那么您可以说类似的话
frequency [ (30, foo), (70, bar) ]
oneof
接受一个简单的 Gen 动作列表,并随机选择其中之一。frequency
做类似的事情,但每个项目的概率由关联的权重给出。
oneof :: [Gen a] -> Gen a frequency :: [(Int, Gen a)] -> Gen a