Recca Chao 的 gitHub page

推廣網站開發,包含 Laravel 和 Kotlin 後端撰寫、自動化測試、讀書心得等。Taiwan Kotlin User Group 管理員。

View on GitHub

Kotlin 尾遞迴

遞迴的問題

遞迴的撰寫雖然簡潔,但是因為其架構的緣故,如果不好好處理的話,那麼就可能會在運行時出現問題

我們來看看這段個函式

fun sum(number: Long): Long {
    return when (number) {
        1L -> 1L
        else -> sum(number - 1) + number
    }
}

這段程式的邏輯很簡單:如果輸入是 1 就回傳 1,如果不是的話,就往下一層遞迴,並加上參數內輸入的數字

所以,如果輸入的 number5,那麼運作就會是

在一般的狀況下,這段程式運作不會有太大問題。可是我們可以看出來,運作程式時需要一層一層地將函式的狀態都紀錄下來,直到 sum(1) 的時候回傳了 1 我們才能開始解析 sum(2)sum(3)⋯⋯

這樣的結構,如果輸入很大的話,那麼記憶體不就可能會不夠嗎?

我們來試看看

fun main(){
    println(sum(999_999_999))
}

我們可以看到錯誤 Exception in thread "main" java.lang.StackOverflowError

這是什麼意思呢?這個錯誤的意思是,我們用來記錄函式呼叫的堆疊(stack)已經滿出來(overflow)了,所以出現了錯誤。

什麼是尾遞迴

尾遞迴(tail recursion)的定義是,在遞迴函式的最後面,只有呼叫一個函式。

這樣的話,由於呼叫時所需要的資料不包含現在函式的狀態,所以你可以不需要紀錄現在函式,也可以計算出最後的答案

我們來參考這個函式

fun tailSum(number: Long, answer: Long = 0): Long {
    return when (number) {
        0L -> answer
        else -> tailSum(number - 1, answer + number)
    }
}

乍看之下,我們並沒有改變到什麼,tailSum(5) 的答案一樣是 15,只是現在的堆疊會長成

不過,對編譯器來說,有很多方法可以優化這段程式,比方說將 tailSum 的呼叫改成用 goto 實現,或者用迴圈的方式實作⋯⋯等等。

這也是一般現代編譯器裡面,尾遞迴的程式效能會比起一般遞迴更好的原因。

Kotlin 的尾遞迴

如果我們嘗試運作 tailSum()

fun main(){
    println(tailSum(999_999_999))
}

我們一樣會得到 StackOverflowError。這是因為我們沒有告訴 kotlin 編譯器這是一個尾遞迴的函式,所以並沒有太多的優化。

我們在這個函式前面加上一個關鍵字 tailrec

tailrec fun tailSum(number: Long, answer: Long = 0): Long {
    return when (number) {
        0L -> answer
        else -> tailSum(number - 1, answer + number)
    }
}

這時候 println(tailSum(999_999_999)) 就可以跑出我們的答案 499999999500000000 了!

如果我們嘗試將一般遞迴的函式前面加上 tailrec

tailrec fun sum(number: Long): Long {
    return when (number) {
        1L -> 1L
        else -> sum(number - 1) + number
    }
}

這時 IDE 會跳出提示,告訴我們雖然這個函式被宣告為尾遞迴,但是並沒有找到最後面只有呼叫一個函式的宣告。運行這段程式時也依然會有 StackOverflowError 的問題