O que é a anotação Scala para garantir que uma função recursiva de cauda seja otimizada?

98

Acho que há uma @tailrecanotação para garantir que o compilador otimize uma função recursiva de cauda. Você apenas coloca isso antes da declaração? Também funciona se Scala for usado no modo de script (por exemplo, usando :load <file>em REPL)?

Huynhjl
fonte

Respostas:

119

Da postagem do blog " Tail calls, @tailrec and trampolines ":

  • No Scala 2.8, você também poderá usar a nova @tailrecanotação para obter informações sobre quais métodos são otimizados.
    Essa anotação permite marcar métodos específicos que você espera que o compilador otimize.
    Você receberá um aviso se eles não forem otimizados pelo compilador.
  • No Scala 2.7 ou anterior, você precisará confiar no teste manual ou na inspeção do bytecode para descobrir se um método foi otimizado.

Exemplo:

você pode adicionar uma @tailrecanotação para ter certeza de que suas alterações funcionaram.

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

E funciona a partir do REPL (exemplo das dicas e truques do 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
fonte
44

O compilador Scala otimizará automaticamente qualquer método verdadeiramente recursivo de cauda. Se você anotar um método que acredita ser recursivo no final com a @tailrecanotação, o compilador irá avisá-lo se o método não for realmente recursivo no final. Isso torna a @tailrecanotação uma boa ideia, tanto para garantir que um método seja otimizado no momento quanto para que permaneça otimizável à medida que é modificado.

Observe que Scala não considera um método recursivo na cauda se puder ser sobrescrito. Assim, o método deve ser privado, final, em um objeto (em oposição a uma classe ou característica), ou dentro de outro método a ser otimizado.

Dave Griffith
fonte
8
Suponho que seja como a overrideanotação em Java - o código funciona sem ela, mas se você colocá-lo lá, ele avisará se você cometeu um erro.
Zoltán
23

A anotação é scala.annotation.tailrec. Ele aciona um erro do compilador se o método não puder ser otimizado para chamada final, o que acontece se:

  1. A chamada recursiva não está na posição final
  2. O método pode ser substituído
  3. O método não é final (caso especial do anterior)

Ele é colocado logo antes de defem uma definição de método. Funciona no REPL.

Aqui importamos a anotação e tentamos marcar um 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 { 
                    ^

Ops! A última invocação é 1.+(), não length()! Vamos reformular o 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

Observe que length0é automaticamente privado porque está definido no escopo de outro método.

retrônimo
fonte
2
Expandindo o que você disse acima, o Scala só pode otimizar as chamadas finais para um único método. Chamadas mutuamente recursivas não serão otimizadas.
Rich Dougherty
Eu odeio ser exigente, mas no seu exemplo, no caso de Nil, você deve retornar tally para uma função de comprimento de lista correta, caso contrário, você sempre obterá 0 como um valor de retorno quando a recursão terminar.
Lucian Enache