Tricky ::重写函数是尾递归的

时间:2013-12-31 16:11:00

标签: scala recursion functional-programming tail-recursion

我有这个scala函数,由于性能问题,需要重写为tail-recursive。在处理不太大的数据集时,堆栈会逐渐消失,因此我得出结论,只有通过使其尾递归才可以修复它。 这是函数::

private def carve2(x: Int, y: Int) {

    var rand: Int = random.nextInt(4)

    (1 to 4) foreach { _ =>
        val (x1, y1, x2, y2) = randomize(x, y, rand)

        if (canUpdate(x1, y1, x2, y2)) {
           maze(y1)(x1) = 0
           maze(y2)(x2) = 0
           carve2(x2, y2)
       }
       rand = (rand + 1) % 4
    }

}

主要问题是::

> How to get rid of the foreach/for loop

为此,我尝试了一种递归方法,但是使语义正确是很棘手的,特别是因为在if块内部自调用之后,rand var的值被突变...

我尝试的是将rand的状态修改推出体外,并在作为参数::

传递时将其变异
def carve3(x: Int, y: Int, rand: Int)  {

  for (i <- 1 to 4) {

    val (x1, y1, x2, y2) = randomize(x, y, rand)

    if (canUpdate(x1, y1, x2, y2)) {
       maze(y1)(x1) = 0
       maze(y2)(x2) = 0
      if (i == 1) carve3(x2, y2, random.nextInt(4))
      else carve3(x2, y2, (rand + 1) % 4)
    }
 }
}

这不起作用......

还有一件事,我知道这种编码方法不起作用,但我试图去那里......这是我尝试重新考虑的代码。 此外,randomize和canUpdate函数在此上下文中不相关。

有什么建议吗? 非常感谢...

2 个答案:

答案 0 :(得分:4)

如果函数多次调用自身,则无法将其转换为尾递归函数。只有当递归调用是它做的最后一件事时,函数才可以是尾递归的,因此它不需要记住任何东西。

解决此类问题的一个标准技巧是通过保持要在队列中计算的任务来使用堆而不是堆栈。例如:

private def carve2(x: Int, y: Int) {
    carve2(Seq((x, y)));
}

@annotation.tailrec
private def carve2(coords: Seq[(Int,Int)]) {
    // pick a pair of coordinates
    val ((x, y), rest) = coords match {
      case Seq(x, xs@_*) => (x, xs);
      case _             => return; // empty
    }
    // This is functional approach, although perhaps slower.
    // Using a `while` loop instead would result in faster code.
    val rand: Int = random.nextInt(4)
    val add: Seq[(Int,Int)] =
      for(i <- 1 to 4;
          (x1, y1, x2, y2) = randomize(x, y, (i + rand) % 4);
          if (canUpdate(x1, y1, x2, y2))
         ) yield {
        // do something with `maze`
        // ...
        // return the new coordinates to be added to the queue
        (x2, y2)
      }
    // the tail-recursive call
    carve2(rest ++ add);
}

(我没有尝试编译代码,因为您发布的代码示例不是自包含的。)

这里carve2在尾递归循环中运行。每次传递都可能在队列末尾添加新坐标,并在队列为空时完成。

答案 1 :(得分:3)

我假设您的探查器已将此识别为热点:)

正如您所正确推断的那样,您的问题是for-comprehension,它通过循环每次都会增加额外的间接级别。只有4次传递,这个成本可以忽略不计,但我可以看到该方法递归调用自己...

不会做的是从尝试重构使用尾递归开始,你可以先尝试两个更好的选择:

  1. foreach更改为for-comprehension,然后使用optimize标志进行编译,这会导致编译器发出while循环。

  2. 如果这样做无效,请手动将理解转换为while循环。

  3. 然后......只有这样,你可能想尝试尾递归,看看它是否比while循环更快。有可能它不会。

    <强>更新

    无论如何,我正朝着Petr的解决方案前进:)

    所以这里完整的东西整理得更加惯用:

    private def carve2(x: Int, y: Int) {
      carve2(List((x, y)))
    }
    
    @tailrec private def carve2(coords: List[(Int,Int)]) = coords match {
      case (x,y) :: rest =>
        val rand: Int = random.nextInt(4)
    
        //note that this won't necessarily yield four pairs of co-ords
        //due to the guard condition
        val add = for {
          i <- 1 to 4
          (x1, y1, x2, y2) = randomize(x, y, (i + rand) % 4)
          if canUpdate(x1, y1, x2, y2)
        } yield {
          maze(y1)(x1) = 0
          maze(y2)(x2) = 0
          (x2, y2)
        }
    
        // tail recursion happens here...
        carve2(rest ++ add)
    
      case _ => 
    }