Contents
- 5.4 Infinite streams and corecursion
import しておけば、定義より手前で使う時に Stream.cons のように書かなくて済む。
package fpinscala.laziness
import Stream._
sealed trait Stream[+A] {
def map[B](f: A => B): Stream[B] = this match {
case Cons(h, t) => cons(f(h()), t().map(f))
case Empty => empty
}
}
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: _*))
}
無限のStream登場
object Stream {
val ones: Stream[Int] = cons(1, ones)
}
scala> ones
res0: fpinscala.laziness.Stream[Int] = Cons(<function0>,<function0>)
scala> ones.take(3)
res1: fpinscala.laziness.Stream[Int] = Cons(<function0>,<function0>)
scala> ones.take(3).toList
res2: List[Int] = List(1, 1, 1)
scala> ones.toList
java.lang.OutOfMemoryError: GC overhead limit exceeded
at scala.collection.immutable.List.$colon$colon(List.scala:112)
at fpinscala.laziness.Stream$class.go$1(stream.scala:14)
at fpinscala.laziness.Stream$class.toList(stream.scala:17)
at fpinscala.laziness.Cons.toList(stream.scala:43)
... 28 elided
無限のStreamの計算でも、次のいずれかにあてはまるなら返ってくる。
- thunk (unevaluated form of an expression)
- 有限個で答えが出る場合
scala> ones.map(_ + 1).exists(_ % 2 == 0)
res0: Boolean = true
scala> ones.takeWhile(_ == 1)
res1: fpinscala.laziness.Stream[Int] = Cons(<function0>,<function0>)
scala> ones.forAll(_ != 1)
res2: Boolean = false
そうでない場合は、
scala> ones.map(_ + 1).exists(_ == 1)
java.lang.StackOverflowError
at scala.runtime.ObjectRef.zero(ObjectRef.java:23)
at fpinscala.laziness.Stream$.cons(stream.scala)
at fpinscala.laziness.Stream$class.map(stream.scala:41)
at fpinscala.laziness.Cons.map(stream.scala:64)
at fpinscala.laziness.Stream$$anonfun$map$2.apply(stream.scala:41)
at fpinscala.laziness.Stream$$anonfun$map$2.apply(stream.scala:41)
at fpinscala.laziness.Stream$.tail$lzycompute$1(stream.scala:69)
at fpinscala.laziness.Stream$.fpinscala$laziness$Stream$$tail$1(stream.scala:69)
at fpinscala.laziness.Stream$$anonfun$cons$2.apply(stream.scala:70)
at fpinscala.laziness.Stream$$anonfun$cons$2.apply(stream.scala:70)
at fpinscala.laziness.Stream$class.exists(stream.scala:53)
at fpinscala.laziness.Cons.exists(stream.scala:64)
...
ones の拡張で、一定値を無限に繰り返す
scala> constant(1).take(3).toList
res0: List[Int] = List(1, 1, 1)
scala> constant("hoge").take(3).toList
res1: List[String] = List(hoge, hoge, hoge)
scala> constant((1,2,3)).take(3).toList
res2: List[(Int, Int, Int)] = List((1,2,3), (1,2,3), (1,2,3))
定義
object Stream {
def constant[A](a: A): Stream[A] = {
cons(a, constant(a))
}
}
nから始まる整数列
scala> from(0).take(5).toList
res0: List[Int] = List(0, 1, 2, 3, 4)
定義
object Stream {
def from(n: Int): Stream[Int] = {
cons(n, from(n+1))
}
}
(おまけ) Pythonだと generator を使うと似たものが書ける。
from itertools import islice
def _from(n):
while True:
yield n
n += 1
print(list(islice(_from(0), 0, 5)))
print(list(islice(map(lambda x: x + 1, _from(0)), 0, 5)))
おなじみのフィボナッチ列
scala> fibs.take(7).toList
res0: List[Int] = List(0, 1, 1, 2, 3, 5, 8)
scala> fibs.take(48).drop(46).toList
res1: List[Int] = List(1836311903, -1323752223)
定義
object Stream {
val fibs: Stream[Int] = {
def go(p: Int, q: Int): Stream[Int] = {
cons(p, go(q, p+q))
}
go(0, 1)
}
}
次のようなシグネチャを持つStream構築関数を考える
def unfold[A, S](z: S)(f: S => Option[(A, S)]): Stream[A]
- z ... 初期状態
- f ... 状態をとり、値と次の状態のペアを返す。
- Option なのは有限打ち切りを表現するため
unfold みたいな関数を corecursive function と呼ぶ。
- cf. recursive function は小さな方に行ってとまる。
- 止まらないことを Stream で表現してる。
- 次の章で出てくる状態の話とも関連: 状態 => (値, 次の状態)
定義 (EXERCISE 5.11)
object Stream {
def unfold[A, S](z: S)(f: S => Option[(A, S)]): Stream[A] = f(z) match {
case Some((a, s)) => cons(a, unfold(s)(f))
case None => empty
}
}
使用例 (EXERCISE 5.12)
文法(タプルの扱い方)がわからなかったので当てずっぽうで書いてみた:
object Stream {
def fibs_unfold: Stream[Int] = {
unfold((0, 1))((p, q) => Some(p, (q, p+q)))
}
}
コンパイルエラー(エラーメッセージが役に立つこともある例!)
% scalac stream.scala
stream.scala:100: error: missing parameter type
Note: The expected type requires a one-argument function accepting a 2-Tuple.
Consider a pattern matching anonymous function, `{ case (p, q) => ... }`
unfold((0, 1))((p, q) => Some(p, (q, p+q)))
^
stream.scala:100: error: missing parameter type
unfold((0, 1))((p, q) => Some(p, (q, p+q)))
^
two errors found
修正版
object Stream {
def fibs_unfold: Stream[Int] = {
unfold((0, 1))({case (p, q) => Some(p, (q, p+q))})
}
}
scala> fibs_unfold.take(7).toList
res0: List[Int] = List(0, 1, 1, 2, 3, 5, 8)
scala> fibs.take(48).drop(46).toList
res1: List[Int] = List(1836311903, -1323752223)
object Stream {
def from_unfold(n: Int): Stream[Int] = {
unfold(n)(n => Some(n, n+1))
}
}
scala> from_unfold(0).take(5).toList
res0: List[Int] = List(0, 1, 2, 3, 4)
object Stream {
def constant_unfold[A](a: A): Stream[A] = {
unfold(a)(a => Some((a, a)))
}
}
scala> constant_unfold(1).take(3).toList
res0: List[Int] = List(1, 1, 1)
ones は上記で出来てしまっているので skip
sealed trait Stream[+A] {
def map_unfold[B](f: A => B): Stream[B] = {
unfold(this)({
case Cons(h, t) => Some((f(h()), t()))
case Empty => None
})
}
}
scala> from(0).map_unfold(_ + 1).take(5).toList
res1: List[Int] = List(1, 2, 3, 4, 5)
sealed trait Stream[+A] {
def take(n: Int): Stream[A] = this match {
case Cons(h, t) if n > 0 => cons(h(), t().take(n-1))
case _ => Empty
}
def take_unfold(n: Int): Stream[A] = {
unfold((n, this))({
case (m, a) => a match {
case Cons(h, t) if m > 0 => Some(h(), (m-1, t()))
case _ => None
}
})
}
}
scala> from_unfold(0).take_unfold(5).toList
res0: List[Int] = List(0, 1, 2, 3, 4)
sealed trait Stream[+A] {
def take(n: Int): Stream[A] = this match {
case Cons(h, t) if n > 0 => cons(h(), t().take(n-1))
case _ => Empty
}
def take_unfold(n: Int): Stream[A] = {
unfold((n, this))({
case (m, a) => a match {
case Cons(h, t) if m > 0 => Some(h(), (m-1, t()))
case _ => None
}
})
}
}
scala> Stream(1,2,3).takeWhile_unfold(_ < 3).toList
res0: List[Int] = List(1, 2)
sealed trait Stream[+A] {
def zipWith_unfold[B,C](b: Stream[B])(f: (A, B) => C): Stream[C] =
unfold((this, b))({
case (Cons(ah, at), Cons(bh, bt)) => Some((f(ah(), bh()), (at(), bt())))
case _ => None
})
}
scala> from(1).zipWith_unfold(from(0))(_ - _).take(3).toList
res1: List[Int] = List(1, 1, 1)
sealed trait Stream[+A] {
def zipAll_unfold[B](s2: Stream[B]): Stream[(Option[A],Option[B])] =
unfold((this, s2))({
case (Cons(ah, at), Cons(bh, bt)) => Some(((Some(ah()), Some(bh())), (at(), bt())))
case (Cons(ah, at), Empty) => Some(((Some(ah()), None), (at(), Empty)))
case (Empty, Cons(bh, bt)) => Some(((None, Some(bh())), (Empty, bt())))
case (Empty, Empty) => None
})
}
scala> Stream(1,2,3).zipAll_unfold(Stream(11,12,13,14)).toList
res0: List[(Option[Int], Option[Int])] = List((Some(1),Some(11)), (Some(2),Some(12)), (Some(3),Some(13)), (None,Some(14)))
Streamは無限にもなり得ることに注意 (take して一致確認というのは駄目)
駄目な例
sealed trait Stream[+A] {
def startsWith[A](s2: Stream[A]): Boolean =
this.zipAll_unfold(s2).forAll({
case (Some(_), None) => true
case (Some(h1), Some(h2)) => h1 == h2
case (None, _) => false // (None, None) が網羅されてないという警告も防ぐ(実際はないけど)
})
}
scala> Stream(1,2,3) startsWith Stream(1,2)
res0: Boolean = true
scala> from(1) startsWith Stream(1,2)
java.lang.StackOverflowError
at scala.runtime.VolatileByteRef.<init>(VolatileByteRef.java:18)
at scala.runtime.VolatileByteRef.create(VolatileByteRef.java:21)
at fpinscala.laziness.Stream$.cons(stream.scala:115)
at fpinscala.laziness.Stream$.from(stream.scala:132)
at fpinscala.laziness.Stream$$anonfun$from$2.apply(stream.scala:132)
at fpinscala.laziness.Stream$$anonfun$from$2.apply(stream.scala:132)
at fpinscala.laziness.Stream$.tail$lzycompute$1(stream.scala:117)
at fpinscala.laziness.Stream$.fpinscala$laziness$Stream$$tail$1(stream.scala:117)
at fpinscala.laziness.Stream$$anonfun$cons$2.apply(stream.scala:118)
at fpinscala.laziness.Stream$$anonfun$cons$2.apply(stream.scala:118)
at fpinscala.laziness.Stream$$anonfun$zipAll_unfold$1.apply(stream.scala:96)
at fpinscala.laziness.Stream$$anonfun$zipAll_unfold$1.apply(stream.scala:94)
at fpinscala.laziness.Stream$.unfold(stream.scala:142)
...
修正版
sealed trait Stream[+A] {
def startsWith[A](s2: Stream[A]): Boolean =
unfold(this.zipAll_unfold(s2).takeWhile(_._2 != None))({
case Cons(h, t) => Some((h()._1 == h()._2, t()))
case _ => None
}).forAll(_ == true)
}
scala> Stream(1,2,3) startsWith Stream(1,2)
res0: Boolean = true
scala> from(1) startsWith Stream(1,2)
res1: Boolean = true
scala> from(1) startsWith ones
res2: Boolean = false
sealed trait Stream[+A] {
def tails: Stream[Stream[A]] =
cons(this, unfold(this)({
case Empty => None
case Cons(_, t) => Some((t(), t()))
}))
}
scala> Stream(1,2,3).tails.map(_.toList).toList
res0: List[List[Int]] = List(List(1, 2, 3), List(2, 3), List(3), List())
sealed trait Stream[+A] {
def hasSubsequence[A](s: Stream[A]): Boolean =
tails exists (_ startsWith s)
}
scala> from(1) hasSubsequence from(2).take(3)
res0: Boolean = true
tails の一般化(?)
scala> Stream(1,2,3).scanRight(0)(_ + _).toList
res0: List[Int] = List(6,5,3,0)
// List(1+2+3+0, 2+3+0, 3+0, 0)
sealed trait Stream[+A] {
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 scanRight[B](z: => B)(f: (A, => B) => B): Stream[B] =
this.tails.map_unfold(_.foldRight(z)(f))
}
畳み込み結果の再利用ができていない。
scala> Stream(1,2,3).scanRight(0)((a, b) => {println("!");a + b}).toList
!
!
!
!
!
!
res0: List[Int] = List(6, 5, 3, 0)
わからないので答えを見ました。
sealed trait Stream[+A] {
def scanRight[B](z: => B)(f: (A, => B) => B): Stream[B] =
foldRight((z, Stream(z)))((a, p0) => {
lazy val p1 = p0
val b2 = f(a, p1._1)
(b2, cons(b2, p1._2))
})._2
}
scala> Stream(1,2,3).scanRight(0)((a, b) => {println("!");a + b}).toList
!
!
!
res0: List[Int] = List(6, 5, 3, 0)
なお、同じことを無限リストに適用するとあふれます。
scala> from(1).scanRight(0)(_ + _)
java.lang.StackOverflowError
at scala.runtime.AbstractFunction0$mcI$sp.<init>(AbstractFunction0.scala:12)
at fpinscala.laziness.Stream$$anonfun$from$1.<init>(stream.scala:155)
at fpinscala.laziness.Stream$.from(stream.scala:155)
at fpinscala.laziness.Stream$$anonfun$from$2.apply(stream.scala:155)
at fpinscala.laziness.Stream$$anonfun$from$2.apply(stream.scala:155)
at fpinscala.laziness.Stream$.tail$lzycompute$1(stream.scala:140)
at fpinscala.laziness.Stream$.fpinscala$laziness$Stream$$tail$1(stream.scala:140)
at fpinscala.laziness.Stream$$anonfun$cons$2.apply(stream.scala:141)
at fpinscala.laziness.Stream$$anonfun$cons$2.apply(stream.scala:141)
at fpinscala.laziness.Stream$$anonfun$foldRight$1.apply(stream.scala:116)
at fpinscala.laziness.Stream$$anonfun$scanRight$2.p1$lzycompute$1(stream.scala:123)
at fpinscala.laziness.Stream$$anonfun$scanRight$2.fpinscala$laziness$Stream$class$$anonfun$$p1$1(stream.scala:123)
at fpinscala.laziness.Stream$$anonfun$scanRight$2$$anonfun$2.apply(stream.scala:124)
at scala.Function0$class.apply$mcI$sp(Function0.scala:40)
at scala.runtime.AbstractFunction0.apply$mcI$sp(AbstractFunction0.scala:12)
...
Q. 無限リストに適用してもちゃんと返ってくる例を作れる?
回答例
scala> from(1).scanRight(empty[Int])(cons(_, _))
res0: fpinscala.laziness.Stream[fpinscala.laziness.Stream[Int]] = Cons(<function0>,<function0>)
scala> from(1).scanRight(empty[Int])(cons(_, _)).map(_.take(3).toList).take(3).toList
res1: List[List[Int]] = List(List(1, 2, 3), List(2, 3, 4), List(3, 4, 5))
- Stream を使うことで、必要になるまで評価を遅延させつつ、リスト的なものを扱える
- 無限要素の Stream も扱える便利な関数たちを学んだ
- 定義に foldRight が使われていることに注意。
- 同じ計算を何度もしないように配慮して書くのはコツが必要(私はまだつかめていません)