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

Scala 如何在递归函数中优雅地使用纯随机数生成器?,scala,functional-programming,Scala,Functional Programming,在我用Scala制作的游戏地下城生成器中,我有一个递归函数,可以生成树状结构。它接收一个纯随机数生成器RNG,并输出一个随机树和一个新的随机数生成器 我的问题是,因为它是递归的,每次我的函数分支时,我不想将同一个RNG传递给两个分支,所以我必须在外部函数中保留一个内部变量 我的代码摘录: def generate(parameters: RandomBSPTreeParameters)(rng: RNG): (BSPTree, RNG) = { var varRng: RNG = r

在我用Scala制作的游戏地下城生成器中,我有一个递归函数,可以生成树状结构。它接收一个纯随机数生成器RNG,并输出一个随机树和一个新的随机数生成器

我的问题是,因为它是递归的,每次我的函数分支时,我不想将同一个RNG传递给两个分支,所以我必须在外部函数中保留一个内部变量

我的代码摘录:

  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,并使它的状态更少。

首先,即使你去掉了var,你的函数仍然会有副作用。原因是nextPositiveInt产生随机数的副作用。即使nextPositiveInt从不可变的预生成集合中获取数字,它也不必在每次调用时递增内部位置计数器

其次,如果数字是纯随机的,这是不可能的,依我看,但假设它们至少是独立且相同分布的i.i.d——将不同的生成器传递给不同的分支没有任何意义,就像传递同一个生成器一样——分支之间的随机数据仍然不会相关。因此,您可以在任何地方使用相同的rng

如果不是这种情况,并且我们讨论的是伪随机性,它可能会与每种情况下的不同种子自动关联,只需将rng作为内部函数的参数传递,如:

 def verticalBranch(vrng: RNG) = ...
我还注意到,您并不是真的通过左或右分支传播rng-您有两个VAR,然后是lrng,rrng,这意味着您根本不需要VAR-它们可以是VAL:

如何更好地封装副作用

您可以将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而不是显式递归来重新思考/重写代码

 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 ...
 }