首頁>Club>
從1到m的範圍中抽取n個隨機數
16
回覆列表
  • 1 # CFY舒服

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

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

    任務

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

    列表長度是 1 到 7

    列表中的每一項都是 0.0 到 1.0 之間的浮點數

    命令式

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

    setStdGen :: StdGen -> IO ()

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

    setStdGen (mkStdGen 42)

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

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

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

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

    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.Randommain = do setStdGen (mkStdGen 42) -- 這步是可選的,如果有這一步,你每一次執行的結果都是一樣的,因為隨機種子固定是 42 s <- randomStuff print srandomStuff :: 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作為隨機種子,返回 a到 z之間包含 a和 z的無限列表:

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

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

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

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

    randoms (mkStdGen 42) :: [Double]

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

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

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

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

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

    import System.Randommain = do let g = mkStdGen 42 let [s] = take 1 (randomStuff g) print srandomStuff :: 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的無限列表。擷取第一個值,並擴大這個值到 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

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

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

    我們可以用 split來獲得兩個生成器,這樣我們就可以產生兩個隨機列表了。

    c <- randomRIO ("a", "z")0

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

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

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

    你也可以透過 getStdGen獲取全域性生成器:

    c <- randomRIO ("a", "z")1

  • 中秋節和大豐收的關聯?
  • 新破天一劍去龍星怎麼去?