【问题标题】:How to use `getBounds' with STArray?如何在 STArray 中使用“getBounds”?
【发布时间】:2012-10-02 13:55:48
【问题描述】:

我正在尝试使用 STArray 编写 Fisher-Yates 洗牌算法。与我在网上找到的所有其他示例不同,我试图避免使用本机列表。我只想就地打乱一个数组。

这就是我所拥有的:

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 的答案,但我无法选择它,因为坦率地说我并不完全理解它。也许一旦我更好地了解自己的问题,我会在未来回答我自己的问题......谢谢。

【问题讨论】:

    标签: arrays haskell random shuffle


    【解决方案1】:

    你可能不应该在你的函数中使用runSTrunST 应该在一些内部使用突变但具有纯接口的计算之外使用一次。你可能希望你的 shuffle 函数,它就地洗牌,有一个像 STArray s i e -&gt; ST s () 的类型(或者可能是一个更通用的类型),然后有一个不同的函数使用 runST 来呈现一个纯接口,如果你想要那个(不过,该函数可能需要复制值)。通常ST 的目标是STRefs 和STArrays 永远不能从一个runST 调用中逃脱并在另一个调用中使用。

    在没有runST 的情况下为您的函数推断的类型很好,只是更加多态(它适用于 IO 数组、ST 数组、STM 数组、未装箱数组等)。但是,如果您指定显式类型签名,则推理错误会更轻松。

    【讨论】:

    • 嗯,这是来自Haskell wiki 的随机洗牌(我认为是 Fischer-Yates),它在内部使用 runST 并具有纯接口(洗牌)。我正在尝试制作该函数的纯数组版本。我不明白你为什么认为使用 runST 调用 ST-monadic shuffle 函数的不同函数会产生任何影响,特别是考虑到 wiki 本身的 shuffle 函数使用 runST 并呈现纯接口 ---完全按照您的建议。
    • 您要求进行就地随机播放,这与 wiki 上给出的示例(或其他答案)非常不同。该示例将输入列表复制到一个数组中,就地打乱该数组,然后将其复制回来。如果你想要一个真正的就地洗牌,它不能在内部使用ST,同时保持一个纯界面。另一方面,如果你有就地代码,很容易使用runST 和几个副本(或runSTArray)给它一个纯界面。
    • @shachaf,我假设您指的是我的答案(如果我错了,请纠正我)。是的,它确实首先复制数组,然后进行就地洗牌,但是,解决这个问题只需将thawunsafethaw 交换。
    • 在什么情况下它变成了一个不安全的函数来改变一个不可变的数组?这对我来说听起来很糟糕。如果您想要一个就地随机播放数组的函数,请给它一个反映这一点的类型;在这种情况下,该类型可能涉及 ST。
    • @shachaf 是的,我已经更改了答案以反映这一点。
    【解决方案2】:

    以下是实施就地 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 的结果),然后再对副本执行算法。为了避免复制数组,您至少有两个选择:

    1. 使用unsafeThaw,它(顾名思义)是不安全的,只有当你
      确保输入数组永远不会被再次使用。这不是微不足道的 由于延迟评估而保证。

    2. fisherYates 具有(RandomGen g,Ix ix, Random ix) =&gt; g -&gt; STArray s ix e -&gt; ST s (STArray s ix e) 类型并执行需要在ST monad 中就地fisher-yates 算法的整个操作,并且仅使用runST 给出最终答案。

    【讨论】:

    • +1 表示可行的解决方案。尽管这不是我所要求的(使用runST),但我喜欢真实、简洁、有效的示例。
    【解决方案3】:

    这是因为 runST 的 rank-2 类型阻止您为 randShuffleST 提供有意义的类型。 (编写的代码存在第二个问题:可变 ST 数组不能有意义地存在于 ST monad 之外,因此从 runST 内部返回一个是不可能的,并且不太可能构造一个传递给纯函数最好。这是“无趣的”,但最终可能会让人感到困惑;请参阅此答案的底部以了解如何解决它。)

    那么,让我们看看为什么你不能写下类型签名。值得一提的是 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 单子。所以这应该有效,对吧?没那么快!

    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 <em>s1</em>))? s1哪里来的?答案是它来自bounds 定义中的runST。一般来说,runST 的类型是runST :: (forall σ. ST σ α) -&gt; α;当我们在这里使用它时,我们将它限制为(forall σ. ST σ (i,i)) -&gt; (i,i) 类型。这里发生的情况是 forall 就像一个 lambda(实际上,它一个 lambda),在括号内本地绑定 σ。所以当getBounds arr返回ST s (i,i)类型的东西时,我们可以将α(i,i)统一——但我们不能σs统一,因为σ 不在范围内。在 GHC 中,runST 的类型变量是 sa,而不是 σα,因此它将 s 重命名为 s1 以消除歧义,它是 this em> 键入您所看到的变量。

    所以这个错误是公平的:我们声称对于某些特定的sMArray a e (ST s) 成立。但是runST 需要 每个 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) 适用于每个 s,但我们仍然会遇到类型错误。似乎"GHC does not support polymorphic constraints to the left of an arrow"——事实上,在谷歌搜索试图找到该信息时,我发现了an excellent blog post at "Main Is Usually A Function",它遇到了与您相同的问题,解释了错误,并提供了以下解决方法。 (他们还会收到高级错误消息“格式错误的类断言”,这清楚地表明这样的事情是不可能的;这可能是由于不同的 GHC 版本。)

    当我们想要从 GHC 的内置系统中获得更多类型类约束时,这个想法很常见,通过 (ab) 使用 GADT 来为此类类型类的存在提供明确的证据:

    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 上的模式匹配将使该约束再次可用。 (唯一的另一种可能性是您的该类型的值未定义,这就是为什么模式匹配实际上是必要的。)现在,我们可以将其作为显式参数提供给 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 的参数列表中进行模式匹配,则来自evidences 会过早地固定为特定值,因此我们需要将其推迟;并且(我认为因为使用更高级别的类型进行推断很困难)我们还需要为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) 是不可满足的。出于同样的原因,在您的原始代码中,即使没有遇到类型错误,您也无法编写 randShuffleST,因为您无法编写在 ST 之外返回 STArray 的函数。

    这两个问题的原因是一样的:an STArray's first parameter is the state thread it lives onSTArrayMArray 实例是instance MArray (STArray s) e (ST s),因此您将始终拥有ST s (STArray s i e) 形式的类型。由于runST :: (forall s. ST s a) -&gt; a,运行runST mySTArrayAction 会以非法方式“泄漏”s。看看

    runSTArray :: Ix i =&gt; (forall s. ST s (STArray s i e)) -&gt; Array i e

    和它未装箱的朋友

    runSTUArray :: Ix i =&gt; (forall s. ST s (STUArray s i e)) -&gt; UArray i e.

    你也可以使用

    unsafeFreeze :: (Ix i, MArray a e m, IArray b e) =&gt; a i e -&gt; m (b i e)

    做同样的事情,只要你保证这是你在可变数组上调用的最后一个函数; freeze 函数放宽了这个限制,但必须复制数组。同样的道理,如果你想将一个数组而不是一个列表传递给你的函数的纯版本,你可能还需要

    thaw :: (Ix i, IArray a e, MArray b e m) =&gt; a i e -&gt; m (b i e);

    在这里使用unsafeThaw 可能是灾难性的,因为您传递的是一个您无法控制的不可变数组!这一切都会给我们带来类似的东西:

    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")
    

    这需要 O(n) 时间来复制输入的不可变数组,但是通过优化 - 需要 O(1) 时间冻结输出的可变数组,因为 STArrayArray 在底层是相同的。

    特别是将此应用于您的问题,我们有以下内容:

    {-# 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'')
    

    同样,您可以在 shufflePure 中将 freeze 替换为 Data.Array.Unsafe.unsafeFreeze;这可能会产生加速,因为如果它是Array i e,它就不必复制数组来返回它。 runSTArray 函数安全地包装了unsafeFreeze,所以这在shufflePure' 中不是问题。 (两者是等价的,以一些关于拆分 PRNG 的细节为模。)

    我们在这里看到了什么?重要的是,只有可变代码会引用可变数组,并且它保持可变(,在ST s 中返回一些内容)。由于shuffle 进行就地洗牌,它不需要返回一个数组,只需要返回PRNG。为了构建一个纯接口,我们将thaw 一个不可变数组转换为一个可变数组,将 that 就地打乱,然后将freeze 生成的数组重新转换为一个不可变数组。这很重要:它可以防止我们将可变数据泄漏回纯粹的世界。您不能直接可变地打乱传入的数组,因为它是不可变的;相反,你不能直接将可变混洗数组作为不可变数组返回,因为它是可变的,如果有人可以改变它怎么办?

    这不会与我们在上面看到的任何错误发生冲突,因为所有这些错误都来自对runST 的不当使用。如果我们限制使用runST,只有在我们组装了一个纯结果后才运行它,所有内部状态线程都可以自动发生。由于runST 是唯一具有rank-2 类型的函数,因此它是唯一可以产生严重类型怪异的地方;其他一切都只需要您标准的基于类型的推理,尽管可能需要更多考虑以保持s state-thread 参数一致。

    你瞧:

    *Main> let arr10 = listArray (0,9) [0..9] :: Array Int Int
    *Main> elems arr10
    [0,1,2,3,4,5,6,7,8,9]
    *Main> elems . fst . shufflePure arr10 <$> newStdGen
    [3,9,0,5,1,2,8,7,6,4]
    *Main> elems . fst . shufflePure arr10 <$> newStdGen
    [3,1,0,5,9,8,4,7,6,2]
    *Main> elems . fst . shufflePure' arr10 <$> newStdGen
    [3,9,2,6,8,4,5,0,7,1]
    *Main> elems . fst . shufflePure' arr10 <$> newStdGen
    [8,5,2,1,9,4,3,0,7,6]
    

    终于成功了! (真的太久了。很抱歉这个答案的长度。)

    【讨论】:

    • 我不知道我的问题会让我陷入如此深的兔子洞……我需要一些时间才能完全理解你的答案。但是,我必须使用如此多的扩展并绕过如此多的箍,这一事实让我相信,对于大多数 Haskeller 来说,就地改组 STArray 几乎是不可能的。但是,非常感谢您说“可变 ST 数组不能有意义地存在于 ST monad 之外”——仅此一项就对我有很大帮助。 +1 说明您的解释的广度和深度。
    • @opert:如果我“[引导你]相信对大多数 Haskeller 来说,就地改组 STArray 几乎是不可能的”,那么我给了你一个错误的答案!毕竟,看看HaskellElephant's solution——你明白了,它就是这么做的!我回答的目的是引导您了解为什么您会遇到错误;这个故事的寓意(我应该更明确一点)是runST 的错。如果您按照建议将runST 保留在外面,那么您的代码将变得非常简单。我将编辑我的答案以包含一个示例。
    猜你喜欢
    • 2020-06-29
    • 1970-01-01
    • 1970-01-01
    • 2012-07-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-24
    相关资源
    最近更新 更多