Scala Lazy!(Scala関数型デザインより)

Scala関数型デザイン&プログラミング」の「第 5 章   正格 と 遅延」

ストリームによる、Lazy処理を行う。

コード

package example.laziness

import Stream._

trait Stream[+A] {
  def toListRecursive: List[A] = this match {
    case Cons(h,t) => h() :: t().toListRecursive
    case _ => List()
  }

  def toList: List[A] = {
    @annotation.tailrec
    def go(s: Stream[A], acc: List[A]): List[A] = s match {
      case Cons(h,t) => go(t(), h() :: acc)
      case _ => acc
    }
    go(this, List()).reverse
  }

  def take(n: Int): Stream[A] = this match {
    case Cons(h, t) if n > 1 => cons(h(), t().take(n - 1))
    case Cons(h, _) if n == 1 => cons(h(), empty)
    case _ => empty
  }

  @annotation.tailrec
  final def drop(n: Int): Stream[A] = this match {
    case Cons(_, t) if n > 0 => t().drop(n - 1)
    case _ => this
  }

  // (A, => B) は、(A,B)と同じようではあるが、遅延実行させるため
  // 第2引数を名前渡しで受け取る
  // def foldRight[B](z: => B)(f: (A, B) => B): B = this match {
  def foldRight[B](z: => B)(f: (A, => B) => B): B = this match {
    case Cons(h,t) => f(h(), t().foldRight(z)(f))
    case _ => z
  }

  def exists(p: A => Boolean): Boolean =
    foldRight(false)((a, b) => p(a) || b)

  def forAll(f: A => Boolean): Boolean =
    foldRight(true)((a, b) => f(a) && b)

  def takeWhile(f: A => Boolean): Stream[A] =
    foldRight(empty[A])((h, t) =>
      {
        println(s"h:${h}")
        if (f(h)) cons(h,t)
        else empty
      }
    )

  def headOption: Option[A] =
    foldRight(None: Option[A])((h,_) =>
    {
      println(s"h:${h}")
      Some(h)
    })
}
case object Empty extends Stream[Nothing]
case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A]

object Stream {
  def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {
    lazy val head = hd
    lazy val tail = tl
    Cons(() => head, () => tail)
  }

  def empty[A]: Stream[A] = Empty

  def apply[A](as: A*): Stream[A] =
    if (as.isEmpty) empty
    else cons(as.head, apply(as.tail: _*))

  def main(args: Array[String]): Unit = {
    val sl = Stream(1,2,3,4)
    println(s"sl:${sl}")
    println(s"sl:${sl.toList}")

    val r = sl.foldRight(0)((a,b) => a + b)
    println(s"r:${r}")

    val tw = sl.takeWhile((a) => a < 4)
    // この段階では、先頭だけfetchされる
    println(s"tw:${tw}")
    // toListを呼び出すと全部fetchされる
    println(s"tw:${tw.toList}")

  }

}

Stream 定義

case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A]

object Stream {
  def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {
    lazy val head = hd
    lazy val tail = tl
    Cons(() => head, () => tail)
  }

Streamは、サンク「() => A」で、渡して、すぐに評価させないようにする。
lazyキーワードをつけて、head遅延評価しつつ、キャッシュさせるようにする。

foldRight

  // (A, => B) は、(A,B)と同じようではあるが、遅延実行させるため
  // 第2引数を名前渡しで受け取る
  // def foldRight[B](z: => B)(f: (A, B) => B): B = this match {
  def foldRight[B](z: => B)(f: (A, => B) => B): B = this match {
    case Cons(h,t) => f(h(), t().foldRight(z)(f))
    case _ => z
  }

foldRightのfの定義次第で、遅延実行されるようになる。

    val sl = Stream(1,2,3,4)
    val tw = sl.takeWhile((a) => a < 4)
    // この段階では、先頭だけfetchされる
    println(s"tw:${tw}")
    // toListを呼び出すと全部fetchされる
    println(s"tw:${tw.toList}")

即実行の場合の結果

def foldRightB(f: (A, B) => B): B = this match {

h:4
h:3
h:2
h:1
tw:Cons(example.laziness.Stream$$$Lambda$7/226170135@3b6eb2ec,example.laziness.Stream$$$Lambda$8/381707837@1e643faf)
tw:List(1, 2, 3)

takeWhileが呼ばれた段階で、全データが取られてしまう。

遅延実行の場合の結果

def foldRightB(f: (A, => B) => B): B = this match {

h:1
tw:Cons(example.laziness.Stream$$$Lambda$7/226170135@43a25848,example.laziness.Stream$$$Lambda$8/381707837@3ac3fd8b)
h:2
h:3
h:4
tw:List(1, 2, 3)

必要になった時(toListが呼ばれた時等)に、データを取る。

ORMフレームワークのlazy loadのようだ。

one more thing

  @annotation.tailrec
  final def drop(n: Int): Stream[A] = this match {
    case Cons(_, t) if n > 0 => t().drop(n - 1)
    case _ => this
  }

末尾再帰させるために、finalをつける。 scalaが末尾再帰を最適化するためには、 再帰関数が、オーバーライドされる可能性のないメソッドであること。 つまり、メソッドを定義するクラスが final かメソッド自体がfinal、あるいは private であることなためである。