在Option.getOrElse上断言@tailrec

Syn*_*sso 2 scala tail-recursion

在下面,该行maybeNext.map{rec}.getOrElse(n)使用Optionmonad来实现递归或转义模式.

scala> @tailrec                                          
     | def rec(n: Int): Int = {                          
     |   val maybeNext = if (n >= 99) None else Some(n+1)
     |   maybeNext.map{rec}.getOrElse(n)                 
     | }
Run Code Online (Sandbox Code Playgroud)

看起来不错,但是:

<console>:7: error: could not optimize @tailrec annotated method: 
it contains a recursive call not in tail position
       def rec(n: Int): Int = {
           ^
Run Code Online (Sandbox Code Playgroud)

我觉得编译器应该能够在这种情况下整理尾递归.它相当于以下(有点令人厌恶,但可编译)的样本:

scala> @tailrec                                          
     | def rec(n: Int): Int = {                          
     |   val maybeNext = if (n >= 99) None else Some(n+1)
     |   if (maybeNext.isEmpty) n                        
     |   else rec(maybeNext.get)                         
     | }
rec: (n: Int)Int
Run Code Online (Sandbox Code Playgroud)

有人能提供照明吗?为什么编译器无法弄明白呢?这是一个错误,还是一个疏忽?问题太难了吗?

编辑:@tailrec从第一个示例中删除并编译方法; 循环终止.最后一次通话总是getOrElse相当于if option.isEmpty defaultValue else recurse.我认为这可以而且应该由编译器推断出来.

Dan*_*ral 8

它不是一个bug,它不是一个疏忽,它不是一个尾递归.

是的,您可以以尾递归方式编写代码,但这并不意味着每个等效算法都可以进行尾递归.我们来看看这段代码:

maybeNext.map{rec].getOrElse(n)
Run Code Online (Sandbox Code Playgroud)

首先,最后一个电话是getOrElse(n).此调用不是可选的 - 它始终是,并且必须调整结果.但是,让我们忽略它.

倒数第二个电话是map{rec}.rec.其实,rec不叫在你的代码!其他一些函数调用它(事实上,它不是最后一次调用map),但不是你的函数.

对于尾部递归的东西,你需要能够用"goto"替换这个调用,可以这么说.像这样:

def rec(n: Int): Int = { 
  BEGINNING:                         
  val maybeNext = if (n >= 99) None else Some(n+1)
  if (maybeNext.isEmpty) n                        
  else { 
    n = maybeNext.get
    goto BEGINNING
   }                         
}
Run Code Online (Sandbox Code Playgroud)

怎么会在其他代码中发生?

def rec(n: Int): Int = {                  
  BEGINNING:        
  val maybeNext = if (n >= 99) None else Some(n+1)
  maybeNext.map{x => n = x; goto BEGINNING}.getOrElse(n)                 
}
Run Code Online (Sandbox Code Playgroud)

这里的goto 不在里面rec.这里面是一个匿名Function1apply,其中,其又是内部Optionmap,所以这里的一个分支会离开每个呼叫的两个栈帧.假设首先可以进行方法间分支.