В Scala принято писать функции, локальные для тела другой функции. В функциональном программировании мы не должны рассматривать это как нечто большее, чем локальные целые числа или строки.
Мы пишем циклы функционально, без изменения переменной al oop, с помощью рекурсивной функции. Здесь мы определяем рекурсивную вспомогательную функцию внутри тела факториальной функции. Такую вспомогательную функцию часто называют go или l oop по соглашению.
def factorial(n: Int): Int = {
def go(n: Int, acc: Int): Int =
if (n <= 0) acc
else go(n - 1, n * acc)
go(n, 1)
}
Поскольку она локальная, на функцию go можно ссылаться только из тела факториальной функции, так же, как и локальная переменная. Наконец, определение факториала состоит из вызова go с начальными условиями для l oop.
Аргументы go - это состояние для l oop. В данном случае это оставшееся значение n и текущий накопленный факториал a cc. Чтобы перейти к следующей итерации, мы просто рекурсивно вызываем go с новым состоянием l oop (здесь go(n-1, n*acc))
, а для выхода из l oop мы возвращаем значение без рекурсивного вызова (здесь, мы возвращаем cc в случае, если n <= 0
). Scala обнаруживает такого рода саморекурсию и компилирует его в тот же вид байт-кода, который будет выдаваться на некоторое время l oop (мы можем писать while циклы вручную в Scala, но это редко необходимо и считается плохим тоном, поскольку мешает хорошему композиционному стилю.), пока рекурсивный вызов находится в хвостовой позиции.
Вызов считается в хвосте позиции, если вызывающий не делает ничего, кроме возврата значения рекурсивного вызова. Например, рекурсивный вызов go(n-1,n*acc)
находится в хвостовой позиции, поскольку метод возвращает значение этого рекурсивного вызова напрямую и больше ничего с ним не делает. с другой стороны, если бы мы сказали, что 1 + go(n-1,n*acc)
, go функция больше не будет в хвостовой позиции, так как у метода все равно будет работа, когда go вернет ее s результат (а именно прибавление к нему 1). Если все рекурсивные вызовы, сделанные функцией, находятся в хвостовой позиции, Scala автоматически компилирует рекурсию в итерационные циклы, которые не используют кадры стека вызовов для каждой итерации, и мы можем избежать проблем с StackOverflow. По умолчанию Scala не сообщает нам, было ли устранение хвостового вызова успешным, но если мы ожидаем, что это произойдет для рекурсивной функции, которую мы пишем, мы можем сообщить компилятору Scala об этом предположении, используя tailre c аннотации, поэтому она может выдать нам ошибку компиляции, если не может устранить хвостовые вызовы функции.
def factorial(n: Int): Int = {
@annotation.tailrec
def go(n: Int, acc: Int): Int =
if (n <= 0) acc
else go(n - 1, n * acc)
go(n, 1)
}
Это очень распространенный сценарий для написания внутренней функции или локального определения.