【问题标题】:Pseudorandom number generators in HaskellHaskell 中的伪随机数生成器
【发布时间】:2014-01-20 00:21:38
【问题描述】:

我正在研究最新的 Programming Praxis 难题的解决方案——the first 用于实现最小标准随机数生成器,the second 用于实现一个随机播放盒以配合该一个或不同的伪随机数生成器。实现数学非常简单。对我来说,棘手的一点是弄清楚如何正确地将各个部分组合在一起。

从概念上讲,伪随机数生成器是一个函数stepRandom :: s -> (s, a),其中s 是生成器内部状态的类型,a 是生成的随机选择对象的类型。例如,对于线性同余 PRNG,我们可以有 s = a = Int64,或者可能有 s = Int64a = Double。 PSE 上的This post 很好地展示了如何使用 monad 通过随机计算线程化 PRNG 状态,并使用 runRandom 完成任务以运行具有特定初始状态(种子)的计算。

从概念上讲,随机播放盒是一个函数shuffle :: box -> a -> (box, a),以及一个使用来自 PRNG 的值初始化具有所需大小的新盒子的函数。然而,在实践中,这个盒子的表示有点棘手。为了提高效率,它应该被表示为一个可变数组,这迫使它进入STIO。有点像这样:

mkShuffle :: (Integral i, Ix i, MArray a e m) => i -> m e -> m (a i e) mkShuffle 大小 getRandom = 做 thelist a b b -> b -> m b 洗牌盒 n = 做 (start,end)

然而,我真正想做的是 attach 一个(已初始化?)随机播放盒到 PRNG,以便将 PRNG 的输出“管道”到随机播放盒中。我不明白如何正确设置管道。

【问题讨论】:

  • 你确定你真的需要可变数组提供的效率吗?一个未装箱但不可变的数组可能就足够了。
  • @icktoofay,我敢肯定,对于一个小盒子,或者对于编译器可以找出以单线程方式使用的盒子来说,这会很好,但在这一点上我对概念比对应用程序更感兴趣。
  • 哪个概念?可变数组或“管道”?每一个都可以很容易地单独演示,但它们一起可以很复杂。如果目标是使用随机播放框修改 PRNG 的输出,那么我在 mwc-randomvector 的可变向量上写了几行代码,但这种机制比你所拥有的要复杂得多在这里。
  • @J.Abrahamson,管道,特别是如何将它与可变数组放在一起。不幸的是,我想我可能还没有真正准备好:/。 Haskell 中的事情倾向于“当然,我明白了。好吧,没问题。当然。简单。等等!WTF?这甚至意味着什么????”
  • 我会继续发布回复,然后使用vectormwc-randompipes。我相信它可以满足您的需求,并且如果您围绕一些高度通用的类型进行手动操作,那么阅读它就不会难。

标签: haskell random


【解决方案1】:

我假设目标是实现如下算法:我们有某种随机生成器,我们可以认为它以某种方式产生随机值流

import Pipes

prng :: Monad m => Producer Int m r  

-- produces Ints using the effects of m never stops, thus the 
-- return type r is polymorphic

我们想通过随机播放盒修改此 PRNG。随机播放盒具有可变状态Box,它是一个随机整数数组,它们以特定方式修改随机整数流

shuffle :: Monad m => Box -> Pipe Int Int m r   

-- given a box, convert a stream of integers into a different 
-- stream of integers using the effects of m without stopping 
-- (polymorphic r)

shuffle 以整数为基础工作,通过传入的随机值以框的大小为模索引到其Box,将传入的值存储在那里,并发出之前存储在那里的值。从某种意义上说,它就像一个随机延迟函数。


因此,通过该规范,让我们开始一个真正的实现。我们想使用一个可变数组,所以我们将使用vector 库和ST monad。 ST 要求我们传递一个与特定 ST monad 调用匹配的幻象 s 参数,因此当我们编写 Box 时,它需要公开该参数。

import qualified Data.Vector.Mutable as Vm
import           Control.Monad.ST

data Box s = Box { sz :: Int, vc :: Vm.STVector s Int }

sz 参数是Box 的内存大小,Vm.STVector s 是一个可变的ST Vector 链接到s ST 线程。我们可以立即使用它来构建我们的 shuffle 算法,现在知道 Monad m 实际上必须是 ST s

import           Control.Monad

