Qual è l'annotazione Scala per garantire che una funzione ricorsiva di coda sia ottimizzata?


98

Penso che ci sia @tailrecun'annotazione per garantire che il compilatore ottimizzi una funzione ricorsiva di coda. Lo metti solo davanti alla dichiarazione? Funziona anche se Scala viene utilizzato in modalità di scripting (ad esempio :load <file>in REPL)?

Risposte:


119

Dal post del blog " Tail calls, @tailrec and trampolines ":

  • In Scala 2.8, sarai anche in grado di utilizzare la nuova @tailrecannotazione per ottenere informazioni su quali metodi sono ottimizzati.
    Questa annotazione ti consente di contrassegnare metodi specifici che speri che il compilatore ottimizzi.
    Riceverai quindi un avviso se non sono ottimizzati dal compilatore.
  • In Scala 2.7 o versioni precedenti, dovrai fare affidamento su test manuali, o ispezioni del bytecode, per capire se un metodo è stato ottimizzato.

Esempio:

potresti aggiungere @tailrecun'annotazione in modo da essere sicuro che le tue modifiche hanno funzionato.

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 funziona dal REPL (esempio dai suggerimenti e trucchi 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 = {
                    ^

44

Il compilatore Scala ottimizzerà automaticamente qualsiasi metodo veramente ricorsivo di coda. Se annoti un metodo che ritieni sia ricorsivo in coda con l' @tailrecannotazione, il compilatore ti avviserà se il metodo non è effettivamente ricorsivo in coda. Questo rende il file@tailrec annotazione una buona idea, sia per garantire che un metodo sia attualmente ottimizzabile sia per mantenerlo ottimizzabile man mano che viene modificato.

Nota che Scala non considera un metodo ricorsivo in coda se può essere sovrascritto. Pertanto il metodo deve essere privato, finale, su un oggetto (al contrario di una classe o di un tratto) o all'interno di un altro metodo per essere ottimizzato.


8
Suppongo che questo sia un po 'come l' overrideannotazione in Java: il codice funziona senza di essa, ma se lo metti lì ti dice se hai fatto un errore.
Zoltán

23

L'annotazione è scala.annotation.tailrec. Attiva un errore del compilatore se il metodo non può essere ottimizzato per la chiamata di coda, cosa che accade se:

  1. La chiamata ricorsiva non è nella posizione di coda
  2. Il metodo potrebbe essere ignorato
  3. Il metodo non è definitivo (caso speciale del precedente)

Si trova appena prima di defin una definizione di metodo. Funziona in REPL.

Qui importiamo l'annotazione e proviamo a contrassegnare un metodo come @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! L'ultima invocazione è 1.+(), no length()! Riformuliamo il metodo:

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

Notare che length0è automaticamente privato perché è definito nell'ambito di un altro metodo.


2
Espandendo ciò che hai detto sopra, Scala può ottimizzare solo le chiamate di coda per un singolo metodo. Le chiamate reciprocamente ricorsive non saranno ottimizzate.
Rich Dougherty

Odio essere pignolo, ma nel tuo esempio nel caso Nil dovresti restituire il conteggio per una funzione di lunghezza dell'elenco corretta, altrimenti otterrai sempre 0 come valore di ritorno al termine della ricorsione.
Lucian Enache
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.