如何在没有副作用的情况下在 Scala 中实现 Fisher-Yates 洗牌?
How can I implement a Fisher-Yates shuffle in Scala without side effects?
我想通过对局部突变效应使用 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]
我想我需要以某种方式将 State
与 ST
堆叠起来,但这让我感到困惑。我需要 [S]StateT[ST[S,?],Seed,A]
吗?我必须重写 RNG
才能使用 StateT
吗?
(编辑)我不想涉及 IO
,我也不想用 Vector
代替 STArray
因为洗牌不会在 -地点。
我知道有一个 Haskell 实现 here,但我目前无法理解并将其移植到 Scalaz。但也许你可以? :)
提前致谢。
你有很多选择。一种简单(但不是很有原则)的方法是将 Rng
和 ST
操作都提升到 IO
中,然后在那里一起使用它们。另一种方法是在同一个 ST
中同时使用 STRef[Long]
和 STArray
。另一种方法是使用 State[(Long, Vector[A]), ?]
.
您也可以使用 StateT[State[Long, ?], Vector[A], ?]
但这有点毫无意义。您可能会在 ST
(对于数组)上使用 StateT
(对于 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 中的所有交换操作,然后将它们折叠起来,将您的集合作为累加器,边交换边交换。
这与 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)
}
}
这里或多或少直接翻译自 Haskell version you linked,它使用可变的 STArray
。 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.
我想通过对局部突变效应使用 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]
我想我需要以某种方式将 State
与 ST
堆叠起来,但这让我感到困惑。我需要 [S]StateT[ST[S,?],Seed,A]
吗?我必须重写 RNG
才能使用 StateT
吗?
(编辑)我不想涉及 IO
,我也不想用 Vector
代替 STArray
因为洗牌不会在 -地点。
我知道有一个 Haskell 实现 here,但我目前无法理解并将其移植到 Scalaz。但也许你可以? :)
提前致谢。
你有很多选择。一种简单(但不是很有原则)的方法是将 Rng
和 ST
操作都提升到 IO
中,然后在那里一起使用它们。另一种方法是在同一个 ST
中同时使用 STRef[Long]
和 STArray
。另一种方法是使用 State[(Long, Vector[A]), ?]
.
您也可以使用 StateT[State[Long, ?], Vector[A], ?]
但这有点毫无意义。您可能会在 ST
(对于数组)上使用 StateT
(对于 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 中的所有交换操作,然后将它们折叠起来,将您的集合作为累加器,边交换边交换。
这与 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)
}
}
这里或多或少直接翻译自 Haskell version you linked,它使用可变的 STArray
。 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.