如何在哈斯克尔中生成不同的随机值

  • 本文关键字:随机 哈斯克 haskell random
  • 更新时间 :
  • 英文 :


假设我有一个这样的列表:

let list = ["random", "foo", "random", "bar", "random", "boo"]

我想遍历一个列表并将所有"随机"元素映射到不同的随机字符串:

let newList = fmap randomize list
print newList
-- ["dasidias", "foo", "gasekir", "bar", "nabblip", "boo"]

我的随机化函数如下所示:

randomize :: String -> String
randomize str = 
case str of
"random" -> randStr
_        -> str
where
randStr = take 10 $ randomRs ('a','z') $ unsafePerformIO newStdGen

但是我为每个"随机"元素得到相同的随机字符串:

["abshasb", "foo", "abshasb", "bar", "abshasb", "boo"]

我不知道为什么会发生这种情况以及如何为每次出现的"随机"获得不同的随机值。

你的代码有两个问题:

  1. 您正在调用unsafePerformIO,但显式违反了该函数的协定。你有责任证明你提供给unsafePerformIO的东西实际上是纯粹的,编译器有权表现得好像是这样,而这里绝对不是。
  2. 使用更新的随机数生成器状态后,您没有仔细跟踪它。事实上,不可能用randomRs正确做到这一点;如果你使用randomRs,那么对于第一近似,那一定是你的程序需要的最后一个随机性。

解决这两个问题的最简单方法是承认你真的,真的在做IO。所以:

import Control.Monad
import System.Random
randomize :: String -> IO String
randomize "random" = replicateM 10 (randomRIO ('a', 'z'))
randomize other = pure other

在 ghci 中尝试一下:

> traverse randomize ["random", "foo", "random", "bar", "random", "boo"]
["xytuowzanb","foo","lzhasynexf","bar","dceuvoxkyh","boo"]

没有调用unsafePerformIO,因此没有推卸责任;并且randomRIO在隐藏的IORef中为您跟踪更新的生成器状态,因此您在每次调用时正确地继续推进它。

如何在随机数生成中不涉及 IO:

这个问题得到了很好的回答。然而,这可能会给一些读者留下这样的印象,即Haskell中的伪随机数生成(PRNG)必然与IO相关联。

其实然。只是在 Haskell 中,默认的随机数生成器恰好"托管"在 IO 类型中。但这是出于选择,而不是迫不得已。

作为参考,以下是最近关于PRNG主题的综述文件。 PRNG是确定性的数学自动机。它们不涉及 IO。在Haskell中使用PRNG不需要涉及IO类型。在此答案的底部,我提供了解决手头问题的代码,而不涉及 IO 类型,除了打印结果。

Haskell库提供了诸如mkStdGen之类的函数,这些函数采用整数种子并返回伪随机数生成器,伪随机数生成器是RandomGen类的对象,其状态取决于种子的值。请注意,mkStdGen没有什么神奇之处。如果由于某种原因您不喜欢它,还有其他选择,例如基于Threefish分组密码的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是一个不涉及IO的纯Haskell函数。因此,它具有引用透明度,即当给定相同的参数时,它返回相同的输出值。

处理这种情况的一种可能方法是在源代码中手动传递更新的状态。这很麻烦且容易出错,但可以管理。这给出了这种风格的代码:

-- 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"]
$ 

你的方法的根本问题是Haskell是一种纯粹的语言,你试图使用它,就好像它不是一样。事实上,这并不是对代码显示的语言的唯一根本误解。

randomise函数中:

randomize :: String -> String
randomize str = 
case str of
"random" -> randStr
_        -> str
where
randStr = take 10 $ randomRs ('a','z') $ unsafePerformIO newStdGen

您显然打算每次使用randStr时都采用不同的值。但是在 Haskell 中,当你使用=符号时,你并没有像命令式语言那样"为变量赋值"。你是说这两个值是相等的。由于Haskell中的所有"变量"实际上都是"常量"且不可变的,因此编译器完全有权假设程序中出现的每次randStr都可以替换为它首先为其计算的任何值。

与命令式语言不同,Haskell程序不是要执行的语句序列,它们执行诸如更新状态之类的副作用。Haskell程序由表达式组成,这些表达式或多或少地以编译器认为最好的顺序进行计算。(特别是main表达式,它描述了整个程序将要做什么 - 然后由编译器和运行时将其转换为可执行的机器代码。因此,当您将复杂表达式分配给变量时,您并不是说"在执行流的这一点上,执行此计算并将结果分配给此变量"。您说"这是变量的值",对于"所有时间" - 该值不允许更改。

事实上,这里似乎发生变化的唯一原因是因为您使用了unsafePerformIO.顾名思义,这个功能是"不安全的"——它基本上不应该被使用,至少除非你真的知道你在做什么。它不应该是一种"作弊"的方式,就像你在这里使用它一样,使用 IO,从而产生一个"不纯"的结果,在程序的不同部分可能不同,但假装结果是纯粹的。这不起作用也就不足为奇了。

由于生成随机值本质上是不纯的,因此您需要在 monadIO中完成整个事情,正如@DanielWagner在他的答案中展示的一种方法。

(实际上还有另一种方法,涉及使用随机生成器和 randomR 等函数与新生成器一起生成随机值。这允许您在纯代码中做更多的事情,这通常是可取的 - 但这需要更多的努力,可能包括使用Statemonad 来简化生成器值的线程化,并且您最终仍然需要IO以确保每次运行程序时都能获得新的随机序列。

最新更新