【问题标题】:Why return in getOrElse makes tail recursion not possible?为什么在 getOrElse 中返回会使尾递归成为不可能?
【发布时间】:2015-03-02 10:08:30
【问题描述】:

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

@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:它包含一个不在尾部位置的递归调用

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

@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)
}

【问题讨论】:

  • 我感觉编译器无法确定您的方法是否是尾递归,因为 return 语句,可能他是在防御并告诉您不能保证递归。

标签: scala recursion tail-recursion


【解决方案1】:

发生这种情况是因为您的第一个 sn-p 中的 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 {
    // ...
  }
}

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

另外,这个sn-p中的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 参数所需的类型。尽管评估非常简单,e 甚至在执行 map 之前从封闭函数返回(显然,在方法调用之前评估参数)。这可能会令人困惑,因为您希望 map(return e) 被解析/解释为 map(_ => return e),但事实并非如此。

【讨论】:

  • 您能否更详细地解释一下map( return s ) 是如何处理的?我之前对它有一些奇怪的感觉,但我仍然不明白它的作用 - 为什么编译器接受它,它如何匹配预期的映射签名,应该是 Unit => X?
【解决方案2】:

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

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

this 博客文章或this 问题中了解有关在Scala 中实现return 的更多信息。

【讨论】:

    【解决方案3】:

    return 总是中断递归调用。你应该把你的代码改成这样:

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

    【讨论】:

    • 在您自己的示例中,您可以将case Nil => s 替换为case Nil => return s,并且该函数仍将编译为尾递归?
    • 是的,因为尾递归的最后一次调用应该是returnnext call the same function。在您的代码中,return 不在最后一步。您的 return 是 val 分配的替代方案,然后(在下一步中)最终是递归调用。
    • 恐怕还是看不出原因。 :(getOrElse 中的returnSome(()).map( return s ) 中的 return 或内联的 getOrElse 有何不同?另外,是否有一些链接列出了您提到的要求:“最后一次调用应该是 return 或 next调用相同的函数”?
    • 我添加了更多示例。两个map 变体有什么区别?为什么map( return s ) 编译而map( x => return s ) 不编译?
    【解决方案4】:

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

    @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 语句总是返回一些东西。

    【讨论】:

    • 我试过了,编译器也拒绝了。
    猜你喜欢
    • 2015-12-21
    • 1970-01-01
    • 2019-09-13
    • 2013-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多