我正在尝试使用STArray编写Fisher-Yates shuffle算法。与我在网上找到的所有其他示例不同,我试图避免使用本机列表。我只是想在适当的位置随机播放一个阵列。
这就是我所拥有的:
randShuffleST arr gen = runST $ do
_ <- getBounds arr
return (arr, gen)
arr
是STArray,gen
将是类型的生成器状态(RandomGen g)。
我希望我可以依赖MArray中定义的(MArray (STArray s) e (ST s))
instance declaration来使用MArray的getBounds
,但GHCi无法推断出randShuffleST
的类型。它失败了:
Could not deduce (MArray a e (ST s))
arising from a use of `getBounds'
from the context (Ix i)
bound by the inferred type of
randShuffleST :: Ix i => a i e -> t -> (a i e, t)
at CGS/Random.hs:(64,1)-(66,25)
Possible fix:
add (MArray a e (ST s)) to the context of
a type expected by the context: ST s (a i e, t)
or the inferred type of
randShuffleST :: Ix i => a i e -> t -> (a i e, t)
or add an instance declaration for (MArray a e (ST s))
In a stmt of a 'do' block: _ <- getBounds arr
In the second argument of `($)', namely
`do { _ <- getBounds arr;
return (arr, gen) }'
In the expression:
runST
$ do { _ <- getBounds arr;
return (arr, gen) }
有趣的是,如果我像这样删除对`runST'的调用:
randShuffleST arr gen = do
_ <- getBounds arr
return (arr, gen)
它编译得很好,带有类型签名
randShuffleST :: (Ix i, MArray a e m) => a i e -> t -> m (a i e, t)
。我在Arch Linux上使用GHC 7.4.2。
请在回复中提供明确的类型签名,以帮助我理解您的代码,谢谢。
编辑:我真的很喜欢Antal S-Z的答案,但我不能选择它,因为坦率地说我并不完全理解它。也许一旦我更好地理解了自己的问题,我将来会回答我自己的问题......谢谢。答案 0 :(得分:7)
您可能不应在功能中使用runST
。 runST
应该使用一次,在一些内部使用变异但具有纯接口的计算的外部。您可能希望您的shuffle函数(就地将数组混洗)具有类似STArray s i e -> ST s ()
的类型(或者可能是更通用的类型),然后使用不同的函数使用runST
来呈现纯接口,如果你想要(那个函数可能需要复制值)。一般来说,ST
的目标是STRef
和STArray
永远无法从一个runST
调用中逃脱,并在另一个调用中使用。
为没有runST
的函数推断的类型很好,只是更多态(它适用于IO数组,ST数组,STM数组,未装箱的数组等)。但是,如果指定显式类型签名,则可以更轻松地使用推理错误。
答案 1 :(得分:6)
这是因为runST
的rank-2类型阻止您向randShuffleST
提供有意义的类型。 (你的代码写的第二个问题是:可变的ST数组在ST
monad之外有意义地存在,所以从runST
内部返回一个是不可能的,并构建一个传递到一个纯粹的函数是不太可能的。这是无趣的,#34;但最终可能会让自己感到困惑;请参阅本答案的底部以了解如何解决它。)
所以,让我们看看为什么你不能记下类型签名。值得预先说明I agree with shachaf关于编写函数的最佳方式,例如您正在撰写的函数:留在ST
内,并仅使用runST
一次,在最后。如果您这样做,那么我在答案的底部包含了一些示例代码,其中显示了如何成功编写代码。但我觉得理解你为什么会得到你所犯的错误很有意思;您收到的错误就是您不希望以这种方式编写代码的一些原因!
首先,让我们首先看一下产生相同错误信息的函数的简化版本:
bounds arr = runST (getBounds arr)
现在,让我们尝试为bounds
提供一个类型。显而易见的选择是
bounds :: (MArray a e (ST s), Ix i) => a i e -> (i,i)
bounds arr = runST (getBounds arr)
我们知道arr
必须是MArray
而我们并不关心它具有哪些元素或索引类型(只要其索引位于Ix
),但我们知道它必须住在ST
monad里面。所以这应该有效,对吗?没那么快!
ghci> :set -XFlexibleContexts +m
ghci> :module + Control.Monad.ST Data.Array.ST
ghci> let bounds :: (MArray a e (ST s), Ix i) => a i e -> (i,i)
ghci| bounds arr = runST (getBounds arr)
ghci|
<interactive>:8:25:
Could not deduce (MArray a e (ST s1))
arising from a use of `getBounds'
from the context (MArray a e (ST s), Ix i)
bound by the type signature for
bounds :: (MArray a e (ST s), Ix i) => a i e -> (i, i)
at <interactive>:7:5-38
...
等一下:Could not deduce (MArray a e (ST s1))
? s1
来自哪里?我们不会在任何地方提到这样的类型变量!答案是它来自runST
定义中的bounds
。通常,runST
具有类型(为方便起见,重命名一些类型变量)runST :: (forall σ. ST σ α) -> α
;当我们在这里使用它时,我们将其约束为(forall σ. ST σ (i,i)) -> (i,i)
类型。这里发生的是forall
就像一个lambda(事实上,它是一个lambda),在括号内局部绑定σ
。因此,当getBounds arr
返回ST s (i,i)
类型的内容时,我们可以将α
与(i,i)
统一起来,但我们无法统一 σ
与s
的{{1}},因为σ
不在范围内。在GHC中,runST
的类型变量为s
和a
,而不是σ
和α
,因此它将s
重命名为{{1}消除歧义,它是你正在看到的这个类型变量。
所以错误是公平的:我们声称某些特定s1
,s
成立。但MArray a e (ST s)
需要每个 runST
。然而,错误是非常不清楚的,因为它引入了一个你无法实际引用的新类型变量(所以&#34;可能的修复&#34;没有意义,尽管它无论如何都没有用)。
现在,显而易见的问题是,&#34;我可以写一个正确的类型签名吗?&#34;答案是&#34; ...有点。&#34; (但你可能不想。)所需的类型如下:
s
此约束表示ghci> :set -XConstraintKinds -XRank2Types
ghci> let bounds :: (forall s. MArray a e (ST s), Ix i) => a i e -> (i,i)
ghci| bounds arr = runST (getBounds arr)
ghci|
<interactive>:170:25:
Could not deduce (MArray a e (ST s))
arising from a use of `getBounds'
from the context (forall s. MArray a e (ST s), Ix i)
...
适用于每个 MArray a e (ST s)
,但我们仍会遇到类型错误。它似乎是"GHC does not support polymorphic constraints to the left of an arrow" - 事实上,在搜索信息时谷歌搜索时,我发现an excellent blog post at "Main Is Usually A Function"遇到了与您相同的问题,解释了错误,并提供了以下解决方法。 (他们也得到了优秀的错误信息&#34;格式错误的类断言,&#34;这表明这样的事情是不可能的;这可能是由于GHC版本的不同。)
这个想法是,当我们想要从GHC的内置系统中获得更多类型类约束时,通常可以通过(ab)为这种类型类的存在提供明确的证据。使用GADT:
s
现在,只要我们有ghci> :set -XNoFlexibleContexts -XNoConstraintKinds
ghci> -- We still need -XRank2Types, though
ghci> :set -XGADTs
ghci> data MArrayE a e m where
ghci| MArrayE :: MArray a e m => MArrayE a e m
ghci|
ghci>
类型的值,我们就知道该值必须使用MArrayE a e m
构造函数构造;只有在MArrayE
约束可用时才能调用此构造函数,因此MArray a e m
上的模式匹配将使该约束再次可用。 (唯一的另一种可能性是你的那个类型的值是未定义的,这就是为什么模式匹配实际上是必要的。)现在,我们可以提供它作为MArrayE
函数的显式参数,所以我们&#39 ; d将其称为bounds
:
bounds MArrayE arr
请注意我们必须将身体分解为自己的功能并在那里进行模式匹配的奇怪之处。正在进行的是,如果您在ghci> :set -XScopedTypeVariables
ghci> let bounds :: forall a e i.
ghci| Ix i => (forall s. MArrayE a e (ST s)) -> a i e -> (i,i)
ghci| bounds evidence arr = runST (go evidence)
ghci| where go :: MArrayE a e (ST s) -> ST s (i,i)
ghci| go MArrayE = getBounds arr
ghci|
ghci> -- Hooray!
的参数列表中进行模式匹配,则bounds
中的s
会过早地固定为特定值,所以我们需要把它关掉; (我认为因为推断更高级别的类型很难)我们还需要为evidence
提供一个显式类型,这需要使用范围类型变量。
最后,回到原始代码:
go
现在,正如我在开头所说的那样,还有一个问题需要解决。在上面的代码中,永远不会成为构造ghci> let randShuffleST :: forall a e i g. Ix i => (forall s. MArrayE a e (ST s))
ghci| -> a i e
ghci| -> g
ghci| -> (a i e, g)
ghci| randShuffleST evidence arr gen = runST $ go evidence
ghci| where go :: MArrayE a e (ST s) -> ST s (a i e,g)
ghci| go MArrayE = do _ <- getBounds arr
ghci| return (arr, gen)
ghci|
ghci> -- Hooray again! But...
类型值的方法,因为约束forall s. MArrayE a e (ST s)
约束是不可满足的。出于同样的原因,在您的原始代码中,即使没有您遇到的类型错误,也无法编写forall s. MArray a e (ST s)
,因为您无法编写返回{{1在} randShuffleST
之外。
这两个问题的原因是相同的:an STArray
's first parameter is the state thread it lives on。 STArray
的{{1}}实例为ST
,因此您始终拥有MArray
形式的类型。自STArray
开始,正在运行instance MArray (STArray s) e (ST s)
会泄漏&#34; ST s (STArray s i e)
以非法方式出局。看看
runSTArray :: Ix i => (forall s. ST s (STArray s i e)) -> Array i e
及其未装箱的朋友
runSTUArray :: Ix i => (forall s. ST s (STUArray s i e)) -> UArray i e
您也可以使用
unsafeFreeze :: (Ix i, MArray a e m, IArray b e) => a i e -> m (b i e)
做同样的事情,只要你保证这是你在可变数组上调用的最后一个函数; freeze
函数放宽了这个限制,但必须复制数组。出于同样的原因,如果你想将一个数组而不是一个列表传递给你的函数的纯版本,你可能也想要
thaw :: (Ix i, IArray a e, MArray b e m) => a i e -> m (b i e)
;
使用unsafeThaw
在这里可能是灾难性的,因为你传递的是一个你无法控制的不可变数组!这一切都会给我们带来类似的东西:
runST :: (forall s. ST s a) -> a
这需要 O ( n )时间来复制输入不可变数组,但是 - 通过优化 - 需要 O (1)时间冻结输出的可变数组,因为runST mySTArrayAction
和s
是相同的。
特别是将此问题应用于您的问题,我们有以下内容:
ghci> :set -XNoRank2Types -XNoGADTs
ghci> -- We still need -XScopedTypeVariables for our use of `thaw`
ghci> import Data.Array.IArray
ghci> let randShuffleST :: forall ia i e g. (Ix i, IArray ia e)
ghci| => ia i e
ghci| -> g
ghci| -> (Array i e, g)
ghci| randShuffleST iarr gen = runST $ do
ghci| marr <- thaw iarr :: ST s (STArray s i e)
ghci| _ <- getBounds marr
ghci| iarr' <- unsafeFreeze marr
ghci| return (iarr', gen)
ghci|
ghci> randShuffleST (listArray (0,2) "abc" :: Array Int Char) "gen"
(array (0,2) [(0,'a'),(1,'b'),(2,'c')],"gen")
同样,您可以使用STArray
中的Array
替换冻结;这可能会产生加速,因为如果它是{-# LANGUAGE FlexibleContexts #-}
import System.Random
import Control.Monad
import Control.Applicative
import Control.Monad.ST
import Data.Array.ST
import Data.STRef
import Data.Array.IArray
updateSTRef :: STRef s a -> (a -> (b,a)) -> ST s b
updateSTRef r f = do
(b,a) <- f <$> readSTRef r
writeSTRef r a
return b
swapArray :: (MArray a e m, Ix i) => a i e -> i -> i -> m ()
swapArray arr i j = do
temp <- readArray arr i
writeArray arr i =<< readArray arr j
writeArray arr j temp
shuffle :: (MArray a e (ST s), Ix i, Random i, RandomGen g)
=> a i e -> g -> ST s g
shuffle arr gen = do
rand <- newSTRef gen
bounds@(low,_) <- getBounds arr
when (rangeSize bounds > 1) .
forM_ (reverse . tail $ range bounds) $ \i ->
swapArray arr i =<< updateSTRef rand (randomR (low,i))
readSTRef rand
-- Two different pure wrappers
-- We need to specify a specific type, so that GHC knows *which* mutable array
-- to work with. This replaces our use of ScopedTypeVariables.
thawToSTArray :: (Ix i, IArray a e) => a i e -> ST s (STArray s i e)
thawToSTArray = thaw
shufflePure :: (IArray a e, Ix i, Random i, RandomGen g)
=> a i e -> g -> (a i e, g)
shufflePure iarr g = runST $ do
marr <- thawToSTArray iarr
g' <- shuffle marr g
iarr' <- freeze marr
return (iarr',g')
shufflePure' :: (IArray a e, Ix i, Random i, RandomGen g)
=> a i e -> g -> (Array i e, g)
shufflePure' iarr g =
let (g',g'') = split g
iarr' = runSTArray $ do
marr <- thaw iarr -- `runSTArray` fixes the type of `thaw`
void $ shuffle marr g'
return marr
in (iarr',g'')
,它就不必复制数组以返回它。 Data.Array.Unsafe.unsafeFreeze
函数安全地包装shufflePure
,因此Array i e
中不存在问题。 (这两个是等价的,模块化了关于拆分PRNG的一些细节。)
我们在这看到什么?重要的是,只有可变代码引用了可变数组,并且它保持可变(即。,返回runSTArray
内的内容)。由于unsafeFreeze
执行就地随机播放,因此不需要返回数组,只需返回PRNG。为了构建一个纯接口,我们将shufflePure'
一个不可变数组放入一个可变数组中,将 就地,然后ST s
生成的数组回到一个不可变的数组中。这很重要:它可以防止我们将可变数据泄漏回纯净的世界。你不能直接改变传入的数组,因为它是不可变的;相反,你不能直接将可变混乱数组作为不可变数组返回,因为它是可变的,如果有人可以改变它会怎么样?
这与我们上面看到的任何错误都没有冲突,因为所有这些错误都来自于shuffle
的不当使用。如果我们限制使用thaw
,只有在我们组装了纯结果后才运行它,所有内部状态线程都可以自动发生。由于freeze
是唯一具有rank-2类型的函数,因此它是唯一可以产生严重类型怪异的地方;其他一切只需要你的基于标准类型的推理,尽管可能需要更多考虑保持runST
状态线程参数一致。
瞧瞧:
runST
成功,终于! (方式太长了,真的。很抱歉这个答案的长度。)
答案 2 :(得分:3)
以下是实施就地Fisher-Yates的一种方式(我认为是
称为Durstenfeld或Knuth Shuffle)。请注意,永远不会调用runST
,而是调用runSTArray
,并且只调用一次。
import Data.Array
import Data.Array.ST
import Control.Monad.ST
import Control.Monad
import System.Random
fisherYates :: (RandomGen g,Ix ix, Random ix) => g -> Array ix e -> Array ix e
fisherYates gen a' = runSTArray $ do
a <- thaw a'
(bot,top) <- getBounds a
foldM (\g i -> do
ai <- readArray a i
let (j,g') = randomR (bot,i) g
aj <- readArray a j
writeArray a i aj
writeArray a j ai
return g') gen (range (bot,top))
return a
请注意,尽管算法是就地执行的,但在执行复制算法之前,函数首先复制输入中给出的数组(使用函数thaw
的结果)。为了避免复制数组,您至少有两个选项:
使用unsafeThaw
,(顾名思义)不安全,只有在您使用时才能使用
确保输入数组永远不会再次使用。这不是一件容易的事
保证,因为懒惰的评价。
让fisherYates
具有类型(RandomGen g,Ix ix, Random ix) => g -> STArray s ix e -> ST s (STArray s ix e)
并执行整个操作,该操作需要ST
monad中的就地渔民算法,并且只给出最终答案runST
。