Clojure消除互遞迴

再議遞迴

透過 loop/recur 來支援顯式的尾部呼叫最佳化。

互遞迴(mutualrecursion)。

當兩個或更多個函式相互之間來回呼叫時,就發生了互遞迴。A呼叫B再呼叫A了。

Clojure消除互遞迴

因為my-odd?和my-even?彼此呼叫了對方,需要在真正定義著兩個函式之前,先建立這兩個變數。可以用def來做這件事,不過declare宏允許在一行程式碼裡同時建立兩個變數(沒有初始繫結)。

Clojure消除互遞迴

my-odd?和 my-even?會根據引數的大小,成正比的消耗棧幀,所以,當遇到很大的數字時,它們就玩完了。

Clojure消除互遞迴

recur只能用於自遞迴,對互遞迴則無能力為。

Clojure消除互遞迴

別的遞迴問題可就不會這麼簡單了,往往找不到既優雅,同時還用不著遞迴的解決方案。

用來解決那些複雜問題的方法。

● 轉換為自遞迴。

● 採用Trampolining技術。

● 用惰性化取代遞迴。

● 用快存為遞迴抄條近路。

轉換為自遞迴

對彼此獨立但卻存在某種聯絡的概念進行建模時,互遞迴往往是種很好的方式。

Clojure消除互遞迴

Clojure消除互遞迴

基於partity實現my-odd?和my-even?

Clojure消除互遞迴

採用Trampolining技術

Trampolining 是一種用於最佳化互遞迴的技術。trampoline 就像是一種事後生效的recur,由函式的呼叫者來施行,而不是實現者。因為在 trampoline 函式內部,呼叫者可以呼叫多個函式,所以trampoline函式可以用來最佳化互遞迴。讓Clojure的trampoline函式去呼叫互遞迴函式中的一員。

Clojure消除互遞迴

Clojure消除互遞迴

如果返回值是一個函式,那麼 trampoline 會假定呼叫者打算遞迴的呼叫這個函式,並替呼叫者執行該呼叫。

trampoline 管理著它自己的 recur,所以它會一直呼叫f的函式,直到返回的不再是函式為止。

Clojure消除互遞迴

Clojure消除互遞迴

由於 trampoline 替你做了 recur,所以它能妥善處理很大的輸入值,不會丟擲StackOverflowError。

Clojure消除互遞迴

trampoline沒有任何優勢可言,所以還是應該優選recur。

但對於互遞迴而言,trampoline就可以好好施展一番了。使用trampoline對這兩個消耗棧的蹩腳實現加以轉換:在所有尾部遞迴呼叫前,簡單地加上一個#符號。

Clojure消除互遞迴

與最初實現的唯一差別,就是在第6和第11行的#包裝。

Clojure消除互遞迴

trampoline 是一種用來解決特定問題的特殊方案。它需要把原始函式的返回值篡改為不一樣的型別,用來指示遞迴。

用惰性化替代遞迴

Clojure消除互遞迴

Clojure消除互遞迴

replace-symbol 和 replace-symbol-expression 是兩個互遞迴函式,所以傳進去的結構如果巢狀過深的話,就會撐爆棧空間。

Clojure消除互遞迴

Clojure消除互遞迴

Clojure提供了一個*print-level*,可以用來控制Clojure的列印器會深入一個巢狀資料結構到何種程度。把*print-level*設定為一個適度的值,這樣列印器就不會瘋狂地試圖去列印一個深度巢狀的結構了。

當巢狀的更深時,列印器只是簡單的停了下來,並打印出一個#。

Clojure消除互遞迴

Clojure消除互遞迴

所有對replace-symbol的遞迴呼叫都位於cons之中。為了打破遞迴,只需要把遞迴用 lazy-seq 給包裝起來就行了。

Clojure消除互遞迴

在第 4 行,lazy-seq 打破了遞迴,防止了遇到深度巢狀的結構時的棧溢位。另一處改進位於第2行。這裡不再使用兩個不同的函式來分別處理符號和列表,而是採用了多重方法replace-symbol,其中的一個方法面向列表,另一個則面向符號。這麼做,不但消除了一個if,還提高了可讀性。

Clojure消除互遞迴

用快存為遞迴抄條近路

女性與男性序列定義

Clojure消除互遞迴

Clojure消除互遞迴

序列中的每個值都需要從頭計算另外的兩個值,而這又需要依次從頭計算另外的兩個值。

Clojure消除互遞迴

快存,就是把過去的計算結果給快取起來,用空間來換取時間。

Clojure消除互遞迴

現在對於每一個n值,Clojure僅需計算一次F和M即可。

Clojure消除互遞迴

Clojure消除互遞迴

然而,單獨用快存還是不夠的。只有在快取得到填充的情況下,快存才會為遞迴提供捷徑。如果是從一個空快取開始,並且讓m或f去處理很大的數,那麼在快取被建立起來之前,棧空間就會被撐爆。

Clojure消除互遞迴

為了保證快取能萬丈高樓平地起,把暴露方式由函式變成序列。

Clojure消除互遞迴

Clojure消除互遞迴

Clojure消除互遞迴

● 以自然的方式定義一個互遞迴函式。

● 為了給遞迴找條捷徑,對那些已經被計算過的值使用快存。

● 暴露為一個序列,這樣依賴的那些值在使用之前就已經被快取起來了。

這種方法會消耗堆空間,因為它需要快取所有之前的值。

身處一個沒有可變狀態的世界,並且很可能會開始探討狀態單子(statemonad)。