¿Qué es la anotación Scala para garantizar que una función recursiva de cola esté optimizada?

98

Creo que hay una @tailrecanotación para garantizar que el compilador optimizará una función recursiva de cola. ¿Lo pones delante de la declaración? ¿También funciona si Scala se usa en modo de scripting (por ejemplo, usando :load <file>bajo REPL)?

huynhjl
fuente

Respuestas:

119

De la publicación del blog " Llamadas de cola, @tailrec y trampolines ":

  • En Scala 2.8, también podrá utilizar la nueva @tailrecanotación para obtener información sobre qué métodos están optimizados.
    Esta anotación le permite marcar métodos específicos que espera que el compilador optimice.
    A continuación, recibirá una advertencia si el compilador no los optimiza.
  • En Scala 2.7 o versiones anteriores, deberá confiar en las pruebas manuales o en la inspección del código de bytes para determinar si un método se ha optimizado.

Ejemplo:

puede agregar una @tailrecanotación para estar seguro de que los cambios han funcionado.

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

Y funciona desde el REPL (ejemplo de los consejos y trucos de 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
fuente
44

El compilador de Scala optimizará automáticamente cualquier método recursivo de cola. Si anotas un método que crees que es recursivo por cola con la @tailrecanotación, el compilador te advertirá si el método en realidad no es recursivo por cola. Esto hace que la @tailrecanotación sea una buena idea, tanto para garantizar que un método sea optimizable actualmente como para que siga siendo optimizable a medida que se modifica.

Tenga en cuenta que Scala no considera que un método sea recursivo en cola si se puede anular. Por lo tanto, el método debe ser privado, final, en un objeto (a diferencia de una clase o rasgo), o dentro de otro método a optimizar.

Dave Griffith
fuente
8
Supongo que esto es algo así como la overrideanotación en Java: el código funciona sin él, pero si lo pones allí te dice si cometiste un error.
Zoltán
23

La anotación es scala.annotation.tailrec. Desencadena un error del compilador si el método no se puede optimizar para la llamada final, lo que sucede si:

  1. La llamada recursiva no está en la posición final.
  2. El método podría anularse
  3. El método no es definitivo (caso especial del anterior)

Se coloca justo antes de defen una definición de método. Funciona en REPL.

Aquí importamos la anotación e intentamos marcar un método como @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 { 
                    ^

¡Ups! ¡La última invocación es 1.+(), no length()! Reformulemos el método:

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

Tenga en cuenta que length0es automáticamente privado porque está definido en el alcance de otro método.

retrónimo
fuente
2
Ampliando lo que ha dicho anteriormente, Scala solo puede optimizar las llamadas finales para un solo método. Las llamadas recursivas mutuas no se optimizarán.
Rich Dougherty
Odio ser el quisquilloso, pero en su ejemplo, en el caso Nil, debe devolver el recuento para una función de longitud de lista correcta; de lo contrario, siempre obtendrá 0 como valor de retorno cuando finalice la recursividad.
Lucian Enache