我假设目标是实现如下算法:我们有某种随机生成器,我们可以认为它以某种方式产生随机值流
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)
在这里,我们使用(>->) 构建生产管道,并使用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]
请注意,这些部分的实际操作并不是非常复杂。不幸的是,ST、mwc-random、vector 和 pipes 中的类型都是单独高度泛化的,因此一开始理解起来可能相当繁琐。希望上面我故意弱化和专门针对这个确切问题的几乎所有类型的上述内容,将更容易理解,并为这些出色的库中的每一个如何单独和协同工作提供一点直觉。