我尝试使用可变数组编写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
,但我的尝试很短暂。有人可以告诉我我错过了什么,以及我如何做我想做的事情?
答案 0 :(得分:2)
我有两条建议:
你试图最后运行随机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))