Was ist die Scala-Annotation, um sicherzustellen, dass eine rekursive Schwanzfunktion optimiert wird?

98

Ich denke, es gibt @tailrecAnmerkungen, um sicherzustellen, dass der Compiler eine rekursive Endfunktion optimiert. Stellen Sie es einfach vor die Erklärung? Funktioniert es auch, wenn Scala im Skriptmodus verwendet wird (z. B. :load <file>unter REPL)?

huynhjl
quelle

Antworten:

119

Aus dem Blog-Beitrag " Tail Calls, @tailrec und Trampoline ":

  • In Scala 2.8 können Sie auch die neue @tailrecAnmerkung verwenden, um Informationen darüber zu erhalten, welche Methoden optimiert sind.
    Mit dieser Anmerkung können Sie bestimmte Methoden markieren, die der Compiler hoffentlich optimieren wird.
    Sie erhalten dann eine Warnung, wenn sie vom Compiler nicht optimiert wurden.
  • In Scala 2.7 oder früheren Versionen müssen Sie sich auf manuelle Tests oder die Überprüfung des Bytecodes verlassen, um herauszufinden, ob eine Methode optimiert wurde.

Beispiel:

Sie können eine @tailrecAnmerkung hinzufügen , um sicherzugehen, dass Ihre Änderungen funktioniert haben.

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

Und es funktioniert mit der REPL (Beispiel aus den Tipps und Tricks der 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
quelle
44

Der Scala-Compiler optimiert automatisch jede wirklich rekursive Methode. Wenn Sie eine Methode mit @tailrecAnmerkungen versehen, von denen Sie glauben, dass sie mit der Anmerkung endrekursiv ist, werden Sie vom Compiler gewarnt, wenn die Methode tatsächlich nicht rekursiv ist. Dies macht die @tailrecAnnotation zu einer guten Idee, um sicherzustellen, dass eine Methode derzeit optimierbar ist und dass sie beim Ändern optimierbar bleibt.

Beachten Sie, dass Scala eine Methode nicht als schwanzrekursiv betrachtet, wenn sie überschrieben werden kann. Daher muss die Methode entweder privat, endgültig, für ein Objekt (im Gegensatz zu einer Klasse oder einem Merkmal) oder innerhalb einer anderen zu optimierenden Methode sein.

Dave Griffith
quelle
8
Ich nehme an, dies ähnelt der overrideAnnotation in Java - der Code funktioniert ohne ihn, aber wenn Sie ihn dort ablegen, erfahren Sie, ob Sie einen Fehler gemacht haben.
Zoltán
23

Die Anmerkung ist scala.annotation.tailrec. Es löst einen Compilerfehler aus, wenn die Methode nicht optimiert werden kann. Dies geschieht, wenn:

  1. Der rekursive Aufruf befindet sich nicht in der Endposition
  2. Die Methode könnte überschrieben werden
  3. Die Methode ist nicht endgültig (Sonderfall der vorhergehenden)

Es wird defin einer Methodendefinition direkt vor dem platziert . Es funktioniert in der REPL.

Hier importieren wir die Annotation und versuchen, eine Methode als zu markieren @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 { 
                    ^

Hoppla! Der letzte Aufruf ist 1.+()nicht length()! Lassen Sie uns die Methode neu formulieren:

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

Beachten Sie, dass dies length0automatisch privat ist, da es im Bereich einer anderen Methode definiert wird.

Retronym
quelle
2
Scala kann Tail Calls nur für eine einzige Methode optimieren. Gegenseitig rekursive Aufrufe werden nicht optimiert.
Rich Dougherty
Ich hasse es, nicht wählerisch zu sein, aber in Ihrem Beispiel im Fall Nil sollten Sie Tally für eine korrekte Listenlängenfunktion zurückgeben, da Sie sonst nach Abschluss der Rekursion immer 0 als Rückgabewert erhalten.
Lucian Enache