В принципе, хвостовые вызовы всегда могут повторно использовать кадр стека вызывающего
функция. Однако в некоторых средах выполнения (таких как Java VM) отсутствует
примитивы для повторного использования стекового фрейма для хвостовых вызовов. Качество продукции
Поэтому реализация Scala требуется только для повторного использования стекового фрейма
прямо-хвостовая рекурсивная функция, последним действием которой является вызов самого себя. Другие хвостовые вызовы могут
также можно оптимизировать, но не следует полагаться на это в разных реализациях.
Кто-нибудь может объяснить, что означают эти два средних предложения этого параграфа?
Хвостовая рекурсия является частным случаем хвостового вызова. Прямая рекурсия хвоста является частным случаем рекурсии хвоста. Только прямая хвостовая рекурсия гарантированно оптимизируется. Другие также могут быть оптимизированы , но это в основном просто оптимизация компилятора. Как языковая функция , Scala гарантирует только прямое устранение хвостовой рекурсии.
Так в чем же разница?
Ну, хвостовой вызов - это просто последний вызов в подпрограмме:
def a = {
b
c
}
В этом случае вызов на c
является завершающим вызовом, вызов на b
- нет.
Tail рекурсия - это когда хвостовой вызов вызывает подпрограмму, которая уже была вызвана ранее:
def a = {
b
}
def b = {
a
}
Это хвостовая рекурсия: a
вызывает b
(оконечный вызов), что, в свою очередь, вызывает a
снова. (В отличие от прямой хвостовой рекурсии, описанной ниже, ее иногда называют косвенной хвостовой рекурсией.)
Однако ни один из двух примеров не будет оптимизирован Scala. Или, точнее: реализация Scala позволяет оптимизировать их, но не требуется для этого. Это в отличие от, например, Схема, где спецификация языка гарантирует, что все вышеперечисленные случаи займут O(1)
стекового пространства.
Спецификация языка Scala * только 1045 * гарантирует, что прямая хвостовая рекурсия оптимизирована, т. Е. Когда подпрограмма напрямую вызывает сама без других звонки между:
def a = {
b
a
}
В этом случае вызов a
является хвостовым вызовом (потому что это последний вызов в подпрограмме), это хвостовая рекурсия (потому что он снова вызывает себя), и что наиболее важно, это direct хвостовая рекурсия, потому что a
напрямую вызывает себя, не выполняя еще один вызов.
Обратите внимание, что есть много тонких вещей, которые могут привести к тому, что метод не будет напрямую хвост-рекурсивным. Например, если a
перегружен, то рекурсия может фактически проходить через различные перегрузки и, следовательно, больше не будет прямой.
На практике это означает две вещи:
- вы не можете выполнить рефакторинг извлечения метода для хвостового рекурсивного метода, по крайней мере, не включая хвостовой вызов, потому что это превратит прямой хвостовой рекурсивный метод (который будет оптимизирован) в косвенный хвостовой рекурсивный метод (который не будет оптимизирован).
- Вы можете только использовать прямую хвостовую рекурсию. Отсутствует парсер хвостовой рекурсии или конечный автомат, который может быть очень элегантно выражен с помощью косвенной хвостовой рекурсии.
Основная причина этого заключается в том, что когда в вашем базовом механизме выполнения отсутствуют мощные функции управления потоком управления, такие как GOTO
, продолжения, первоклассный изменяемый стек или правильные вызовы хвоста, тогда вам нужно либо реализовать свой собственный стек поверх из этого, используйте батуты, сделайте глобальное преобразование CPS или что-то похожее противное, чтобы обеспечить обобщенные правильные хвостовые вызовы Все это оказывает серьезное влияние на производительность или совместимость с другим кодом на той же платформе.
Или, как сказал Рич Хики, создатель Clojure, когда столкнулся с той же проблемой: «Производительность, взаимодействие с Java, вызовы хвостом. Выберите два». И Clojure, и Scala решили пойти на компромисс в отношении хвостовых вызовов и обеспечить только хвостовую рекурсию, а не полные хвостовые вызовы.
Короче говоря, да: конкретный пример, который вы разместили, будет оптимизирован, поскольку это прямая хвостовая рекурсия. Вы можете проверить это, поместив аннотацию @tailrec
на метод. Аннотация не меняет, будет ли метод оптимизирован, но делает , однако гарантирует, что вы получите ошибку компиляции, если метод может не быть оптимизированным.
Из-за вышеупомянутых тонкостей, как правило, стоит добавить аннотацию @tailrec
для методов, которые необходимо оптимизировать, как для получения ошибки компиляции, так и для других разработчиков. поддержание вашего кода.