假设我有一个这样的列表:
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"]
我不知道为什么会这样,以及如何为每次出现的“随机”获得不同的随机值。
答案 0 :(得分:7)
您的代码有两个问题:
unsafePerformIO
,但是明显违反了该函数的约定。您有责任证明您提供给unsafePerformIO
的东西实际上是纯净的,并且编译器在其权限范围内可以充当事实,而在这里绝对不是。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)
这个问题得到了很好的回答。但是,这可能会使某些读者感到Haskell中的伪随机数生成(PRNG)必须与IO相关联。
好吧,不是。只是在Haskell中,默认随机数生成器恰好是“托管”在IO类型中的。但这是选择,而不是必要。
作为参考,这里是recent review paper on the subject of PRNGs。 PRNG是确定性的数学自动机。它们不涉及IO。在Haskell中使用PRNG不需要涉及IO类型。在此答案的底部,我提供了解决当前问题的代码,除了打印结果外,无需使用IO类型。
Haskell库提供诸如mkStdGen
之类的函数,这些函数采用整数 seed 并返回伪随机数生成器,该生成器是RandomGen
类的对象,其类状态取决于种子的价值。请注意,mkStdGen
没有任何魔术。如果由于某些原因您不喜欢它,则可以选择其他方法,例如基于mkTFGen的Threefish 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"]
$