Kotlin et queue récursion

1. Introduction

Certains algorithmes fonctionnent mieux lorsqu’ils sont implémentés de manière récursive, où un calcul est basé sur une forme plus simple du même calcul.

Dans la plupart des langages de programmation, il existe un risque de débordement de pile associé à la récursivité. Le nombre d’appels de méthode imbriqués pouvant être effectués en une seule fois est limité, sans renvoyer.

Si cela pose un problème, l’algorithme peut être réécrit de manière impérative, en utilisant plutôt une boucle traditionnelle.

  • Récursion de la queue est une technique dans laquelle le compilateur peut réécrire une méthode récursive de manière impérative ** , en supposant que certaines règles soient respectées.

2. Règles pour la récursion de la queue à Kotlin

Pour implémenter une fonction dans Kotlin en utilisant la récursion avec la queue, il faut suivre une règle: l’appel récursif doit être le tout dernier appel de la méthode . Cette règle n’est pas aussi simple à suivre qu’il n’y paraît. Par exemple, en prenant l’exemple Factorial, ceci serait mis en œuvre comme suit:

Cette règle n’est pas aussi simple à suivre qu’il n’y paraît. Par exemple, en prenant l’exemple Factorial, ceci serait mis en œuvre comme suit:

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

Cela fonctionne parfaitement bien. Cependant, il n’est pas éligible pour la récursion de la queue.

Décomposée, la fonction ci-dessus fait ce qui suit:

  1. Si n est ⇐ 1 alors retourne n

  2. Calculer accum = recursiveFactorial (n - 1)

  3. retourne n ** accum

Ecrit comme ça, vous pouvez voir que l’appel récursif n’est pas le dernier appel de la fonction.

3. Mise en œuvre du factoriel en tant que récursion de queue

Au lieu de cela, pour implémenter une fonction factorielle en utilisant la récursion de la queue, nous devons la retravailler pour changer l’endroit où le calcul est effectué. Nous devons nous assurer que la multiplication est faite avant l’appel récursif, pas après . Ceci est plus facile en passant le résultat partiel en tant que paramètre:

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

Cela peut être décomposé comme suit:

  1. Calculer soFar = n ** accum

  2. Si n ⇐ 1, retournez soFar

  3. Appelez la fonction factorial en passant n - 1 et soFar

Cette fois, l’appel récursif est la dernière étape de notre processus.

Une fois que nous avons une fonction récursive sous la forme correcte , nous demandons à Kotlin de la prendre en compte pour la récursion finale en utilisant le mot clé tailrec . Ceci informe le compilateur qu’il est autorisé à réécrire la fonction sous forme de boucle s’il le peut. Ce mot clé s’applique à la fonction elle-même, il devient alors:

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

4. Compilation Sortie de Factorial

L’objectif est d’écrire du code récursif qui s’exécute de manière impérative, afin d’éviter les problèmes de débordement de pile. Si nous décompilons la fonction ci-dessus, nous pouvons voir que le résultat produit par le compilateur est bien impératif:

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

Nous pouvons immédiatement voir comment cela fonctionne et constater qu’il n’ya absolument aucun risque de débordement de pile dans ce code.

5. Amélioration des performances

De temps à autre, nous pouvons constater des améliorations de performances grâce à cette optimisation, ainsi que des gains de sécurité. Ces avantages dépendent d’autres facteurs, tels que la profondeur de la récursion et la complexité des calculs.

  • Les améliorations proviennent du fait que les appels de méthode sont plus coûteux que la simple mise en boucle. **

En utilisant à nouveau notre exemple factoriel, nous pouvons mesurer le temps nécessaire pour exécuter et comparer:

  • Calcul de factorial (50) 1 000 000 fois sans récursion de la queue

prend ~ 70ms ** Le calcul de factorial (50) 1 000 000 de fois avec récursivité prend

~ 45ms

En utilisant le critère naïf, nous avons obtenu une accélération de 36%, ce qui est significatif uniquement pour permettre au compilateur de retravailler notre implémentation.

Notez que ces mesures proviennent d’une analyse très simple d’une fonction simple. Les changements de performances réels varient en fonction des circonstances et doivent toujours être mesurés avant toute décision.

6. Résumé

Dans certains scénarios, la récursion des queues peut apporter des avantages importants à notre code, en termes de sécurité et de performances. Le compilateur de Kotlin peut le faire automatiquement - il suffit d’utiliser le mot-clé approprié.