簡體   English   中英

Haskell中的尾遞歸

[英]Tail Recursion in Haskell

我試圖理解Haskell中的尾遞歸。 我想我明白它是什么以及它是如何工作的但是我想確保我沒有搞砸了。

這是“標准”因子定義:

factorial 1 = 1
factorial k = k * factorial (k-1)

在運行時,例如, factorial 3 ,我的函數將自己調用3次(給它或者拿它)。 如果我想計算因子99999999,這可能會產生問題,因為我可能有堆棧溢出。 在得到factorial 1 = 1我將不得不在堆棧中“返回”並乘以所有值,因此我有6個操作(3個用於調用函數本身,3個用於乘以值)。

現在我向您介紹另一種可能的因子實現:

factorial 1 c = c
factorial k c = factorial (k-1) (c*k)

這個也是遞歸的。 它會稱自己為3次。 但它沒有問題,然后仍然必須“回來”計算所有結果的乘法,因為我已經將結果作為函數的參數傳遞。

根據我的理解,這就是Tail Recursion的內容。 現在,它似乎比第一個好一點,但你仍然可以輕松地擁有堆棧溢出。 我聽說Haskell的編譯器會在后台將Tail-Recursive函數轉換為for循環。 我想這就是為什么它能夠為尾遞歸功能付出代價呢?

如果這就是原因,那么如果編譯器不打算做這個聰明的技巧,那么絕對沒有必要嘗試使函數尾遞歸 - 我是對的嗎? 例如,雖然理論上C#編譯器可以檢測並將尾遞歸函數轉換為循環,但我知道(至少是我所聽到的)目前它沒有這樣做。 所以現在絕對沒有必要使函數尾遞歸。 是嗎?

謝謝!

這里有兩個問題。 一個是尾遞歸,另一個是Haskell處理事物的方式。

關於尾遞歸,您似乎有正確的定義。 有用的部分是,因為只需要每次遞歸調用的最終結果,所以不需要在堆棧上保留較早的調用。 該函數不是“自稱”,而是更接近於“替換”自身,最終看起來就像一個迭代循環。 這是一個非常直接的優化,正常的編譯器通常會提供。

第二個問題是懶惰評估 因為Haskell只根據需要計算表達式,所以默認情況下尾遞歸並不像通常那樣工作。 它不是替換每個調用,而是構建一個巨大的嵌套“thunk”堆,即尚未請求其值的表達式。 如果這個thunk堆足夠大,它確實會產生堆棧溢出。

Haskell實際上有兩個解決方案,具體取決於您需要做什么:

  • 如果結果由嵌套數據構造函數組成 - 比如生成一個列表 - 那么你想避免尾遞歸; 而是將遞歸放在其中一個構造函數字段中。 這將使結果也變得懶惰並且不會導致堆棧溢出。

  • 如果結果由單個值組成,則需要嚴格評估它,以便在需要最終值時立即強制遞歸的每個步驟。 這給出了通常的尾遞歸假迭代。

另外,請記住,GHC非常聰明,如果您使用優化進行編譯,它通常會發現評估應該嚴格的地方並為您處理。 但是,這在GHCi中不起作用。

您應該使用內置機制,然后您不必考慮使函數尾遞歸的方法

fac 0 = 1
fac n = product [1..n]

或者,如果產品尚未定義:

fac n = foldl' (*) 1 [1..n]

(請參閱http://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl%27關於哪個折疊...使用的版本)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM