【问题标题】:How do I elegantly use a pure random number generator inside a recursive function?如何优雅地在递归函数中使用纯随机数生成器?
【发布时间】:2017-03-29 03:28:19
【问题描述】:

在我用 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 的需要,并使其无状态。

【问题讨论】:

    标签: scala functional-programming


    【解决方案1】:

    首先,即使您摆脱了var,您的功能仍然会产生副作用。原因是nextPositiveInt 具有生成随机数的副作用。即使nextPositiveInt 从不可变的预生成集合中获取数字(因此,没有 IO),它也必须在每次调用它时增加内部位置计数器。

    其次,如果数字是“纯”随机的,这实际上是不可能的,IMO,但是假设它们至少是 i.i.d(独立同分布) - 将不同的生成器传递给一个没有任何意义不同的分支,即使您传递相同的生成器 - 分支之间的随机数据仍然不相关。所以你可以在任何地方使用相同的 rng。

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

     def verticalBranch(vrng: RNG) = ...
    

    我还注意到你并没有真正通过左(或右)分支传播 rng - 你有两个 vars 然后是 lrng, rrng,这反过来意味着你不需要vars - 他们可能是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 而不是显式递归来完全重新思考/重写您的代码。

    【讨论】:

      猜你喜欢
      • 2019-08-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-26
      • 1970-01-01
      • 1970-01-01
      • 2016-06-01
      • 2020-07-24
      相关资源
      最近更新 更多