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