(译) Haskell 中随机数的使用

随机数(我指的是伪随机数)是经过显式或隐式的状态来生成的。这意味着在 Haskell 中,随机数的使用(经过 System.Random 库)是伴随着状态的传递的。 html

大部分须要得到帮助的人都有命令式编程的背景,所以,我会先用命令式的方式,而后再用函数式的方式来教你们在 Haskell 中使用随机数。git

任务

我会生成知足如下条件的随机列表:github

  • 列表长度是 1 到 7编程

  • 列表中的每一项都是 0.0 到 1.0 之间的浮点数并发

命令式

在 IO monad 中有一个全局的生成器,你能够初始化它,而后获取随机数。下面有一些经常使用的函数:dom

setStdGen :: StdGen -> IO ()

初始化或者设置全局生成器,咱们能够用 mkStdGen 来生成随机种子。所以,有一个很傻瓜式的用法:函数

setStdGen (mkStdGen 42)

固然,你能够用任意的 Int 来替换 42编码

其实,你能够选择是否调用 setStdGen,若是你不调用的话,全局的生成器仍是可用的。由于在 runtime 会在启动的时候用一个任意的种子去初始化它,因此每次启动的时候,都会有一个不一样的种子。.net

randomRIO :: (Random a) => (a,a) -> IO a

在给定范围随机返回一个类型为 a 的值,同时全局生成器也会更新。你能够经过一个元组来指定范围。下面这个例子会返回 az 之间的随机值(包含 az):code

c <- randomRIO ('a', 'z')

a 能够是任意类型吗?并不是如此。在 Haskell 98 标准中, Random 库只支持 Bool, Char, Int, Integer, Float, Double(你能够本身去扩展这个支持的范围,但这是另一个话题了)。

randomIO :: (Random a) => IO a

返回一个类型为 a 的随机数(a 能够是任意类型吗?看上文),全局的生成器也会更新。下面这个例子会返回一个 Double 类型的随机数:

x <- randomIO :: IO Double

随机数返回的范围由类型决定。

须要注意的是,这些都是 IO 函数,所以你只能够在 IO 函数中使用它们。换句话说,若是你写了一个要使用它们的函数,它的返回类型也会变成是 IO 函数。

举个例子,上面提到的代码片断都要写在 do block 中。这只是一个提醒,由于咱们想要用命令式的方式来生成随机数。

下面这个例子展现如何在 IO monad 中完成以前的任务:

import System.Random

main = do
    setStdGen (mkStdGen 42)  -- 这步是可选的,若是有这一步,你每一次运行的结果都是同样的,由于随机种子固定是 42
    s <- randomStuff
    print s

randomStuff :: IO [Float]
randomStuff = do
    n <- randomRIO (1, 7)
    sequence (replicate n (randomRIO (0, 1)))

纯函数式

你可能有如下缘由想知道如何用函数式的方式生成随机数:

  • 你有好奇心

  • 你不想用 IO monad

  • 由于一些并发或者其余缘由,你想几个生成器同时存在,共享全局生成器不能解决你的问题

实际上,有两种方法来用函数式的方式去生成随机数:

  • 从 stream(无限列表) 中提取随机数

  • 把生成器当成函数参数的一部分,而后返回随机数

这里有一些经常使用的函数用来建立生成器和包含随机数的无限列表。

mkStdGen :: Int -> StdGen

用随机种子建立生成器。

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

用生成器生成给定范围的无限列表。例子:用 42 做为随机种子,返回 az 之间包含 az 的无限列表:

randomRs ('a', 'z') (mkStdGen 42)

类型 a 是随机数的类型。类型 g 看起来是通用的,但实际上它老是 StdGen

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

用给定的生成器生成随机数的无限列表。例如:用 42 做为随机种子生成 Double 类型的列表:

randoms (mkStdGen 42) :: [Double]

随机数的范围由类型决定,你须要查文档来肯定具体范围,或者直接用 randomRs

注意,这些都是函数式的 —— 意味着这里面没有反作用,特别是生成器并不会更新。若是你用一个生成器去生成第一个列表,而后用相同的生成器去生成第二个列表...

g = mkStdGen 42
a = randoms g :: [Double]
b = randoms g :: [Double]

猜猜结果,因为透明引用,这两个列表的结果是同样的!(若是你想用一个随机种子来生成两个不一样的列表,我等下告诉你一个方法)。

下面一种方法来完成建立 17 的随机列表:

import System.Random

main = do
    let g   = mkStdGen 42
    let [s] = take 1 (randomStuff g)
    print s

randomStuff :: RandomGen g => g -> [[Float]]
randomStuff g = work (randomRs (0.0, 1.0) g)

work :: [Float] -> [[Float]]
work (r:rs)      =
    let n        = truncate (r * 7.0) + 1
        (xs, ys) = splitAt n rs
    in xs : work ys

除了必要的打印操做外,这是纯函数式的。它用生成器生成了无限列表,而后再用这个无限列表来生成另外一个无限列表做为答案,最后取第一个做为返回值。

我这样作是由于尽管咱们今天的人物是生成一个随机数,但你一般会须要不少个,我但愿这个例子能够对你有点帮助。

上面的代码的工做原理是:用一个生成器,建立一个包含 Float 的无限列表。截取第一个值,并扩大这个值到 17,而后用剩下的列表来生成答案。换句话说,把输入的列表分红 (r:rs)r 决定生成列表的长度(17),rs 以后会被计算答案。

split :: (RandomGen g) => g -> (g, g)

用一个随机种子建立两个不一样的生成器,其余状况下重用相同的种子是不明智的。

g = mkStdGen 42
(ga, gb) = split g
-- do not use g elsewhere

若是你想建立多余两个的生成器,你能够对新的生成器中的其中一个使用 split

g = mkStdGen 42
(ga, g') = split g
(gb, gc) = split g'
-- do not use g, g' elsewhere

咱们能够用 split 来得到两个生成器,这样咱们就能够产生两个随机列表了。

randomStuff :: RandomGen g => g -> [[Float]]
randomStuff g = work (randomRs (1, 7) ga) (randomRs (0.0, 1.0) gb)
    where (ga,gb) = split g

work :: [Int] -> [Float] -> [[Float]]
work (n:ns) rs =
    let (xs,ys) = splitAt n rs
    in xs : work ns ys

它把生成器分红两个,而后产生两个列表。

我在主程序中硬编码了随机种子。正常状况下你能够在其余地方获取随机种子 —— 从输入中获取,从文件中获取,从时间上获取,或者从某些设备中获取。

这些在主程序中都是 do-able 的,由于它们均可以在 IO monad 中访问。

你也能够经过 getStdGen 获取全局生成器:

main = do
    g <- getStdGen
    let [s] = take randomStuff g
    print s

出处

http://scarletsky.github.io/2016/02/06/random-numbers-in-haskell/

参考资料

原文

相关文章
相关标签/搜索