如何在没有副作用的情况下在Scala中实现Fisher-Yates shuffle?

时间:2015-05-25 04:00:51

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

我想实现Fisher-Yates算法(就地数组shuffle),没有副作用,使用STArray进行局部变异效应,以及一个函数随机数生成器

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。但也许你可以吗? :)

提前致谢。

3 个答案:

答案 0 :(得分:4)

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

您也可以使用StateT[State[Long, ?], Vector[A], ?],但这有点毫无意义。您可以在StateT(对于数组)使用ST(对于RNG状态),但同样,我并没有真正看到这一点。

尽管如此,仅使用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中选择所有交换操作,然后将您的集合作为累加器折叠起来,随时进行交换。

答案 1 :(得分:3)

以下是使用可变STArray的{​​{3}}或多或少的直接翻译。 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函数仍然是纯函数,因为所有可变状态都是 local

答案 2 :(得分:1)

这与特拉维斯解决方案相同只是区别在于它使用了状态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)
  }

}