Quelle est l'annotation Scala pour garantir qu'une fonction récursive de queue est optimisée?

98

Je pense qu'il y a une @tailrecannotation pour garantir que le compilateur optimisera une fonction récursive de queue. Le mettez-vous juste devant la déclaration? Cela fonctionne-t-il également si Scala est utilisé en mode script (par exemple en utilisant :load <file>sous REPL)?

huynhjl
la source

Réponses:

119

Extrait du billet de blog « Appels de queue, @tailrec et trampolines »:

  • Dans Scala 2.8, vous pourrez également utiliser la nouvelle @tailrecannotation pour obtenir des informations sur les méthodes optimisées.
    Cette annotation vous permet de marquer des méthodes spécifiques que vous espérez que le compilateur optimisera.
    Vous recevrez alors un avertissement s'ils ne sont pas optimisés par le compilateur.
  • Dans Scala 2.7 ou version antérieure, vous devrez vous fier à des tests manuels ou à une inspection du bytecode pour déterminer si une méthode a été optimisée.

Exemple:

vous pouvez ajouter une @tailrecannotation pour être sûr que vos modifications ont fonctionné.

import scala.annotation.tailrec

class Factorial2 {
  def factorial(n: Int): Int = {
    @tailrec def factorialAcc(acc: Int, n: Int): Int = {
      if (n <= 1) acc
      else factorialAcc(n * acc, n - 1)
    }
    factorialAcc(1, n)
  }
}

Et cela fonctionne à partir du REPL (exemple des trucs et astuces Scala REPL ):

C:\Prog\Scala\tests>scala
Welcome to Scala version 2.8.0.RC5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_18).
Type in expressions to have them evaluated.
Type :help for more information.

scala> import scala.annotation.tailrec
import scala.annotation.tailrec

scala> class Tails {
     | @tailrec def boom(x: Int): Int = {
     | if (x == 0) throw new Exception("boom!")
     | else boom(x-1)+ 1
     | }
     | @tailrec def bang(x: Int): Int = {
     | if (x == 0) throw new Exception("bang!")
     | else bang(x-1)
     | }
     | }
<console>:9: error: could not optimize @tailrec annotated method: it contains a recursive call not in tail position
       @tailrec def boom(x: Int): Int = {
                    ^
<console>:13: error: could not optimize @tailrec annotated method: it is neither private nor final so can be overridden
       @tailrec def bang(x: Int): Int = {
                    ^
VonC
la source
44

Le compilateur Scala optimisera automatiquement toute méthode vraiment récursive de queue. Si vous annotez une méthode que vous pensez être récursive avec l' @tailrecannotation, le compilateur vous avertira si la méthode n'est pas réellement récursive. Cela fait de l' @tailrecannotation une bonne idée, à la fois pour s'assurer qu'une méthode est actuellement optimisable et qu'elle reste optimisable lorsqu'elle est modifiée.

Notez que Scala ne considère pas une méthode comme étant récursive à la fin si elle peut être remplacée. Ainsi la méthode doit être soit privée, finale, sur un objet (par opposition à une classe ou un trait), soit à l'intérieur d'une autre méthode à optimiser.

Dave Griffith
la source
8
Je suppose que c'est un peu comme l' overrideannotation en Java - le code fonctionne sans elle, mais si vous la mettez là, elle vous indique si vous avez fait une erreur.
Zoltán
23

L'annotation est scala.annotation.tailrec. Il déclenche une erreur du compilateur si la méthode ne peut pas être optimisée pour l'appel de fin, ce qui se produit si:

  1. L'appel récursif n'est pas en position de queue
  2. La méthode pourrait être remplacée
  3. La méthode n'est pas définitive (cas particulier de la précédente)

Il est placé juste avant le defdans une définition de méthode. Cela fonctionne dans le REPL.

Ici, nous importons l'annotation et essayons de marquer une méthode comme @tailrec.

scala> import annotation.tailrec
import annotation.tailrec

scala> @tailrec def length(as: List[_]): Int = as match {  
     |   case Nil => 0
     |   case head :: tail => 1 + length(tail)
     | }
<console>:7: error: could not optimize @tailrec annotated method: it contains a recursive call not in tail position
       @tailrec def length(as: List[_]): Int = as match { 
                    ^

Oups! La dernière invocation est 1.+(), non length()! Reformulons la méthode:

scala> def length(as: List[_]): Int = {                                
     |   @tailrec def length0(as: List[_], tally: Int = 0): Int = as match {
     |     case Nil          => tally                                       
     |     case head :: tail => length0(tail, tally + 1)                    
     |   }                                                                  
     |   length0(as)
     | }
length: (as: List[_])Int

Notez que length0c'est automatiquement privé car il est défini dans le cadre d'une autre méthode.

rétronyme
la source
2
En développant ce que vous avez dit ci-dessus, Scala ne peut optimiser les appels de queue que pour une seule méthode. Les appels mutuellement récursifs ne seront pas optimisés.
Rich Dougherty
Je déteste être le plus pointilleux, mais dans votre exemple dans le cas Nil, vous devriez renvoyer le tally pour une fonction de longueur de liste correcte, sinon vous obtiendrez toujours 0 comme valeur de retour lorsque la récursivité se termine.
Lucian Enache