Популярный и общепринятый ответ на этот вопрос на самом деле вводит в заблуждение, потому что сам вопрос сбивает с толку. OP не делает различий между tailrec
и TCO
, и ответ не касается этого.
Ключевым моментом является то, что требования для tailrec
более строгие, чем требования для TCO
.
Аннотация tailrec
требует, чтобы хвостовые вызовы выполнялись для одной и той же функции, тогда как TCO
можно использовать для хвостовых вызовов любой функции.
Компилятор может использовать TCO
на fact
, потому что есть вызов в хвостовой позиции. В частности, он может превратить вызов к fact
в переход к fact
, настроив стек соответствующим образом. Не имеет значения, что эта версия fact
не совпадает с функцией, выполняющей вызов.
Таким образом, принятый ответ правильно объясняет, почему незавершенная функция не может быть tailrec
, потому что вы не можете гарантировать, что хвостовые вызовы относятся к той же функции, а не к перегруженной версии функции. Но это неправильно подразумевает, что использовать TCO
в этом методе небезопасно, хотя на самом деле это было бы совершенно безопасно и было бы хорошей оптимизацией.
[Обратите внимание, что, как объяснил Джон Харроп, вы не можете реализовать TCO
на JVM, но это ограничение компилятора, а не языка, и не связано с tailrec
]
И для справки, вот как можно избежать проблемы, не применяя метод final
:
class C {
def fact(n: Int): Int = {
@tailrec
def loop(n: Int, result: Int): Int =
if (n == 0) {
result
} else {
loop(n - 1, n * result)
}
loop(n, 1)
}
}
Это работает, потому что loop
- это конкретная функция, а не метод, и ее нельзя переопределить. Эта версия также имеет преимущество удаления ложного параметра result
на fact
.
Это шаблон, который я использую для всех рекурсивных алгоритмов.
person
Tim
schedule
27.02.2019
TCO
, который можно безопасно использовать с этим методом, и более строгийtailrec
, который нельзя использовать, потому что метод не может быть саморекурсивным. - person Tim   schedule 27.02.2019