【问题标题】:How can I generate different random values in Haskell?如何在 Haskell 中生成不同的随机值?
【发布时间】:2019-09-07 18:59:24
【问题描述】:

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

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

我无法弄清楚为什么会发生这种情况以及如何为每次出现的“随机”获取不同的随机值。

【问题讨论】:

  • 只是一个想法,请记住,我根本不了解haskell。在 .NET 中,如果您构造一个 Random 类的新实例并要求它提供一个随机数,如果您在循环中快速执行此操作,您会观察到在很长一段时间内都会得到相同的结果。原因是该类是由计算机的时钟播种的,但该时钟值的分辨率约为 16ms,这意味着如果您在同一 16ms 间隔内播种 2 个Random 实例,它们将产生相同的“随机”值序列。这里会出现类似的情况吗?如果没有,请忽略我。
  • unsafe 函数确实不安全,很容易破坏语言。你应该假装这些功能不存在。初学者永远不应该被告知他们的存在。

标签: haskell random


【解决方案1】:

你的代码有两个问题:

  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 [String],而不是 [String]。有没有办法做到这一点并以 [String] 或 [Data.Text] 结尾?
  • @AugustoDias 不,没有正确的方法来结束 [String] 并且没有其他上下文。你可能以StdGen -> (StdGen, [String]) 或同构的东西结尾;这是你能得到的最接近纯的。
  • @AugustoDias 允许IO [String] 每次生成不同的字符串。 [String] 不是——它是给定的、不可变的字符串列表。如果你想打印你的x :: IO [String],你可以使用x >>= traverse putStrLn之类的东西(或者,更好的是,在正确的导入之后使用traverse_)。我建议阅读有关 IO 在 Haskell 中的工作原理,网上应该有很多教程。
  • 我正在努力学习haskell,但是一旦IO进去了东西就出不来,这似乎很不切实际。
  • @AugustoDias Strong 不同意:我发现当事物的行为取决于我(调用者)无法控制的值时,明确地做广告确实非常实用。我几乎每天都在工作中使用它;为了评估实用性,我能想到的东西很少比“实践”更符合“实践”的要求。
【解决方案2】:

如何在随机数生成中不涉及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 ciphermkTFGen

现在,在命令式语言(如 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"]
$ 

【讨论】:

    【解决方案3】:

    您的方法的根本问题是 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 monad 中完成所有操作,正如 @DanielWagner 在他的回答中展示的一种方法。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-01-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多