4

我想要一个stdGen在没有 IO 的情况下在每次调用中返回不同的函数。我尝试使用unsafePerformIO, 作为以下代码。

import System.IO.Unsafe
import System.Random

myStdGen :: StdGen
myStdGen = unsafePerformIO getStdGen

但是当我尝试调用myStdGenghci 时,我总是得到相同的值。我虐待了unsafePerformIO吗?或者还有其他方法可以达到我的目标吗?

编辑 对不起,我想我应该更准确地描述我的问题。

实际上,我正在实现一个变体的数据结构,它需要一个特殊的“合并”操作。它依赖于一些随机性来保证摊销的 O(log n) 预期时间复杂度。

我尝试使用一对like(Tree, StdGen)来为每个trap 保留随机生成器。在向 treap 插入新数据时,我会使用random随机值给新节点,然后更新我的生成器。但是我遇到了一个问题。我有一个调用函数empty,它将返回一个空的treap,我使用myStdGen上面的函数来获取这个treap 的随机生成器。但是,如果我有两个空的trep,它们StdGen将是相同的。因此,在我将数据插入到两个treap 之后,当我想合并它们时,它们的随机值也将是相同的。因此,我失去了我所依赖的随机性。

这就是为什么我想要一个“全局”随机生成器,它StdGen为每次调用产生不同的结果,这样每个空的treap 可以有不同的StdGen.

4

3 回答 3

8

我被虐待了吗unsafePerformIO

哎呀是的!“纯函数的显着特征”是

  • 无副作用
  • 引用透明,即结果的每个后续评估必须产生相同的结果。

实际上有一种方法可以实现您的“目标”,但这种想法是错误的。

myStdGen :: () -> StdGen
myStdGen () = unsafePerformIO getStdGen

因为这是一个(无用的)函数调用而不是CAF,所以它将分别评估IO每次调用的操作。

尽管如此,我认为编译器几乎可以自由地优化它,所以绝对不要依赖它。

尝试编辑时我注意到,getStdGen在给定进程中使用时,它本身总是给出相同的生成器,所以即使上面会使用更合理的类型,它也不起作用。


请注意,在算法等中正确使用伪随机性并不需要StdGen无处不在的IO——例如,您可以手动播种您split的. 整个程序将始终产生相同的结果,但在内部具有所有不同的随机数,以有效地工作。

或者,您可以从 IO 获取生成器,但仍以纯随机 monad 而不是IO.


还有另一种在完全纯算法中获得“随机性”的方法:要求输入为Hashable! 然后,您可以有效地将任何函数参数用作随机种子。这是一个有点奇怪的解决方案,但可能适用于您的 treap 应用程序(尽管我认为有些人不会再将其归类为 treap,而是作为一种特殊的 hashmap)。

于 2014-11-20T14:38:15.880 回答
7

这不是一个很好的用途unsafePerformIO

您在 GHCi 中重复看到相同数字的原因是 GHCi 本身不知道该值是不纯的,因此会记住您上次调用它时的值。您可以在 GHCi 的顶层输入 IO 命令,因此如果您只输入getStdGen. 但是,这也不起作用,因为 GHCi 工作方式的一个模糊部分涉及不恢复顶级表达式。您可以使用以下命令关闭它:set +r

> :set +r
> getStdGen
2144783736 1
> getStdGen
1026741422 1

请注意,您伪装成纯的不纯函数仍然无法正常工作。

> myStdGen
480142475 1
> myStdGen
480142475 1
> myStdGen
480142475 1

你真的不想走这条路。unsafePerformIO不应该以这种方式使用,也不是一个好主意。有很多方法可以得到你想要的东西(比如unsafePerformIO randomIO :: Int),但它们不会引导你找到好东西。相反,您应该根据随机 monad 中的随机数进行计算,并在 IO monad 中运行它。

更新

我从你的更新中看到你为什么首先想要这个。

在这个答案中,对于其他引用透明的函数中的随机性问题,有许多有趣的想法。

尽管有些人主张unsafePerformIO在这种情况下使用,但由于该页面各个部分中概述的许多原因,这仍然是一个坏主意。最后,如果一个函数依赖于随机源,最好在它的类型中指定它,最简单的方法是将它放在随机单子中。这仍然是一个纯函数,只是在调用它时需要一个生成器。IO您可以通过在主例程中请求随机生成器来提供此生成器。

可以在这里找到如何使用随机单子的一个很好的例子。

于 2014-11-20T14:43:13.133 回答
5

是的,你滥用了unsafePerformIO. 使用的正当理由很少unsafePerformIO,例如在与 C 库交互时,它也用于少数核心库的实现(我认为ST是其中之一)。简而言之,unsafePerformIO除非你真的很确定自己在做什么,否则不要使用。它不适用于生成随机数。

回想一下,Haskell 中的函数必须是纯函数,这意味着它们只依赖于它们的输入。这意味着您可以拥有一个生成“随机”数字的纯函数,但该数字取决于您传递给它的随机生成器,您可以执行类似的操作

myStdGen :: StdGen
myStdGen = mkStdGen 42

然后你可以做

randomInt :: StdGen -> (Int, StdGen)
randomInt g = random

但是你必须使用StdGen这个函数返回的新值,否则你总是会从randomInt.


所以你可能想知道,你如何干净地生成随机数而不求助于IO?答案是State单子。它看起来类似于

newtype State s a = State { runState :: s -> (a, s) }

它的单子实例看起来像

instance Monad (State s) where
    return a = State $ \s -> (a, s)
    (State m) >>= f = State $ \s -> let (a, newState) = m s
                                        (State g)     = f a
                                    in g newState

第一次看有点混乱,但基本上所有 state monad 所做的都是花哨的函数组合。有关更详细的说明,请参阅LYAH 。这里需要注意的是,s步骤之间的类型不会改变,只是a参数可以改变。

你会注意到它s -> (a, s)看起来很像我们的函数StdGen -> (Int, StdGen),带有s ~ StdGena ~ Int。这意味着如果我们这样做了

randomIntS :: State StdGen Int
randomIntS = State randomInt

然后我们可以做

twoRandInts :: State StdGen (Int, Int)
twoRandInts = do
    a <- randomIntS
    b <- randomIntS
    return (a, b)

然后可以通过提供初始状态来运行它:

main = do
    g <- getStdGen
    print $ runState twoRandInts g

StdGen仍然来自,IO但是所有逻辑本身都纯粹发生在状态单子中。

于 2014-11-20T14:37:49.270 回答