Apa itu anotasi Scala untuk memastikan fungsi rekursif ekor dioptimalkan?

98

Saya rasa ada @tailrecpenjelasan untuk memastikan kompilator akan mengoptimalkan fungsi rekursif ekor. Apakah Anda hanya meletakkannya di depan deklarasi? Apakah itu juga berfungsi jika Scala digunakan dalam mode skrip (misalnya menggunakan di :load <file>bawah REPL)?

huynhjl
sumber

Jawaban:

119

Dari entri blog " Panggilan ekor, @tailrec, dan trampolin ":

  • Di Scala 2.8, Anda juga dapat menggunakan @tailrecanotasi baru untuk mendapatkan informasi tentang metode mana yang dioptimalkan.
    Anotasi ini memungkinkan Anda menandai metode tertentu yang Anda harap akan dioptimalkan oleh compiler.
    Anda kemudian akan mendapatkan peringatan jika tidak dioptimalkan oleh kompiler.
  • Di Scala 2.7 atau sebelumnya, Anda perlu mengandalkan pengujian manual, atau pemeriksaan bytecode, untuk mengetahui apakah suatu metode telah dioptimalkan.

Contoh:

Anda dapat menambahkan @tailrecanotasi sehingga Anda dapat yakin bahwa perubahan Anda telah berhasil.

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

Dan itu bekerja dari REPL (contoh dari tip dan trik 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
sumber
44

Compiler Scala akan secara otomatis mengoptimalkan metode tail-recursive apa pun. Jika Anda memberi anotasi pada metode yang Anda yakini sebagai rekursif-ekor dengan @tailrecanotasi tersebut, maka kompilator akan memperingatkan Anda jika metode tersebut sebenarnya bukan rekursif-ekor. Hal ini membuat @tailrecanotasi menjadi ide yang bagus, baik untuk memastikan bahwa metode saat ini dapat dioptimalkan dan tetap dapat dioptimalkan saat dimodifikasi.

Perhatikan bahwa Scala tidak menganggap metode sebagai tail-recursive jika dapat diganti. Jadi metode tersebut harus pribadi, final, pada objek (sebagai lawan kelas atau sifat), atau di dalam metode lain untuk dioptimalkan.

Dave Griffith
sumber
8
Saya kira ini seperti overrideanotasi di Java - kodenya bekerja tanpa itu, tetapi jika Anda meletakkannya di sana, ia memberi tahu Anda jika Anda membuat kesalahan.
Zoltán
23

Anotasinya adalah scala.annotation.tailrec. Ini memicu kesalahan kompiler jika metode tidak dapat dioptimalkan panggilan ekor, yang terjadi jika:

  1. Panggilan rekursif tidak berada di posisi ekor
  2. Metode tersebut dapat diganti
  3. Metode ini belum final (kasus khusus sebelumnya)

Ini ditempatkan tepat sebelum defdefinisi metode. Ia bekerja di REPL.

Di sini kami mengimpor anotasi, dan mencoba menandai metode sebagai @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! Doa terakhir adalah 1.+(), bukan length()! Mari kita rumuskan kembali metodenya:

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

Perhatikan bahwa length0secara otomatis bersifat pribadi karena ditentukan dalam cakupan metode lain.

retronim
sumber
2
Memperluas apa yang Anda katakan di atas, Scala hanya dapat mengoptimalkan panggilan ekor untuk satu metode. Panggilan yang saling rekursif tidak akan dioptimalkan.
Rich Dougherty
Saya benci menjadi orang yang pilih-pilih, tetapi dalam contoh Anda dalam kasus Nil, Anda harus mengembalikan penghitungan untuk fungsi panjang daftar yang benar jika tidak, Anda akan selalu mendapatkan 0 sebagai nilai pengembalian ketika rekursi selesai.
Lucian Enache