【问题标题】:How can I implement a Fisher-Yates shuffle in Scala without side effects?如何在没有副作用的情况下在 Scala 中实现 Fisher-Yates shuffle?
【发布时间】:2015-08-06 12:03:20
【问题描述】:

我想通过使用 STArray 来实现局部突变效果和功能性随机数生成器来实现没有副作用的 Fisher-Yates 算法(就地数组洗牌)

type RNG[A] = State[Seed,A]

产生算法所需的随机整数。

我有一个方法 def intInRange(max: Int): RNG[Int] 可以用来在 [0,max) 中生成随机的 Int

来自Wikipedia

To shuffle an array a of n elements (indices 0..n-1):
    for i from n − 1 downto 1 do
        j ← random integer such that 0 ≤ j ≤ i
        exchange a[j] and a[i]

我想我需要以某种方式将StateST 堆叠起来,但这让我感到困惑。我需要[S]StateT[ST[S,?],Seed,A] 吗?我是否也必须重写RNG 才能使用StateT

(编辑)我不想涉及IO,也不想用Vector 代替STArray,因为不会就地执行随机播放。

我知道有一个 Haskell 实现 here,但我目前无法理解并将其移植到 Scalaz。但也许你可以? :)

提前致谢。

【问题讨论】:

  • 您要避免的副作用到底是什么?我对 Scala 知之甚少,但至少在 MT 环境中你必须锁定 i 并修改序列本身(你正在交换 a_i 和 a_j)
  • 我不确定您要做什么。使用递归交换元素很简单。要获得没有副作用的随机数,您的 RNG 可以有一个 map 函数,该函数使用随机数而不是 getNext... 方法调用其一阶函数

标签: scalaz state-monad st-monad starray scala-cats


【解决方案1】:

这里或多或少是从使用可变STArrayHaskell version you linked 直接转换而来的。 Scalaz STArray 没有与listArray 完全等效的函数,所以我编造了一个。否则,它是一个简单的音译:

import scalaz._
import scalaz.effect.{ST, STArray}
import ST._
import State._
import syntax.traverse._
import std.list._

def shuffle[A:Manifest](xs: List[A]): RNG[List[A]] = {
  def newArray[S](n: Int, as: List[A]): ST[S, STArray[S, A]] =
    if (n <= 0) newArr(0, null.asInstanceOf[A])
    else for {
      r <- newArr[S,A](n, as.head)
      _ <- r.fill((_, a: A) => a, as.zipWithIndex.map(_.swap))
    } yield r
  for {
    seed <- get[Seed]
    n = xs.length
    r <- runST(new Forall[({type λ[σ] = ST[σ, RNG[List[A]]]})#λ] {
      def apply[S] = for {
        g <- newVar[S](seed)
        randomRST = (lo: Int, hi: Int) => for {
          p <- g.read.map(intInRange(hi - lo).apply)
          (a, sp) = p
          _ <- g.write(sp)
        } yield a + lo
        ar  <- newArray[S](n, xs)
        xsp <- Range(0, n).toList.traverseU { i => for {
          j  <- randomRST(i, n)
          vi <- ar read i
          vj <- ar read j
          _  <- ar.write(j, vi)
        } yield vj }
        genp <- g.read
      } yield put(genp).map(_ => xsp)
    })
  } yield r
}

虽然使用可变数组的渐近性可能很好,但请注意 Scala 中 ST monad 的常数因子非常大。使用常规可变数组在一个整体块中执行此操作可能会更好。整个shuffle 函数保持纯净,因为您的所有可变状态都是本地

【讨论】:

  • 啊,谢谢——这就是 Travis 在同一个 ST 中使用 STRef[Seed]STArray 的意思。感谢关于ST 的常量因素的观点。此外,这颗罗塞塔石碑让我能够阅读 Haskell 版本。
【解决方案2】:

这与 Travis 解决方案几乎相同,唯一的区别是它使用了 State monad。我想找到一组最小的导入,但我最终放弃了:

import com.nicta.rng.Rng
import scalaz._
import Scalaz._

object FisherYatesShuffle {

  def randomJ(i: Int): Rng[Int] = Rng.chooseint(0,i)

  type Exchange = (Int,Int)

  def applyExchange[A](exchange: Exchange)(l: Vector[A]): Vector[A] = {
    val (i,j) = exchange
    val vi = l(i)
    l.updated(i,l(j)).updated(j,vi)
  }

  def stApplyExchange[A](exchange: Exchange): State[Vector[A], Unit] = State.modify(applyExchange(exchange))

  def shuffle[A](l: Vector[A]): Rng[Vector[A]] = {
    val rngExchanges: Rng[Vector[Exchange]] = (l.length - 1 to 1 by -1).toVector.traverseU { i =>
      for {
        j <- randomJ(i)
      } yield (i, j)
    }

    for {
      exchanges <- rngExchanges
    } yield exchanges.traverseU(stApplyExchange[A]).exec(l)
  }

}

【讨论】:

    【解决方案3】:

    你有很多选择。一种简单(但不是很有原则)的方法是将RngST 操作提升到IO,然后在那里与它们一起工作。另一种方法是在同一个ST 中同时使用STRef[Long]STArray。另一种方法是使用State[(Long, Vector[A]), ?]

    您也可以使用StateT[State[Long, ?], Vector[A], ?],但这毫无意义。您可能可以使用 StateT(用于 RNG 状态)而不是 ST(用于数组),但同样,我真的不明白这一点。

    不过,只需Rng 就可以非常干净地做到这一点而没有副作用。例如,使用NICTA's RNG library:

    import com.nicta.rng._, scalaz._, Scalaz._
    
    def shuffle[A](xs: Vector[A]): Rng[Vector[A]] =
      (xs.size - 1 to 1 by -1).toVector.traverseU(
        i => Rng.chooseint(0, i).map((i, _))
      ).map {
        _.foldLeft(xs) {
          case ((i, j), v) =>
            val tmp = v(i)
            v.updated(i, v(j)).updated(j, tmp)
        }
      }
    

    在这里,您只需在 Rng monad 中选择所有交换操作,然后将它们折叠起来,并将您的集合作为累加器,随时交换。

    【讨论】:

    • 感谢 Travis — 在您回答的 6 种可能性中:我不想提升到 IO(#1),我也不想使用 Vector,因为那时我'没有执行就地洗牌,我认为算法的复杂性发生了变化(#3,4,6)。我不明白建议 2(STRef[Long]STArray);但我认为我需要#5(StateT 超过ST)来获得就地更新。你能详细说明那个答案吗?我已经更新了我的问题,希望现在更清楚一点。再次感谢。
    猜你喜欢
    • 1970-01-01
    • 2019-03-06
    • 2011-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-11
    • 1970-01-01
    相关资源
    最近更新 更多