2015-03-02 75 views
5

我很困惑下面的代码:代码是人为的,但我仍然认为它是尾递归。编译器不同意并产生一条错误消息:为什么在getOrElse中返回使尾部递归不可能?

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

上面的代码如何使tail tail recusion不可能?为什么编译器告诉我:

could not optimize @tailrec annotated method listSize: it contains a recursive call not in tail position

类似的代码(与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) 
} 
+2

我感觉编译器无法决定是否你的方法是由于return语句的尾递归,可能他是防御性的,告诉你递归不能保证。 – 2015-03-02 10:28:04

回答

4

这是因为您的第一个片段中的return是非本地片段(它嵌套在lambda中)。斯卡拉使用了异常编译非本地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块中封装时不是尾调用。基本上,这contrieved例如:

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的参数所需的类型。虽然评估很简单,但在执行map之前,函数返回e(显然,在方法调用之前评估参数)。这可能会令人困惑,因为你认为map(return e)被解析/解释为map(_ => return e),但事实并非如此。

+0

请问你能更详细地解释一下'map(return s)'是如何处理的?我对它有些奇怪的感觉之前,但我仍然没有得到确切的结果 - 为什么编译器接受它,它与预期的地图签名相匹配,应该是Unit => X? – Suma 2015-03-04 08:47:31

+0

@Suma sure。我已经更新了我的答案。 – 2015-03-05 16:33:05

0

return总是打断递归调用。你应该改变你的代码到这样的东西:

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

在你自己的例子中,你可以用'case Nil => return s'替换'case Nil => s'并且该函数仍然会被编译为尾递归? – Suma 2015-03-02 11:00:20

+0

是的,因为对于尾递归,最后一次调用应该是'return'或'next调用相同的函数'。在你的代码中,'return'并不是最后一步。您的'返回'是Val分配的替代方法,然后(在下一步中)最终是递归调用。 – 2015-03-02 11:15:11

+0

恐怕我还是看不出原因。 :(在getOrElse中,返回值与例如'Some(()).map(return s)'中的返回值不同,还是内联的'getOrElse'中的返回值是多少?另外,是否有一些链接列出了需求提到:“最后一次调用应该是返回还是下一次调用同一个函数”? – Suma 2015-03-02 12:14:14

-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语句总是返回值。

+0

我试过了,编译器也拒绝了这个。 – Suma 2015-03-02 12:24:00

2

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

它很可能与在斯卡拉表达式中执行return有关。使用异常实现非本地return语句,以便在调用return时引发NonLocalReturnException,并将整个表达式包装在try-catch中。我敢打赌,x => return x被转换为嵌套表达式,当它被封装在try-catch中时,在确定它是否可以使用@tailrec时会混淆编译器。我甚至会说,应该避免使用@tailrec与非本地return一起使用。

阅读更多关于return在斯卡拉的履行this博客文章或在this问题。