Kotlin e recursão da cauda

Kotlin e recursão da cauda

*1. Introdução *

Alguns algoritmos funcionam melhor quando implementados de maneira recursiva - onde uma computação é baseada em uma forma mais simples da mesma computação.

Na maioria das linguagens de programação, existe o risco de um estouro de pilha associado à recursão. Há um limite no número de chamadas de método aninhadas que podem ser feitas de uma só vez, sem retornar.

Se esse é um problema, o algoritmo pode ser reescrito de maneira imperativa, usando um loop tradicional.* https://en.wikipedia.org/wiki/Tail_call [Recursão de cauda] é uma técnica em que o compilador pode reescrever um método recursivo de maneira imperativa *, assumindo que certas regras sejam cumpridas.

*2. Regras para recursão da cauda no Kotlin *

Para implementar uma função no Kotlin usando a recursão de cauda, ​​existe uma regra a seguir:* a chamada recursiva deve ser a última chamada do método *. Esta regra não é tão simples de seguir quanto parece. Por exemplo, tomando o exemplo fatorial, isso seria implementado como:

Esta regra não é tão simples de seguir quanto parece. Por exemplo, tomando o exemplo fatorial, isso seria implementado como:

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

Isso funciona perfeitamente bem. No entanto, não é elegível para recursão da cauda.

Dividida, a função acima faz o seguinte:

  1. Se n for ⇐ 1, retorne n

  2. Calcular _accum = recursiveFactorial (n - 1) _

  3. retornar n* acumular

Escrito assim, você pode ver que a chamada recursiva não é a última na função.

*3. Implementando o fatorial como recursão da cauda *

Em vez disso, para implementar uma função fatorial usando recursão de cauda, ​​precisamos retrabalhá-la para alterar onde o cálculo é realizado.* Precisamos garantir que a multiplicação seja feita antes da chamada recursiva, não depois *. Isso é mais fácil, passando o resultado parcial como parâmetro:

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

Isso pode ser dividido no seguinte:

  1. Calcular soFar = n* acumular

  2. Se n ⇐ 1, retorne soFar

  3. Chame a função factorial, passando n - 1 e soFar

Desta vez, a chamada recursiva é a última etapa do nosso processo.

Uma vez que tenhamos uma função recursiva que esteja na forma correta, instruímos o Kotlin a considerá-la para recursão de cauda usando a palavra-chave tailrec . Isso informa ao compilador que é permitido reescrever a função como um loop, se possível. Esta palavra-chave se aplica à própria função, e se torna:

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

*4. Saída de compilação do fatorial *

O objetivo disso é escrever código recursivo que é executado de maneira imperativa, para evitar problemas de estouro de pilha. Se descompilarmos a função acima, podemos ver que o resultado produzido pelo compilador é realmente imperativo:

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;
}

Podemos ver imediatamente como isso funciona e observar que não há absolutamente nenhum risco de um estouro de pilha nesse código.

===* 5. Melhorias de desempenho*

Ocasionalmente, podemos ver melhorias de desempenho usando essa otimização, além de ganhos de segurança. Esses benefícios dependem de alguns outros fatores - como a profundidade da recursão e a complexidade dos cálculos.

As melhorias vêm do fato de que as chamadas de método são mais caras do que simplesmente fazer loop.

Usando nosso exemplo fatorial novamente, podemos medir quanto tempo leva para executar e comparar:

  • Calcular _fatorial (50) _ 1.000.000 vezes sem recursão da cauda leva ~ 70ms *Calcular _fatorial (50) _ 1.000.000 vezes com recursão da cauda leva ~ 45ms

Usando o ingênuo benchmark, obtivemos uma aceleração de 36%, o que é significativo apenas por permitir que o compilador refaça a execução de nossa implementação.

Observe que essas medidas são de benchmarking muito simples de uma função simples. As mudanças reais de desempenho variam de acordo com as circunstâncias e devem sempre ser medidas antes de tomar qualquer decisão

===* 6. Resumo *

Em alguns cenários, a recursão da cauda pode trazer alguns benefícios importantes ao nosso código - tanto em relação à segurança quanto ao desempenho. O compilador do Kotlin pode fazer isso automaticamente - basta usar a palavra-chave correta.