【问题标题】:pure Knuth/Fisher-Yates shuffle in haskell纯 Knuth/Fisher-Yates 在 haskell 中洗牌
【发布时间】:2023-03-27 23:07:01
【问题描述】:

在 go 中我可以写一个这样的函数:

func pureFisherYates(s []int, swaps []int) []int {
    newS := copy(s)
    for i, _ := range newS {
            for _, j := range swaps {
                    newS[i], newS[j] = newS[j], newS[i]
            }
    }
}

对我来说,这似乎是一个纯函数。它总是在给定相同输入的情况下返回相同的输出,并且它不会改变世界的状态(除了在某种严格意义上与任何其他函数相同的方式,占用 cpu 资源,产生热能等)。然而,每当我寻找如何进行纯洗牌时,我都会找到类似this 的东西,而每当我寻找专门的 Haskell 实现 Fisher-Yates 时,我要么得到一个用列表实现的 0^2 Fisher-Yates,要么得到一个 [a] -> IO [a] 实现。是否存在 [a] -> [a] O(n) shuffle 如果不存在,为什么我上面的 go 实现不纯。

【问题讨论】:

  • 这是一个有趣的事实集合。你有什么问题?
  • @DanielWagner 希望我修复它以使问题更清楚

标签: algorithm haskell functional-programming shuffle purely-functional


【解决方案1】:

ST monad 恰好允许这种封装的可变性,Data.Array.ST 包含可以在 ST 中变异的数组,然后在外部返回一个不可变的版本。

https://wiki.haskell.org/Random_shuffle 给出了两种使用 ST 的 Fisher-Yates shuffle 实现。它们并不是字面意义上的[a] -> [a],但那是因为还需要处理随机数生成:

import System.Random
import Data.Array.ST
import Control.Monad
import Control.Monad.ST
import Data.STRef

-- | Randomly shuffle a list without the IO Monad
--   /O(N)/
shuffle' :: [a] -> StdGen -> ([a],StdGen)
shuffle' xs gen = runST (do
        g <- newSTRef gen
        let randomRST lohi = do
              (a,s') <- liftM (randomR lohi) (readSTRef g)
              writeSTRef g s'
              return a
        ar <- newArray n xs
        xs' <- forM [1..n] $ \i -> do
                j <- randomRST (i,n)
                vi <- readArray ar i
                vj <- readArray ar j
                writeArray ar j vi
                return vj
        gen' <- readSTRef g
        return (xs',gen'))
  where
    n = length xs
    newArray :: Int -> [a] -> ST s (STArray s Int a)
    newArray n xs =  newListArray (1,n) xs

import Control.Monad
import Control.Monad.ST
import Control.Monad.Random
import System.Random
import Data.Array.ST
import GHC.Arr

shuffle :: RandomGen g => [a] -> Rand g [a]
shuffle xs = do
    let l = length xs
    rands <- forM [0..(l-2)] $ \i -> getRandomR (i, l-1)
    let ar = runSTArray $ do
        ar <- thawSTArray $ listArray (0, l-1) xs
        forM_ (zip [0..] rands) $ \(i, j) -> do
            vi <- readSTArray ar i
            vj <- readSTArray ar j
            writeSTArray ar j vi
            writeSTArray ar i vj
        return ar
    return (elems ar)

*Main> evalRandIO (shuffle [1..10])
[6,5,1,7,10,4,9,2,8,3]

编辑:在您的 Go 代码中使用固定的 swaps 参数,代码非常简单

{-# LANGUAGE ScopedTypeVariables #-}

import Data.Array.ST
import Data.Foldable
import Control.Monad.ST

shuffle :: forall a. [a] -> [Int] -> [a]
shuffle xs swaps = runST $ do
    let n = length xs
    ar <- newListArray (1,n) xs :: ST s (STArray s Int a)
    for_ [1..n] $ \i ->
        for_ swaps $ \j -> do
            vi <- readArray ar i
            vj <- readArray ar j
            writeArray ar j vi
            writeArray ar i vj
    getElems ar

但我不确定您是否可以合理地称其为 Fisher-Yates shuffle。

【讨论】:

  • 请注意,在 OP 代码中,改组是由 swaps :: [Int] 参数而不是随机源驱动的,从而使原始函数成为纯函数。在 Haskell 中应该可以实现类似的东西。
  • @chi 很公平,添加了代码。虽然我不认为这个版本可以称为 Fisher-Yates shuffle...
  • @AlexeyRomanov 我有点像 Haskell 菜鸟,所以我现在尽量避免深入研究语言扩展。有没有什么方法可以使用没有ScopedTypeVariables 的交换实现最后一个实现?
  • 没关系,我能够在不了解 ScopedTypeVariables 的作用的情况下删除它
  • @Julian 以前的版本 (stackoverflow.com/revisions/56662592/2) 没有,实际上:let newArray :: Int -&gt; [a] -&gt; ST s (STArray s Int a); newArray n xs = newListArray (1,n) xs 然后是ar &lt;- newArray n xs
猜你喜欢
  • 2013-10-26
  • 1970-01-01
  • 2017-07-08
  • 1970-01-01
  • 2021-09-04
  • 2011-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多