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
Tweet

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))