Scala
遅延評価

遅延評価とは何か

More than 1 year has passed since last update.

前書き

Scalaの遅延評価はStreamで行うことができます。が、遅延評価ってそもそも何だっけ?ということで、遅延評価についてまとめてみました。

遅延評価??となった時にリファレンスとして参考していただけると幸いです。この記事を書くために参考にした文献は記事の最後にまとめてあるので、そちらも参考にしてみてください。記事の中でおかしな点や疑問に感じた点があればコメントをお願いします。

遅延評価とは

遅延評価とは、言葉の通り評価を遅延して行うことを言います。もっとくだけた言い方をすると、値が必要になるまで、値の評価を後回しにするということです。

遅延評価をしないコード

以下のScalaのコードは 遅延評価ではない コードの例です。

NotLaziness.scala
def sum(l: Int, r: Int): Int = l + r

この関数ではInt型の引数を二つ(lとr)を取り、二つを足し算した結果を返します。このどこが遅延評価ではないのでしょうか?このコードは、引数を受け取った段階で、引数の値が評価済みになります。つまり、値が必要となる演算(足し算)が行われる前に、lとrの値が決定済みになっているということです。よって、このコードは遅延評価をしていません。

遅延評価をするコード

遅延評価をするコードは、値が必要になった段階で初めて値が決定する コードです。先ほどの例を遅延評価を行うコードに書き換えてみましょう。

Laziness.scala
def sum(l: () => Int, r: () => Int): Int = l + r

先ほどの例とほとんど変わっていませんが、このコードは遅延評価をしています。つまり、演算(足し算)が行われる時に、lとrの値が初めて決定されるということです。どのようにしてこのコードは遅延評価を実現しているのでしょうか。それは各引数の前に( ) => をつけることで実現しています。この表記は見慣れないかもしれませんが、引数なしの関数を引数に取るということになります。つまり、このコードは関数を引数に取り、演算が行われる時点で関数を実行して必要となる値を得るということをしています。

Scalaでは、引数がない関数の場合、括弧を省略できるので以下のように書くこともできます。以下のような書き方の方がスッキリしていて見やすいですね。

LazinessSimple.scala
def sum(l: => Inte, r: => Int): Int = l + r

遅延評価をすると何が嬉しいのか

ここまで遅延評価とは何か、どうやって遅延評価をするのか、ということについて見てきましたが遅延評価を行なって一体何が嬉しいのでしょうか。今まで見てきた例では遅延評価をしようが、しまいが何も嬉しいことはありませんでした。

ということで、評価を遅延することの有用性を例示したいと思います。以下は3つの引数を取り、一つ目の引数 t が true なら残りの二つの引数を足し算し結果を返す関数になります。この例は、評価を遅延することで余計な演算を行わないようにしています。

mayBeSum.scala
def mayBeSum(t: Boolean, n: => Int, m: => Int): Int =
  if(t) n + m else 0

この例の重要な点は引数の n と m を遅延評価することで、引数 t が false であった場合に n と m を評価しない点にあります。よって、n と m の値を得るためにコストがかかる場合でも必要とされなければ評価はされず、コストがかからなくて済みます。例えば、以下のような例です。

callMayBeSum.scala
// 指定したファイルのサイズを返す関数。ちょっと時間がかかる。
def readFileSize(path: String): Int = { .... }

// isOK が ture なら、appleList.xml と orangeList.xml のファイルサイズの合計を返す。
// isOK が false なら、ファイルサイズの計算は行われず、0が返される。
mayBeSum(isOk, readFileSize("appleList.xml"), readFileSize("orangeList.xml"))

lazyキーワードをつけて、評価の回数を1回にする

lazyキーワードを変数定義の前につけることで、値の評価を一回に限定できます。例えば、次のコードでは遅延評価を行うコードですが、値が必要となるタイミングが2回あるため2回評価されます。

NotLazyKeyWord.scala
def square(n: => Int): Int = n + n

値が複数回評価されることは、無駄なことをしているということです。なぜならば、何回評価を行なっても評価結果は同じだからです。(純粋関数ならば)上記の例では、nは足し算をする時に2回評価され値が決定しますが、一度評価を行い値を得れば再度評価して値を得る必要はないはずです。そこで登場するのがlazyキーワードになります。

早速、lazyキーワードを使って先ほどのコードの、引数の評価回数を一回にしてみましょう。

LazyKeyWord.scala
def square(n: => Int): Int = {
  lazy m = n
  m + m
}

上記のコードでは、nをlazyキーワードがついたmに入れています。その後、mを使用して演算を行なっています。

lazyキーワードをvalとともに記述すると、=の右辺にある評価が最初に使用されるまで遅延されます。そして、一度評価が行われると結果をキャッシュするため、後で使用するときはキャッシュされた値を使用します。こうすることで、評価の回数を1回に限定できます。上記のコードではnの評価はm + mの左辺で初めて行われ、右辺ではキャッシュされた値を使用します。よって、nの評価は1回になります。

参考

Scala Documentation - 具象不変コレクションクラス