如何在随机数生成中不涉及IO:
这个问题得到了很好的答案。但是,它可能会给一些读者留下这样的印象,即 Haskell 中的伪随机数生成 (PRNG) 必然与 IO 相关联。
嗯,它不是。只是在 Haskell 中,默认的随机数生成器恰好是“托管”在 IO 类型中。但这是出于选择,而不是必然。
作为参考,这里是recent review paper on the subject of PRNGs。
PRNG 是确定性数学自动机。它们不涉及 IO。在 Haskell 中使用 PRNG 不需要涉及 IO 类型。在这个答案的底部,我提供了在不涉及 IO 类型的情况下解决手头问题的代码,除了打印结果。
Haskell 库提供了诸如mkStdGen 之类的函数,它们接受一个整数seed 并返回一个伪随机数生成器,它是RandomGen 类的一个对象,其状态取决于种子的价值。请注意,mkStdGen 并没有什么神奇之处。如果由于某种原因你不喜欢它,还有其他选择,例如基于Threefish block cipher 的mkTFGen。
现在,在命令式语言(如 C++)和 Haskell 中,伪随机数生成的管理方式不同。在 C++ 中,您可以像这样提取一个随机值:rval = rng.nextVal();。除了返回值之外,调用 nextVal() 还具有改变rng 对象状态的副作用,确保下次它会返回一个不同的随机数。
但是在 Haskell 中,函数没有副作用。所以你需要有这样的东西:
(rval, rng2) = nextVal rng1
也就是说,评估函数需要返回伪随机值和生成器的更新状态。一个小后果是,如果状态很大(例如对于常见的Mersenne Twister 生成器),Haskell 可能需要比 C++ 更多的内存。
因此,我们希望解决手头的问题,即随机转换字符串列表,将涉及具有以下类型签名的函数:RandomGen tg => [String] -> tg -> ([String], tg)。
为了便于说明,让我们获取一个生成器并使用它来生成几个 0 到 100 之间的“随机”整数。为此,我们需要 randomR 函数:
$ ghci
Prelude> import System.Random
Prelude System.Random> :t randomR
randomR :: (RandomGen g, Random a) => (a, a) -> g -> (a, g)
Prelude System.Random>
Prelude System.Random> let rng1 = mkStdGen 544
Prelude System.Random> let (v, rng2) = randomR (0,100) rng1
Prelude System.Random> v
23
Prelude System.Random> let (v, rng2) = randomR (0,100) rng1
Prelude System.Random> v
23
Prelude System.Random> let (w, rng3) = randomR (0,100) rng2
Prelude System.Random> w
61
Prelude System.Random>
请注意,当我们忘记将生成器 rng2 的 更新 状态输入到下一次计算中时,我们会再次获得相同的“随机”数字 23。这是一个非常常见的错误,也是一个非常普遍的抱怨。函数randomR是一个纯Haskell函数,不涉及IO。因此它具有引用透明性,即当给定相同的参数时,它返回相同的输出值。
处理这种情况的一种可能方法是在源代码中手动传递更新的状态。这很麻烦且容易出错,但可以管理。这给出了这种代码风格:
-- stateful map of randomize function for a list of strings:
fmapRandomize :: RandomGen tg => [String] -> tg -> ([String], tg)
fmapRandomize [] rng = ([], rng)
fmapRandomize(str:rest) rng = let (str1, rng1) = randomize str rng
(rest1, rng2) = fmapRandomize rest rng1
in (str1:rest1, rng2)
谢天谢地,有一个更好的方法,它涉及runRand 函数或其evalRand 兄弟。函数runRand 采用一元计算加上一个生成器(的初始状态)。它返回生成器的伪随机值和更新状态。为一元计算编写代码比手动传递生成器状态要容易得多。
这是从问题文本中解决随机字符串替换问题的一种可能方法:
import System.Random
import Control.Monad.Random
-- generic monadic computation to get a sequence of "count" random items:
mkRandSeqM :: (RandomGen tg, Random tv) => (tv,tv) -> Int -> Rand tg [tv]
mkRandSeqM range count = sequence (replicate count (getRandomR range))
-- monadic computation to get our sort of random string:
mkRandStrM :: RandomGen tg => Rand tg String
mkRandStrM = mkRandSeqM ('a', 'z') 10
-- monadic single string transformation:
randomizeM :: RandomGen tg => String -> Rand tg String
randomizeM str = if (str == "random") then mkRandStrM else (pure str)
-- monadic list-of-strings transformation:
mapRandomizeM :: RandomGen tg => [String] -> Rand tg [String]
mapRandomizeM = mapM randomizeM
-- non-monadic function returning the altered string list and generator:
mapRandomize :: RandomGen tg => [String] -> tg -> ([String], tg)
mapRandomize lstr rng = runRand (mapRandomizeM lstr) rng
main = do
let inpList = ["random", "foo", "random", "bar", "random", "boo", "qux"]
-- get a random number generator:
let mySeed = 54321
let rng1 = mkStdGen mySeed
-- execute the string substitutions:
let (outList, rng2) = mapRandomize inpList rng1
-- display results:
putStrLn $ "inpList = " ++ (show inpList)
putStrLn $ "outList = " ++ (show outList)
注意上面,RandomGen 是生成器的类,而 Random 只是生成值的类。
程序输出:
$ random1.x
inpList = ["random","foo","random","bar","random","boo","qux"]
outList = ["gahuwkxant","foo","swuxjgapni","bar","zdjqwgpgqa","boo","qux"]
$