(譯) Haskell 中隨機數的使用

WilburnClar 8年前發布 | 7K 次閱讀 Haskell

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

隨機數(我指的是偽隨機數)是通過顯式或隱式的狀態來生成的。這意味著在 Haskell 中,隨機數的使用(通過 System.Random 庫)是伴隨著狀態的傳遞的。

大部分需要獲得幫助的人都有命令式編程的背景,因此,我會先用命令式的方式,然后再用函數式的方式來教大家在 Haskell 中使用隨機數。

任務

我會生成滿足以下條件的隨機列表:

  • 列表長度是 1 到 7
  • 列表中的每一項都是 0.0 到 1.0 之間的浮點數

命令式

在 IO monad 中有一個全局的生成器,你可以初始化它,然后獲取隨機數。下面有一些常用的函數:

setStdGen :: StdGen -> IO ()

初始化或者設置全局生成器,我們可以用 mkStdGen 來生成隨機種子。因此,有一個很傻瓜式的用法:

setStdGen (mkStdGen 42)

</div>

當然,你可以用任意的 Int 來替換 42 。

其實,你可以選擇是否調用 setStdGen ,如果你不調用的話,全局的生成器還是可用的。因為在 runtime 會在啟動的時候用一個任意的種子去初始化它,所以每次啟動的時候,都會有一個不同的種子。

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

在給定范圍隨機返回一個類型為 a 的值,同時全局生成器也會更新。你可以通過一個元組來指定范圍。下面這個例子會返回 a 到 z 之間的隨機值(包含 a 和 z ):

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

</div>

a 可以是任意類型嗎?并非如此。在 Haskell 98 標準中, Random 庫只支持 Bool , Char , Int , Integer , Float , Double (你可以自己去擴展這個支持的范圍,但這是另外一個話題了)。

randomIO :: (Random a) => IO a

返回一個類型為 a 的隨機數( a 可以是任意類型嗎?看上文),全局的生成器也會更新。下面這個例子會返回一個 Double 類型的隨機數:

x <- randomIO :: IO Double

</div>

隨機數返回的范圍由類型決定。

需要注意的是,這些都是 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)))

</div>

純函數式

你可能有以下原因想知道如何用函數式的方式生成隨機數:

  • 你有好奇心
  • 你不想用 IO monad
  • 因為一些并發或者其他原因,你想幾個生成器同時存在,共享全局生成器不能解決你的問題

實際上,有兩種方法來用函數式的方式去生成隨機數:

  • 從 stream(無限列表) 中提取隨機數
  • 把生成器當成函數參數的一部分,然后返回隨機數

這里有一些常用的函數用來創建生成器和包含隨機數的無限列表。

mkStdGen :: Int -> StdGen

用隨機種子創建生成器。

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

用生成器生成給定范圍的無限列表。例子:用 42 作為隨機種子,返回 a 到 z 之間包含 a 和 z 的無限列表:

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

</div>

類型 a 是隨機數的類型。類型 g 看起來是通用的,但實際上它總是 StdGen 。

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

用給定的生成器生成隨機數的無限列表。例如:用 42 作為隨機種子生成 Double 類型的列表:

randoms (mkStdGen 42) :: [Double]

</div>

隨機數的范圍由類型決定,你需要查文檔來確定具體范圍,或者直接用 randomRs 。

注意,這些都是函數式的 —— 意味著這里面沒有副作用,特別是生成器并不會更新。如果你用一個生成器去生成第一個列表,然后用相同的生成器去生成第二個列表…

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

</div>

猜猜結果,由于透明引用,這兩個列表的結果是一樣的!(如果你想用一個隨機種子來生成兩個不同的列表,我等下告訴你一個方法)。

下面一種方法來完成創建 1 到 7 的隨機列表:

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

</div>

除了必要的打印操作外,這是純函數式的。它用生成器生成了無限列表,然后再用這個無限列表來生成另一個無限列表作為答案,最后取第一個作為返回值。

我這樣做是因為盡管我們今天的人物是生成一個隨機數,但你通常會需要很多個,我希望這個例子可以對你有點幫助。

上面的代碼的工作原理是:用一個生成器,創建一個包含 Float 的無限列表。截取第一個值,并擴大這個值到 1 到 7 ,然后用剩下的列表來生成答案。換句話說,把輸入的列表分成 (r:rs) , r 決定生成列表的長度( 1 到 7 ), rs 之后會被計算答案。

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

用一個隨機種子創建兩個不同的生成器,其他情況下重用相同的種子是不明智的。

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

</div>

如果你想創建多余兩個的生成器,你可以對新的生成器中的其中一個使用 split :

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

</div>

我們可以用 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

</div>

它把生成器分成兩個,然后產生兩個列表。

我在主程序中硬編碼了隨機種子。正常情況下你可以在其他地方獲取隨機種子 —— 從輸入中獲取,從文件中獲取,從時間上獲取,或者從某些設備中獲取。

這些在主程序中都是 do-able 的,因為它們都可以在 IO monad 中訪問。

你也可以通過 getStdGen 獲取全局生成器:

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

</div>

參考資料

原文

</div>

 本文由用戶 WilburnClar 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!