Що таке анотація Scala для забезпечення оптимізації рекурсивної функції хвоста?


98

Я думаю, є @tailrecанотація, яка гарантує, що компілятор оптимізує рекурсивну функцію хвоста. Ви просто ставите це перед декларацією? Чи працює це також, якщо Scala використовується в режимі сценаріїв (наприклад, за допомогою :load <file>REPL)?

Відповіді:


119

З допису блогу " Хвіст дзвінки, @tailrec і батути ":

  • У Scala 2.8 ви також зможете використовувати нову @tailrecанотацію, щоб отримати інформацію про оптимізовані методи.
    Ця анотація дозволяє позначити конкретні методи, які, як ви сподіваєтесь, оптимізує компілятор.
    Потім ви отримаєте попередження, якщо компілятор не оптимізує їх.
  • У Scala 2.7 або попередній версії вам доведеться покладатися на ручне тестування або перевірку байт-коду, щоб з’ясувати, чи оптимізовано метод.

Приклад:

ви можете додати @tailrecанотацію, щоб бути впевненим, що ваші зміни спрацювали.

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

І це працює з REPL (приклад із підказок та підказок 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

Компілятор Scala автоматично оптимізує будь-який справді рекурсивний метод. Якщо ви анотуєте метод, який, на вашу думку, є хвостово-рекурсивним @tailrec, то компілятор попередить вас, якщо метод насправді не є рекурсивним. Це робить @tailrecанотацію гарною ідеєю як для того, щоб гарантувати, що метод в даний час оптимізується, так і що він залишається оптимізованим у міру модифікації.

Зауважте, що Scala не вважає метод рекурсивним, якщо його можна замінити. Таким чином, метод повинен бути приватним, остаточним, для об’єкта (на відміну від класу чи ознаки), або всередині іншого методу, який потрібно оптимізувати.


8
Я припускаю, що це overrideщось на зразок анотації в Java - код працює без неї, але якщо ви помістите її туди, він повідомить вам, чи зробили ви помилку.
Золтан

23

Анотація є scala.annotation.tailrec. Це викликає помилку компілятора, якщо метод не може бути оптимізований хвостовим викликом, що трапляється, якщо:

  1. Рекурсивний виклик знаходиться не в хвостовому положенні
  2. Метод може бути замінений
  3. Метод не остаточний (окремий випадок попереднього)

Він розміщується безпосередньо перед defу визначенні методу. Це працює в REPL.

Тут ми імпортуємо анотацію та намагаємося позначити метод як @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 { 
                    ^

На жаль! Останній виклик - 1.+()ні length()! Давайте переформулюємо метод:

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

Зверніть увагу, що length0це автоматично приватне, оскільки це визначено в області іншого методу.


2
Розширюючи те, що ви сказали вище, Scala може оптимізувати хвостові виклики лише для одного методу. Взаємно-рекурсивні дзвінки не будуть оптимізовані.
Rich Dougherty

Я ненавиджу бути прискіпливим, але у вашому прикладі у випадку Nil вам слід повернути підрахунок для правильної функції довжини списку, інакше ви завжди отримаєте 0 як значення повернення, коли закінчиться рекурсія.
Lucian Enache
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.