Upgrade to Pro — share decks privately, control downloads, hide ads and more …

fp in scala section 05

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →

fp in scala section 05

勉強会用なので、理解とか答えが間違っていたりすると思います。

Avatar for nozomitaguchi

nozomitaguchi

July 18, 2019

Other Decks in Programming

Transcript

  1. 第5章 正格と遅延 目次 - 第5章 正格と遅延 - 5.1 正格関数と非正格関数 -

    5.2 遅延リストの例 - 5.2.1 ストリームを記憶し、再計算を回避する - 5.2.2 ストリームを検査するためのヘルパー関数 - 5.3 プログラムの記述と評価の切り分け - 5.4 無限ストリームと余再帰 - 5.5 まとめ
  2. 論理演算は非正格 false && {println("!!"); true} // 何も出力しない // res0: Boolean

    = false true || {println("!!"); false} // 何も出力しない // res0: Boolean = true
  3. scala での非正格関数の記述 def if2[A]( cond: Boolean, onTrue: () => A,

    onFalse: () => A): A = if (cond) onTrue() else onFalse() if2( a < 22, () => println("a"), () => println("b") ) def if2[A]( cond: Boolean, onTrue: => A, onFlse: => A): A = if (cond) onTrue else onFalse if2(false, sys.error("fail"), 3) () => A , => A どちらの書き方も(デフォルトでは)キャッシュしな い。
  4. def maybeTwice(b: Boolean, i: => Int) = if (b) i

    + i else 0 val x = maybeTwice(true, { println("hi"); 1 + 41}) // hi // hi // x: Int = 84 i + i の部分で2回参照されているので、その都度評価される。
  5. def maybeTwice2(b: Boolean, i: => Int) = { lazy val

    i = i if (b) j + j else 0 } val x = maybeTwice2(true, {println("hi"); 1 +41}) // hi // x: Int = 84 lazy val にすると最初に参照された時に、 初めて評価されて結果がキャッシュされる。 Scala の非正格関数は引数を値渡しではなく、名前渡しで受け取る。
  6. リスト 5‑2 trait Stream[+A] 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: _*)) }
  7. 関数の作成例 def headOption: Option[A] = this match { case Empty

    => None case Cons(h, t) => Some(h()) } h() を使ってサンクを強制的に評価。 Cons の末尾である t は評価しない。 これがどう役に立つのか見ていく。
  8. 5.2.1 ストリームを記憶し、再計算を回避する val z = Cons(() => expensive(x), tl) val

    h1 = x.headOption val h2 = x.headOption こう書いてしまうと、 expensive(x) が二回計算されてしまう。 スマートコンストラクタを使うと、それを回避できる。
  9. def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {

    lazy val head = hd lazy val tail = tl Cons(() => head, () => tail) } 評価結果がキャッシュされる Stream[A] を作ることができる。 def empty[A]: Stream[A] = Empty 型情報がある Empty を生成できるので、型推論に関して都合が良い場合 がある。
  10. def toListRecursive: List[A] = this match { case Cons(h,t) =>

    h() :: t().toListRecursive case _ => List() } 末尾再帰 ver 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 }
  11. 最後に reverse するのを避けるために局所的な副作用コーディングをし て高速化 ver (関数に閉じているので、純粋性は保たれている。) def toListFast: List[A] =

    { val buf = new collection.mutable.ListBuffer[A] @annotation.tailrec def go(s: Stream[A]): List[A] = s match { case Cons(h,t) => buf += h() go(t()) case _ => buf.toList } go(this) }
  12. 自分の答え. def take(n: Int): Stream[A] = this match { case

    Cons(h, t) if 0 < n => cons(h(), t().take(n - 1)) case _ => empty } 公式の答え. n == 1 のときは empty を詰めるようにすると t() を評価しないで済むた め?? 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 }
  13. EXERCISE 5.3 Stream の先頭から指定された述語とマッチする要素を全て取り出す takeWhile 関数を記述せよ. def takeWhile(f: A =>

    Boolean): Stream[A] = this match { case Cons(h, t) if f(h()) => cons(h(), t().takeWhile(f)) case _ => empty }
  14. 遅延を利用することで、式の記述をその式の評価から切り離すことが可 能になる。 def exists(p: A => Boolean): Boolean = this

    match { case Cons(h, t) => p(h()) || t().exists(p) case _ => false } すべて走査し終える前に、 p(h()) == true を返す場合、それ以降の Streamの末尾は評価されない。
  15. これを foldRight を使って再実装できる。 def foldRight[B](z: => B)(f: (A, => B)

    => B) = this match { case Cons(h, t) => f(h(), t().foldRight(z)(f)) case _ => z } f に渡される二番目の引数が非正格になっているのがポイント。 def exists(p: A => Boolean): Boolean = foldRight(false)((a, b) => p(a) || b) p(a) が true になった場合、b は評価されないで計算が終了する。
  16. EXERCISE 5.5 foldRight を使って takeWhile を実装せよ. def takeWhile(f: A =>

    Boolean): Stream[A] = foldRight(empty[A])((h, t) => if (f(h)) cons(h, t) else empty)
  17. EXERCISE 5.7 foldRight を使って map, filter, append, flatMap を実装せよ. append

    メソッドはその引数に関して非正格でなければならない.
  18. def map[B](f: A => B): Stream[B] = foldRight(empty[B])((h, t) =>

    cons(f(h), t)) def filter(f: A => Boolean): Stream[A] = foldRight(empty[A])((h, t) => if (f(h)) cons(h, t) else t) def append[B >: A](s: => Stream[B]): Stream[B] = foldRight(s)(cons(_, _)) def flatMap[B](f: A => Stream[B]): Stream[B] = foldRight(empty[B])((h, t) => f(h) append t)
  19. トレースすると・・・ Stream(1,2,3,4).map(_+10).filter(_%2==0).toList // map を1つ目の要素に適用. cons(11, Stream(2,3,4).map(_+10)).filter(_%2==0).toList // filter を1つ目の要素に適用.

    Stream(2,3,4).map(_+10).filter(_%2==0).toList // map を2つ目の要素に適用. cons(12, Stream(3,4).map(_+10)).filter(_%2==0).toList // filter を2つ目の要素に適用. 12 :: Stream(3,4).map(_+10).filter(_%2==0).toList // map を3つ目の要素に適用. 12 :: cons(13, Stream(4).map(_+10)).filter(_%2==0).toList // filter を3つ目の要素に適用. 12 :: Stream(4).map(_+10).filter(_%2==0).toList // map を4つ目の要素に適用. 12 :: cons(14, Stream().map(_+10)).filter(_%2==0).toList // filter を4つ目の要素に適用. 12 :: 14 :: Stream().map(_+10)).filter(_%2==0).toList // 終了. 12 :: 14 :: List()
  20. def find(p: A => Boolean): Option[A] = filter(p).headOption filter はストリーム全体を変換するが、遅延式なので、find

    がマッチする ものを検出した時点で終了する。 中間ストリームが生成されないため、ストリームの変換に対しては、 現在の要素を格納して変換するのに必要な作業メモリだけで十分。
  21. 不要と判断された時点でメモリを開放できるので、 無限ストリームを使った処理を書くことが出来る。 val ones: Stream[Int] = Stream.cons(1, ones) ones.take(5).toList //

    res0: List[Int] = List(1,1,1,1,1) ones.exists(_%2 != 0) // res1: Boolean = true ones.forAll(_==1) など、永遠に終わらない処理や スタックセーフではない式を書いてしまいやすいので注意が必要。 これは、無限ループではなくスタックオーバーフローとして現れる。
  22. EXERCISE 5.8 ones を少し一般化し, 指定された値の無限ストリームを返す constant 関 数を記述せよ. def constant[A](a:

    A): Stream[A] = cons(a, constant(a)) オブジェクトを使い回せる効率が良い ver def constant[A](a: A): Stream[A] = { lazy val tail: Stream[A] = Cons(() => a, () => tail) tail }
  23. EXERCISE 5.9 n で始まって n + 1, n + 2

    と続く整数の無限ストリームを生成する関数を 記述せよ. def from(n: Int): Stream[Int] = cons(n, from(n + 1))
  24. EXERCISE 5.10 フィボナッチ数列(0, 1, 1, 2, 3, 5, 8) の無限ストリームを生成する

    fibs 関 数を記述せよ. 自分の答え. val fibs: Stream[Int] = cons(0, cons(1, fibs.zip(fibs.tail).map(a => a._1 + a._2))) 公式の答え. val fibs: Stream[Int] = { def go(f0: Int, f1: Int): Stream[Int] = cons(f0, go(f1, f0 + f1)) go(0, 1) }
  25. unfold undold 関数は、余再帰(corecursive) と呼ばれる関数の一例。 再帰関数 データを消費する 再帰処理によって入力を小さくしていくことで終了する 余再帰関数 データを生成する "生産性"

    が続く限り終了する必要がない。 余再帰関数は、ガード付き再帰(guarded recursion)とも呼ばれる 生産性は、相互終了(cotermination)とも呼ばれる
  26. EXERCISE 5.12 unfold を使って fibs, from, constant, ones を記述せよ. val

    fibs: Stream[Int] = unfold[Int, (Int, Int)] ((0, 1))(a => Some((a._1, (a._2, a._1 + a._2)))) def from(n: Int): Stream[Int] = unfold(n)(a => Some(a, a + 1)) def constant[A](a: A): Stream[A] = unfold(a)(_ => Some((a, a))) val ones: Stream[Int] = unfold(1)(_ => Some((1, 1)))
  27. EXERCISE 5.13 unfold を使って(第3章で示したような) map, take, takeWhile, zipWith, zipAll を実装せよ.

    zipAll 関数では, どちらかのストリームに要素が残っている限り, 評価を 続ける必要がある. この関数はストリームが完全に評価されたかどうかを示すのに Option を 使用する.
  28. def map[B](f: A => B): Stream[B] = unfold(this) { case

    Empty => None case Cons(x, xs) => Some((f(x()), xs())) } def take(n: Int): Stream[A] = unfold((this, n)) { case (Empty, _) => None case (_, _n) if _n == 0 => None case (Cons(x, xs), _n) => Some((x(), (xs(), _n - 1))) } def takeWhile(f: A => Boolean): Stream[A] = unfold(this) { case Cons(x, xs) if f(x()) => Some(x(), xs()) case _ => None }
  29. def zipWith[B](s:Stream[B])(f:(A,B)=>B): Stream[B] = unfold((this, s)) { case (Cons(x, xs),

    Cons(y, ys)) => Some((f(x(), y()), (xs(), ys()))) case _ => None } def zipAll[B](s:Stream[B]):Stream[(Option[A],Option[B])] = unfold(this, s) { case (Cons(x, xs), Cons(y, ys)) => Some((Some(x()), Some(y())), (xs(), ys())) case (Cons(x, xs), _) => Some((Some(x()), None), (xs(), Empty)) case (_, Cons(y, ys)) => Some((None, Some(y())), (Empty, ys())) case _ => None }
  30. EXERCISE 5.14 難問: ここまで記述してきた関数を使って stratsWith を実装せよ. この関数は, ある Stream が別の

    Stream のプレフィックス(接頭辞)であ るかどうかを調べる. 例えば, Stream(1,2,3) startsWith Stream(1,2) の結果は true になる. 自分の答え. def stratsWith[B >: A](s: Stream[B]): Boolean = { zipAll(s).takeWhile(t => t._2.isDefined) .foldRight(true)((a, acc) => a._1 == a._2 && acc) } 公式の答え. def startsWith[A](s: Stream[A]): Boolean = zipAll(s).takeWhile(!_._2.isEmpty) forAll { case (h,h2) => h == h2 }
  31. EXERCISE 5.15 unfold を使って tails を実装せよ. 与えられた Stream に対し, tails

    は元の Stream から始まる入力シーケン スのサフィックス(接尾辞)である Stream を返す. 例えば Stream(1,2,3) が与えられた場合は, Stream(Stream(1,2,3), Stream(2,3), Stream(3), Stream()) を返す. 自分の答え. def tails: Stream[Stream[A]] = unfold(this) { case Empty => None case s@Cons(_, t) => Some((s, t())) } append Stream(empty) 公式の答え. def tails: Stream[Stream[A]] = unfold(this) { case Empty => None case s => Some((s, s drop 1)) } append Stream(empty)
  32. EXERCISE 5.16 難問: tails を scanRight 関数として一般化せよ. foldRight と同様に, この関数は中間結果をストリームで返す.

    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) と同じ意味になるはずであ る. scanRight 関数では, 中間結果を再利用することで, n 個の要素を持つ Stream の評価にかかる時間が常に n に対して線形になるようにすべきで ある. この実装に unfold を使用することは可能か. その場合はどのようになる か, あるいは, 実装できないのであればそれはなぜか. すでに記述した別の関数を使って実装することは可能か.
  33. unfold を使って、一応実装自体は出来るが・・・ def _scanRight[B](z:=>B)(f:(A,=>B)=>B):Stream[B] = unfold((this, z)) { case (_this@Cons(_,

    t), _z) => Some((_this.foldRight(_z)(f), (t(), _z))) case _ => None } append Stream(z) scanRight 関数では, 中間結果を再利用することで, n 個の要素を持 つ Stream の評価にかかる時間が常に n に対して線形になるように すべきである. この部分が、無理っぽい。 scanRight は、右から左に中間結果を累積していく必要があるが、 unfold は、左から右への処理になるため、中間結果を再利用することが 出来ないため。
  34. 公式の答え. 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 scanRight を使って再実装した tails def tails: Stream[Stream[A]] = scanRight(empty[A])((a, acc) => cons(a, acc))