shuffle :: Box s -> Pipe Int Int (ST s) r
shuffle box = forever $ do                          -- this pipe runs forever
  up <- await                                       -- wait for upstream
  next <- lift $ do let index = up `rem` sz box     -- perform the shuffle
                    prior <- Vm.read (vc box) index --   using our mutation
                    Vm.write (vc box) index up      --   primitives in the ST
                    return prior                    --   monad
  yield next                                        -- then yield the result

现在我们希望能够将此shuffle 附加到一些prng Producer。由于我们使用的是vector,因此很高兴使用高性能的mwc-random 库。

import qualified System.Random.MWC   as MWC

-- | Produce a uniformly distributed positive integer
uniformPos :: MWC.GenST s -> ST s Int
uniformPos gen = liftM abs (MWC.uniform gen)

prng :: MWC.GenST s -> Int -> ST s (Box s)
prng gen = forever $ do 
  val <- lift (uniformPos gen)
  yield val

请注意,由于我们将 PRNG 种子 MWC.GenST s 传递到 ST s 线程中,因此我们不需要捕获修改并将它们也线程化。相反,mwc-random 在幕后使用可变的STRef s。另请注意,我们修改MWC.uniform 以仅返回正索引,因为这是shuffle 中的索引方案所必需的。

我们也可以使用mwc-random来生成我们的初始框。

mkBox :: MWC.GenST s -> Int -> ST s (Box s)
mkBox gen size = do 
  vec <- Vm.replicateM size (uniformPos gen)
  return (Box size vec)

这里唯一的技巧是非常好的Vm.replicateM 函数,它有效地具有约束类型

Vm.replicateM :: Int -> ST s Int -> Vm.STVector s Int

其中第二个参数是一个ST s 动作,它生成向量的一个新元素。


终于我们有了所有的部分。我们只需要组装它们。幸运的是,我们通过使用 pipes 获得的模块化使这变得微不足道。

import qualified Pipes.Prelude       as P

run10 :: MWC.GenST s -> ST s [Int]
run10 gen = do
  box <- mkBox gen 1000
  P.toListM (prng gen >-> shuffle box >-> P.take 10)

在这里,我们使用(&gt;-&gt;) 构建生产管道,并使用P.toListM 运行该管道并生成一个列表。最后,我们只需要在IO 中执行这个ST s 线程,这也是我们可以创建初始MWC.GenST s 种子并将其提供给run10 使用MWC.withSystemRandom 生成初始种子的地方,正如它所说, SystemRandom.

main :: IO ()
main = do
  result <- MWC.withSystemRandom run10
  print result

我们有自己的管道。

*ShuffleBox> main
[743244324568658487,8970293000346490947,7840610233495392020,6500616573179099831,1849346693432591466,4270856297964802595,3520304355004706754,7475836204488259316,1099932102382049619,7752192194581108062]

请注意,这些部分的实际操作并不是非常复杂。不幸的是,STmwc-randomvectorpipes 中的类型都是单独高度泛化的,因此一开始理解起来可能相当繁琐。希望上面我故意弱化和专门针对这个确切问题的几乎所有类型的上述内容,将更容易理解,并为这些出色的库中的每一个如何单独和协同工作提供一点直觉。

【讨论】:

  • 我至少需要一两天的时间来消化这个。如果最后能理解,我会很乐意接受这个答案。
  • 请注意,使用高质量的 PRNG 根本不是重点。关键是能够插入我想要的任何 PRNG。有一件事让我有点担心:从概念上讲,附有随机播放盒的 PRNG 本身就是一个 PRNG。我尚不清楚您的代码是否反映了这一点。那可能只是因为我还没有看懂一半。
  • 这个实现确实反映了prngprng + shuffle 都是同一类型。明确地,我们可以看到prng genprng gen &gt;-&gt; shuffle box 都有Producer Int (ST s) r 类型,读作“在ST monad 上产生整数”。
  • 因此,如果您愿意,可以继续:prng gen &gt;-&gt; shuffle box1 &gt;-&gt; shuffle box2 &gt;-&gt; shuffle box3
  • 另外值得注意的是,通过更改Box 和一些类型注释,可以将整个示例切换为IO 而不是ST。这就是为什么mwc-randomvector 具有复杂类型的原因——它们是IOST 的泛型。
猜你喜欢
  • 2014-05-18
  • 2015-08-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多