为什么在getOrElse中返回会导致尾递归不可能?

时间:2015-03-02 10:08:31

标签: scala recursion tail-recursion

我对以下代码感到困惑:代码是人为的,但我认为它仍然是尾递归的。编译器不同意并产生错误消息:

@annotation.tailrec
def listSize(l : Seq[Any], s: Int = 0): Int = {
  if (l.isEmpty) {
    None.getOrElse( return s )
  }
  listSize(l.tail, s + 1)
}

上面的代码如何使尾部回归不可能?为什么编译器会告诉我:

  

无法优化@tailrec带注释的方法listSize:它包含一个不在尾部位置的递归调用

类似的代码(return内的map)编译良好:

@annotation.tailrec
def listSize(l : Seq[Any], s: Int = 0): Int = {
  if (l.isEmpty) {
    Some(()).map( return s )
  }
  listSize(l.tail, s + 1)
}

即使通过内联None.isEmpty获得的代码编译也很好:

@annotation.tailrec
def listSize(l : Seq[Any], s: Int = 0): Int = {
  if (l.isEmpty) {
    if (None.isEmpty) {
      return s
    } else None.get
  }
  listSize(l.tail, s + 1)
}

另一方面,编译器拒绝了稍微修改过的地图的代码并产生错误:

@annotation.tailrec
def listSize(l : Seq[Any], s: Int = 0): Int = {
  if (l.isEmpty) {
    Some(()).map( x => return s )
  }
  listSize(l.tail, s + 1)
}

4 个答案:

答案 0 :(得分:4)

之所以会发生这种情况,是因为第一个代码段中的return是非本地代码(它嵌套在lambda中)。 Scala使用异常来编译非本地return表达式,因此编译器会从中转换代码:

@annotation.tailrec
def listSize(l : Seq[Any], s: Int = 0): Int = {
  if (l.isEmpty) {
    None.getOrElse( return s )
  }
  listSize(l.tail, s + 1)
}

类似的内容(使用scalac -Xprint:tailcalls编译):

def listSize2(l : Seq[Any], s: Int = 0): Int = {
  val key = new Object

  try {
    if (l.isEmpty) {
      None.getOrElse {
        throw new scala.runtime.NonLocalReturnControl(key, 0)
      }
    }

    listSize2(l.tail, s + 1)
  } catch {
    case e: scala.runtime.NonLocalReturnControl[Int @unchecked] =>
      if (e.key == key)
        e.value
      else
        throw e
  }
}

最后一点是,当包装在try / catch块中时,递归调用不是尾调用。基本上,这是一个受到尊重的例子:

def self(a: Int): Int = {
  try {
    self(a)
  } catch {
    case e: Exception => 0
  }
}

类似于此,显然不是尾递归的:

def self(a: Int): Int = {
  if (self(a)) {
    // ...
  } else {
    // ...
  }
}

在某些特殊情况下,您可以对此进行优化(如果不是一个,则可以优化为两个堆栈帧),但似乎并不存在适用于此类情况的通用规则。

此外,此代码段中的return表达式不是非本地return,这就是可以优化该功能的原因:

@annotation.tailrec
def listSize(l : Seq[Any], s: Int = 0): Int = {
  if (l.isEmpty) {
    // `return` happens _before_ map `is` called.
    Some(()).map( return s )
  }
  listSize(l.tail, s + 1)
}

上述工作原因是,在Scala中,return e是表达式,而不是语句。它的类型是Nothing,它是所有内容的子类型,包括Unit => X,这是map的param所需的类型。虽然评估非常简单,但在e执行之前从封闭函数返回map(显然在方法调用之前计算参数)。这可能会令人困惑,因为您希望map(return e)被解析/解释为map(_ => return e),但事实并非如此。

答案 1 :(得分:2)

这几乎肯定是编译器或部分实现的功能的错误。

它很可能与Scala中表达式中return的实现有关。使用异常实现非本地return语句,以便在调用return时抛出NonLocalReturnException,并将整个表达式包装在try-catch中。我敢打赌x => return x被转换为嵌套表达式,当它包含在try-catch中时,在确定它是否可以使用@tailrec时会使编译器感到困惑。我甚至会说,应该避免将@tailrec与非本地return结合使用。

this博客文章或this问题中详细了解Scala中return的实施情况。

答案 2 :(得分:0)

return总是打破递归调用。您应该将代码更改为以下内容:

@tailrec
def listSize(l : Seq[Any], s: Int = 0): Int = {
  l match {
    case Nil => s
    case head :: tail => listSize(tail, s + 1)
  }  
}

答案 3 :(得分:-1)

我现在无法尝试,但这会解决问题吗?

@annotation.tailrec
def listSize(l : Seq[Any], s: Int = 0): Int = {
  if (l.isEmpty) {
    None.getOrElse( return s )
  } else {
    listSize(l.tail, s + 1)
  }
}

使用if-else而不只是if将确保if语句始终返回。