Котлин и Хвост Рекурсия

1. Вступление

Некоторые алгоритмы работают лучше всего, когда реализованы рекурсивным способом - где вычисления основаны на более простой форме того же вычисления.

В большинстве языков программирования существует риск переполнения стека, связанного с рекурсией. Существует ограничение на количество вложенных вызовов методов, которые можно выполнить за один раз, без возврата.

Если это проблема, алгоритм может быть переписан в обязательном порядке, используя вместо этого традиционный цикл.

  • Хвостовая рекурсия - это метод, при котором компилятор может переписать рекурсивный метод императивным образом ** , предполагая, что соблюдены определенные правила.

2. Правила Хвостовой рекурсии в Котлине

Чтобы реализовать функцию в Kotlin с использованием хвостовой рекурсии, нужно соблюдать одно правило: рекурсивный вызов должен быть самым последним вызовом метода . Это правило не так просто следовать, как кажется. Например, на примере факториала это будет реализовано следующим образом:

Это правило не так просто следовать, как кажется. Например, на примере факториала это будет реализовано следующим образом:

fun recursiveFactorial(n: Long) : Long {
    return if (n <= 1) {
        n
    } else {
        n **  recursiveFactorial(n - 1)
    }
}

Это прекрасно работает. Однако он не подходит для рекурсии хвоста.

Сломанная выше функция выполняет следующее:

, Если n равен ⇐ 1, вернуть n

, Вычислить accum = recursiveFactorial (n - 1)

, вернуть n ** накоп

Таким образом, вы можете видеть, что рекурсивный вызов не является последним вызовом функции.

3. Реализация факториала как рекурсии хвоста

Вместо этого, чтобы реализовать функцию факториала с использованием хвостовой рекурсии, нам нужно переработать ее, чтобы изменить место выполнения расчета. Нам нужно убедиться, что умножение выполняется до рекурсивного вызова, а не после . Это проще всего сделать, передав частичный результат в качестве параметра:

fun factorial(n: Long, accum: Long = 1): Long {
    val soFar = n **  accum
    return if (n <= 1) {
        soFar
    } else {
        factorial(n - 1, soFar)
    }
}

Это может быть разбито на следующее:

, Рассчитать soFar = n ** АКБ

, Если n ⇐ 1, верните soFar

, Вызовите функцию factorial , передавая n - 1 и soFar

На этот раз рекурсивный вызов - последний шаг в нашем процессе.

Как только мы получим рекурсивную функцию в правильной форме, мы даем указание Kotlin рассмотреть ее для хвостовой рекурсии с использованием ключевого слова tailrec . Это информирует компилятор о том, что он может переписать функцию как цикл, если он может это сделать. Это ключевое слово относится к самой функции, поэтому становится:

tailrec fun factorial(n: Long, accum: Long = 1): Long

4. Вывод сборника факториала

Целью этого является написание рекурсивного кода, который запускается обязательным образом, чтобы избежать проблем переполнения стека. Если мы декомпилируем вышеприведенную функцию, мы видим, что результат, полученный компилятором, действительно обязателен:

public final long factorial(long n, long accum) {
   while(n > (long) 1) {
      long var10000 = n - (long)1;
      accum = n **  accum;
      n = var10000;
   }

   return n **  accum;
}

Мы можем сразу увидеть, как это работает, и заметить, что в этом коде нет абсолютно никакого риска переполнения стека.

5. Улучшения производительности

Мы можем иногда видеть улучшения производительности, используя эту оптимизацию, а также повышение безопасности. Эти преимущества зависят от некоторых других факторов, таких как глубина рекурсии и сложность вычислений.

  • Улучшения связаны с тем, что вызовы методов обходятся дороже, чем просто циклы. **

Снова используя наш факторный пример, мы можем измерить, сколько времени потребуется для выполнения, и сравнить:

  • Расчет факториала (50) 1 000 000 раз без рекурсии хвоста

занимает ~ 70 мс ** Расчет факториала (50) 1 000 000 раз с использованием хвостовой рекурсии

~ 45ms

Используя наивный тест, мы получили ускорение на 36%, что важно только для того, чтобы позволить компилятору переделать нашу реализацию.

Обратите внимание, что эти измерения взяты из очень простого сравнения простой функции. Фактические изменения производительности будут варьироваться в зависимости от обстоятельств и всегда должны оцениваться до принятия каких-либо решений.

6. Резюме

В некоторых случаях хвостовая рекурсия может дать некоторые важные преимущества нашему коду - как в отношении безопасности, так и производительности. Компилятор Kotlin может сделать это автоматически - нам просто нужно использовать правильное ключевое слово.