将MonadRandom与堆栈中的ST计算相结合

时间:2017-11-25 01:14:57

标签: arrays haskell shuffle

我尝试使用可变数组编写Fisher-Yates shuffle。到目前为止,我有以下代码:

module Main where

import Control.Monad.Random
import Control.Monad.Primitive
import Control.Monad.ST

import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV

fisherYates :: (MonadRandom m, PrimMonad m) => MV.MVector (PrimState m) a -> m ()
fisherYates v = forM_ [0 .. l - 1] (\i -> do j <- getRandomR (i, l)
                                             MV.swap v i j)
  where l = MV.length v - 1

shuffle :: MonadRandom m => V.Vector a -> m (V.Vector a)
shuffle v = _ -- don't know how to write this

main :: IO ()
main = print . evalRand (shuffle . V.generate 10 $ id) $ mkStdGen 42

但是,我完全不确定如何定义shuffle,这是一个高级包装器&#39;围绕可变矢量操作。似乎(至少从我的理解中),我首先必须“运行”。随机的部分&#39;堆栈,保存状态,运行ST&#39;部分&#39;得到一个不可变的向量,然后重新包装整个事物。另外,我知道我必须在某个地方使用thaw,但我的尝试很短暂。有人可以告诉我我错过了什么,以及我如何做我想做的事情?

1 个答案:

答案 0 :(得分:2)

我有两条建议:

  • 选择正确的monad嵌套。
  • 将monad实现与算法逻辑分开。

你试图最后运行随机monad并在内部使用ST,因此你需要ST作为一种monad变换器。决定你的monad堆栈是什么样的 - 哪个monad是变换器,哪个是内部monad?最简单的方法是让ST monad成为内部monad和随机monad变换器(很简单,因为你已经拥有了所有需要的包)。

现在制作一小组辅助函数。它在这里没有真正的回报 -  复杂项目的回报很大。这是我提出的monad堆栈和助手:

{-# LANGUAGE RankNTypes #-}
module Main where

import System.Random (StdGen)
import Control.Monad.Random
import Control.Monad.Primitive
import Control.Monad.ST

import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV


type KozM s a = RandT StdGen (ST s) a

请注意,变换器为RandT,内部monad为ST s

rnd :: (Int,Int) -> KozM s Int
rnd = getRandomR

swp :: MV.MVector s a -> Int -> Int -> KozM s ()
swp v i j = lift (MV.swap v i j)

freeze :: MV.MVector s a -> KozM s (V.Vector    a)
thaw   :: V.Vector     a -> KozM s (MV.MVector s a)
freeze = lift . V.freeze
thaw   = lift . V.thaw

变异向量所需的所有操作。现在我们只需要运行这个monad,这样我们就可以以某种方式逃到另一个有用的上下文。我注意到你之前硬编码了一个RNG(42) - 我使用的是随机的,但无论哪个......

run :: (forall s. KozM s a) -> IO a -- Can be just `a` if you hard-code
                                    -- an RNG as done in the question
run m = do g <- newStdGen
           pure (runST (evalRandT m g))

最后我们可以使用这个monad来实现f-y:

fisherYates :: MV.MVector s a -> KozM s ()
fisherYates v = forM_ [0 .. l - 1] (\i -> do j <- rnd (i, l)
                                             swp v i j)
  where l = MV.length v - 1

此时你可能不会觉得你学到了什么,希望运行功能很有帮助,但我可以看到你会觉得这会变得冗长。需要注意的重要一点是,如果你处理上面monad的管道,你的代码的其余部分是多么干净,这样你就不会有lift和模块限定符污染可能复杂的逻辑你实际上要解决的问题。

那就是说,这是一场平庸的洗牌:

shuffle :: V.Vector a -> KozM s (V.Vector a)
shuffle v = do
    vm <- thaw v
    fisherYates vm
    freeze vm

类型很重要。您以前在shuffle上调用了evalRand,暗示它会是某种MonadRandom m,同时必须调用runST - monad逻辑和算法逻辑的混合只会伤害大脑

主要是无趣的:

main :: IO ()
main = print =<< (run (shuffle (V.generate 10 id)) :: IO (V.Vector Int))

编辑:是的,你可以这样做,同时保持方法更一般。在某些时候你需要指定你运行的monad,或者你不能有一个将执行它的main,但所有的逻辑都可以使用MonadRandom和PrimMonad。

{-# LANGUAGE RankNTypes #-}
module Main where

import System.Random (StdGen)
import Control.Monad.Random
import Control.Monad.Primitive
import Control.Monad.ST

import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV
type KozM s a = RandT StdGen (ST s) a

rnd  :: MonadRandom m => (Int, Int) -> m Int
rnd = getRandomR

swp :: PrimMonad m => MV.MVector (PrimState m)  a -> Int -> Int -> m ()
swp v i j = MV.swap v i j

-- freeze :: MV.MVector s a -> KozM s (V.Vector    a)
-- thaw   :: V.Vector     a -> KozM s (MV.MVector s a)
freeze :: PrimMonad m => MV.MVector (PrimState m) a -> m (V.Vector a)
thaw :: PrimMonad m => V.Vector a -> m (MV.MVector (PrimState m) a)
freeze = V.freeze
thaw   = V.thaw


-- Some monad libraries, like monadlib, have a generalized `run` class method.
-- This doesn't exist, to the best of my knowledge, for the intersection of ST
-- and mtl.
run :: (forall s. KozM s a) -> IO a -- Can be just `a` if you hard-code
                                    -- an RNG as done in the question
run m = do g <- newStdGen
           pure (runST (evalRandT m g))

-- fisherYates :: MV.MVector s a -> KozM s ()
fisherYates :: (MonadRandom m, PrimMonad m) => MV.MVector (PrimState m) a -> m ()
fisherYates v = forM_ [0 .. l - 1] (\i -> do j <- rnd (i, l)
                                             swp v i j)
  where l = MV.length v - 1

shuffle :: (MonadRandom m, PrimMonad m) => V.Vector a -> m (V.Vector a)
shuffle v = do
    vm <- thaw v
    fisherYates vm
    freeze vm

main :: IO ()
main = print =<< (run (shuffle (V.generate 10 id)) :: IO (V.Vector Int))