在我用Scala制作的游戏地下城生成器中,我有一个递归函数,可以生成“树状”结构。它接收纯随机数发生器(RNG),并输出随机树和新的随机数发生器。
我的问题是因为它是递归的,每次我的函数分支出来时,我都不想将同一个RNG传递给两个分支,所以我必须在我的外部函数中保留一个内部var
。
摘自我的代码:
def generate(parameters: RandomBSPTreeParameters)(rng: RNG): (BSPTree, RNG) = {
var varRng: RNG = rng
def inner(size: Size, verticalSplit: Boolean): BSPTree = {
def verticalBranch = {
val leeway = size.height - parameters.minLeafEdgeLength.value * 2
val (topHeightOffset, newRng) = RNG.nextPositiveInt(leeway)(varRng)
varRng = newRng
val topHeight = parameters.minLeafEdgeLength.value + topHeightOffset
VerticalBranch(
inner(Size(size.width, topHeight), verticalSplit = false),
inner(Size(size.width, size.height - topHeight), verticalSplit = false)
)
}
def horizontalBranch = {
val leeway = size.width - parameters.minLeafEdgeLength.value * 2
val (topWidthOffset, newRng) = RNG.nextPositiveInt(leeway)(varRng)
varRng = newRng
val leftWidth = parameters.minLeafEdgeLength.value + topWidthOffset
HorizontalBranch(
inner(Size(leftWidth, size.height), verticalSplit = true),
inner(Size(size.width - leftWidth, size.height), verticalSplit = true)
)
}
def randomOrientationBranch = {
val (splitVertically, newRng) = RNG.nextBoolean(varRng)
varRng = newRng
if (splitVertically)
verticalBranch
else
horizontalBranch
}
if(size.surface > parameters.minLeafSurface)
size.shape match {
case Square if size.width > parameters.minLeafEdgeLength.value * 2 => randomOrientationBranch
case SkewedHorizontally if size.width > parameters.minLeafEdgeLength.value * 2 => horizontalBranch
case SkewedVertically if size.height > parameters.minLeafEdgeLength.value * 2 => verticalBranch
}
else Leaf(size)
}
val (firstSplitIsVertical, newRng) = RNG.nextBoolean(varRng)
varRng = newRng
val tree = inner(parameters.size, firstSplitIsVertical)
(tree, varRng)
}
有人可以指导我朝着正确的方向前进,无需将var varRng: RNG
保留在此功能中,并使其成为无状态。
答案 0 :(得分:1)
首先,即使你摆脱了var
,你的功能仍然是副作用。原因是nextPositiveInt
具有产生随机数的副作用。即使nextPositiveInt
从不可变的预生成集合中获取数字(因此,没有IO),它也必须在每次调用时递增内部位置计数器。
其次,如果数字是“纯”随机的,这是不可能的,IMO,但是让我们说它们至少是iid(独立且相同地分布) - 将不同的生成器传递给a是没有任何意义的即使您传递相同的生成器,不同的分支 - 分支之间的随机数据仍然不会相关。所以你可以在任何地方使用相同的rng。
如果情况并非如此,我们在谈论伪随机性(可能会自动关联)和每种情况下的不同种子,只需将rng作为内部函数的参数传递,例如:
def verticalBranch(vrng: RNG) = ...
我还注意到你并没有真正通过左(或右分支)传播 - 你有两个vars
然后lrng, rrng
,这反过来意味着你根本不需要变量 - 它们可能是vals
:
val lRng = newRng()
val rRng = newRng()
def verticalBranch = {
...
val (topHeightOffset, newRng) = RNG.nextPositiveInt(leeway)(lRng)
}
def horizontalBranch = {
...
val (topHeightOffset, newRng) = RNG.nextPositiveInt(leeway)(rRng)
}
如何更好地封装副作用?
您可以将lRng,rRng和其他必要的生成器表示为迭代器:
val lRngGen = Iterator.continually(RNG.nextPositiveInt(rRng))
并使用类似的东西:
case class Accumulator(size: Size, tree: BSPTree)
val vert = (lRngGen zip rRngGen).foldLeft(Accumulator(parameters.size, emptyTree)(createNode)`
def createNode(rng: (Int, Int), acc: Accumulator): Accumulator = {
acc.size.shape match ...
}
但是,它需要完全重新考虑/重写代码foldLeft
而不是显式递归。