如何在Haskell中生成不同的随机值?

时间:2019-09-07 18:59:24

标签: 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"]

我不知道为什么会这样,以及如何为每次出现的“随机”获得不同的随机值。

3 个答案:

答案 0 :(得分:7)

您的代码有两个问题:

  1. 您正在调用unsafePerformIO,但是明显违反了该函数的约定。您有责任证明您提供给unsafePerformIO的东西实际上是纯净的,并且编译器在其权限范围内可以充当事实,而在这里绝对不是。
  2. 使用后,您没有仔细跟踪更新的随机数生成器的状态。确实,不可能用randomRs正确地做到这一点;如果您使用randomRs,则近似为一,这必须是程序所需的 last 随机性。

这两种方法最简单的解决方法是承认您确实在做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中为您跟踪更新的生成器状态,因此您可以在每次通话时正确地继续前进。

答案 1 :(得分:2)

您的方法的基本问题是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时,这不应该是一种“欺骗”方式,从而使用IO,从而生成“不纯”结果,该结果在程序的不同部分可能有所不同,但假装结果是纯净的。这样做并不奇怪。

由于生成随机值本质上是不纯净的,因此您需要在IO monad中完成全部操作,因为@DanielWagner在其答案中显示了一种解决方法。

(实际上还有另一种方法,包括使用随机生成器和类似randomR的函数与新生成器一起生成随机值。这使您可以用纯代码执行更多操作,这通常是可取的-但这会花费更多的精力,可能包括使用State monad来简化生成器值的线程化,最后您仍然需要IO以确保每次都获得一个新的随机序列您运行该程序。)

答案 2 :(得分:1)

如何不让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没有任何魔术。如果由于某些原因您不喜欢它,则可以选择其他方法,例如基于mkTFGenThreefish block cipher

现在,在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的 updated 状态提供给下一次计算时,我们第二次获得了相同的“随机”数字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进行 monadic计算加上生成器(的初始状态)。它返回伪随机值和生成器的更新状态。编写代码用于单子计算要比手动传递生成器状态容易得多。

这是解决问题文本中随机字符串替换问题的一种可能方法:

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