Популярный и принятый ответ на этот вопрос на самом деле вводит в заблуждение, потому что сам вопрос сбивает с толку. В ОП не проводится различие между 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
.
Это шаблон, который я использую для всех рекурсивных алгоритмов.