(譯) 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 獲取全局生成器: