如何在递归函数中优雅地使用纯随机数生成器?

时间:2017-03-29 03:28:19

标签: scala functional-programming

在我用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保留在此功能中,并使其成为无状态。

1 个答案:

答案 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而不是显式递归。