web-dev-qa-db-ja.com

そのコードは末尾再帰スタイルではありませんか?

私はScala Beggining Scala by DavidPollackを読んで試してみるのはちょっと初めてです。彼はファイルからすべての文字列をロードする単純な再帰関数を定義しています。

def allStrings(expr: => String): List[String] = expr match {
    case null => Nil
    case w => w :: allStrings(expr)
}

巨大な辞書ファイルを読み込もうとしたときにStackOverflow例外がスローされたことを除けば、エレガントで素晴らしいです。

私が理解している限り、Scalaは末尾再帰をサポートしているので、関数呼び出しがスタックをオーバーフローする可能性はなく、おそらくコンパイラはそれを認識しませんか?それで、グーグルした後、@ tailrecアノテーションを試しましたコンパイラを助けてください、しかしそれは言いました

error: could not optimize @tailrec annotated method: it contains a recursive call not in tail position
def allStrings(expr: => String): List[String] =

末尾再帰が間違っていることを理解していますか?このコードを修正するにはどうすればよいですか?

45
Grozz

Scalaは、最後の呼び出しがメソッド自体の呼び出しである場合にのみ、これを最適化できます。

さて、最後の呼び出しはallStringsではなく、実際には::(短所)メソッド。

この末尾再帰を作成する方法は、次のようなアキュムレータパラメータを追加することです。

def allStrings(expr: => String, acc: List[String] = Nil): List[String] =
  expr match {
    case null => acc
    case w => allStrings(expr, w :: acc)
  }

アキュムレータがAPIにリークするのを防ぐために、末尾再帰メソッドをネストされたメソッドとして定義できます。

def allStrings(expr: => String) = {
  def iter(expr: => String, acc: List[String]): List[String] =
    expr match {
      case null => acc
      case w => iter(expr, w :: acc)
    }
  iter(expr, Nil)
}
64
Ben James

最終的な操作はallStringsの再帰呼び出しではなく、::メソッドの呼び出しであるため、末尾再帰ではありません(また、そうすることはできません)。

これを解決する最も安全な方法は、アキュムレータを使用するネストされたメソッドを使用することです。

def allStrings(expr: => String) = {
  @tailrec
  def inner(expr: => String, acc: List[String]): List[String] = expr match {
    case null => acc
    case w => inner(expr, w :: acc)
  }
  inner(expr, Nil)
}

この特定のケースでは、アキュムレータをallStringsのパラメータに持ち上げ、デフォルト値Nilを指定して、内部メソッドの必要性を回避することもできます。しかし、それが常に可能であるとは限りません。相互運用性が心配な場合は、Javaコードから適切に呼び出すことはできません。

23
Kevin Wright