(譯) 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/

參考資料

原文

相關文章
相關標籤/搜索