技術と日常。

日々の気が付いたこと・気になったことを残しておきます。

[Scala]無限ループより再帰を使うのがScalaらしいコードらしい

Scalaで無限ループを書くとエラー

投資商品等で、スタートの金額、利率、目標金額を与えて、何年後に到達するかを計算する関数をループで書きたいとします
※本当は対数計算のほうが筋が良いと思います

Javaの感覚で以下のように書くと、コンパイル時にエラーが出ます

def calc(start: Int, rate: Double, goal: Int): Int = {
  var years = 0
  var current: Double = start
  while(true) {
    if (current >= goal) {
      return years
    }
    years += 1
    current = current * rate
  }
}

>
Found:    Unit
Required: Int
  while(true) {

これは、Intを返さないといけないところ、scalaは最後の文を自動的に戻り値として認識するゆえに、最後の文であるwhileが値を返さないためUnitと認識されてしまっているために起こるエラーのようです

無限ループを成立させるには

到達することはないものの、最後に例外を投げてあげると成立します。

def calc(start: Int, rate: Double, goal: Int): Int = {
  var years = 0
  var current: Double = start
  while(true) {
    if (current >= goal) {
      return years
    }
    years += 1
    current = current * rate
  }

  // この文を追加するとコンパイルが通る
  throw new RuntimeException("ここには来ない");
}

積極的に再帰を使う

本題なのですが、他言語を普段書かれている方だと、再帰はStackOverflowを起こす可能性があることから、普段は再帰よりもループを選ばれると思います
ところが、Scalaでは、 末尾再帰の関数であれば、コンパイル時にループに書き換え、最適化をしてくれます
末尾再帰とは、 自身のシンプルな再帰呼び出しが、最後のステップになっているパターン のことです

例えば、これは末尾再帰です

def foo(): Int = {
  foo()
}

これは、末尾再帰ではありません
自身の呼び出しに+ 1と、余計なことをしてしまっているためです

def bar(): Int = {
  bar() + 1
}

そして、@tailrecアノテーションを付与することで、末尾再帰ではない、最適化がかからない関数に関しては、エラーを発生させることが可能です
まとめると、今回のケースだと、以下のように書き換えが可能です

import scala.annotation.tailrec

@tailrec
def calc(current: Double, rate: Double, goal: Int, years: Int): Int = {
  if (current >= goal) {
    years
  } else {
    calc(current * rate, rate, goal, years + 1)
  }
}

参考

Scala while(true) type mismatch? Infinite loop in scala? - Stack Overflow

末尾再帰 - Wikipedia

Scalaで再帰関数を使う際に覚えておくと役に立つこと - Qiita