2015-03-02 74 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問題。