尾呼叫
尾呼叫(tail call)是函式式程式設計的乙個重要概念,本身非常簡單,即指某個函式的最後一步呼叫另乙個函式。
function
f(x)
上述**中,函式的最後一步是呼叫函式g,這就叫尾呼叫。
以下三種情況,都不屬於尾呼叫。
function
f(x)
function
f(x)
function
f(x)
上面**中,第乙個呼叫函式g後還有賦值操作。第二種也是有後續的操作。而第三個則等同於
function
f(x)
尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置。
我們知道,函式的呼叫會在記憶體姓曾乙個呼叫記錄,又稱為call frame,儲存呼叫位置和內部變數等資訊。如果在函式a的內部呼叫函式b,那麼在a的call frame的上方還會形成乙個b的call frame。等b執行結束,將結果返回a,b的call frame才會消失。如果函式b內部還呼叫c,那麼還有乙個c的call frame,以此類推,所有的call frame 就形成了乙個call stack。
尾呼叫優化
尾呼叫由於是函式的最後一步操作,如果呼叫位置,內部變數等資訊都不會再用到了。尾呼叫時可以刪除此call frame下方的call frame。這就叫做尾呼叫優化(tail call optimization)。
當然,有些尾呼叫還會用到內部變數如:
function
addone
(a) return inner(a);
}
這時候就無法進行尾呼叫優化。
尾遞迴
函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。
遞迴非常的耗費記憶體,因為需要同時儲存成百上千個call frame。很容易發生棧溢位錯誤。對於尾呼叫優化後的尾遞迴來說,因為只有乙個call frame。所以不會存在棧溢位的問題。
如下面階乘計算遞迴:
function
factorail
(n)
這種情況下降無法進行尾呼叫優化。所以這裡的空間複雜度為o(n)。
而如果改寫成尾遞迴並進行優化,則空間複雜度為o(1),像下面這樣
function
factorial
(n, total)
尾遞迴改寫
尾遞迴的實現,往往需要改寫遞迴函式,確保最後一步只呼叫自身。做到這一點的方法,就是把所有用刀的內部變數改寫成函式的引數。像上面的階乘函式改寫一樣。這樣做的缺點是函式不再直觀,或者說會更改函式的呼叫形式。
有兩種方法可以解決這個問題:
一是進行進一步的封裝,並提供預設值
function
tailfactorial
(n, total)
function
factorial
(n)
二是使用(柯理化)currying。意思是將多引數的函式轉換成單引數的函式的形式。
function
curring
(fn, n)
}function
tailfactorail
(n, total)
const factorial = curring(tailfactorail, 1);
factorial(5);
這種方法的原理與第一種類似。只是使用了較為規範的封裝。
尾呼叫優化
前面一直提到尾呼叫優化,那麼尾呼叫優化是怎麼實現的呢。優化的目標就是減少呼叫棧,普通尾呼叫優化實現不清楚。下面闡述尾遞迴函式的優化,尾遞迴優化的策略就是用迴圈替換掉遞迴:
如下面這個遞迴函式:
function
sum(x, y) else
}sum(1, 100000)
上述函式將會產生100000個call frame,通常情況下回報棧溢位異常或錯誤。
蹦床函式
可以使用蹦床函式(trampoline)將遞迴轉化為迴圈
function
trampoline
(f) return f;
}
上面就是蹦床函式的實現,可見其中的引數f必須在執行之後返回乙個同形式的函式。
所以我們需要將遞迴函式改寫成這樣:
function
sum(x, y) else
}
即使用bind將乙個函式形式繫結到sum變數上並返回。以下是bind的作用
bind()方法會建立乙個新函式,當這個新函式被呼叫時,它的this值是傳遞給bind()的第乙個引數, 它的引數是bind()的其他引數和其原本的引數.上述**中,sum函式每次執行,都會返回自身的另乙個版本(新函式)。像下面這樣呼叫,就不會發生棧溢位
trampoline(sum(1, 100000))
真正的優化策略
然而,蹦床函式並不是真正的尾遞迴的優化, 下面的實現才是。
function
tco(f)
active = false;
return value;}};
}var sum = tco(function
(x, y)
else
});sum(1, 100000)
尾呼叫 尾遞迴
首先什麼是尾呼叫呢?我的理解是在,函式的最後呼叫乙個函式,並不包含該函式的任何變數。如 def f n return g n 複製 這個就是尾呼叫,尾呼叫的乙個好處就是,不用生成呼叫棧,因為假設是個尾呼叫,那麼當我執行到函式末尾的時候,這個函式相關的資訊我都可以不用保留了,因此不會出現棧溢位的問題。...
尾呼叫和尾遞迴
造成這樣的結果是因為每個函式在呼叫另乙個函式的時候,沒有return該呼叫,所以執行引擎會認為你還沒有呼叫完畢,會保留呼叫幀。而如果使用尾呼叫優化,呼叫幀就永遠只有一條,這個時候就會節省很大一部分的記憶體空間,維護了 執行的流暢性。以上 就叫做尾呼叫優化,這個時候呼叫幀就永遠只有一條,節省了部分記憶...
遞迴尾呼叫
什麼是遞迴尾呼叫 遞迴呼叫放在函式結尾 區別 def fac n if n 0 return 1 else return n fac n 1 def print fa n if n 0 print fa n 1 print n print fa 10 結果 1到10def fac n if n 0 ...