引き続き気になったこと、調べたこと、忘れないようにメモしたことをまとめていきます。
正格と遅延
標準ライブラリにおける非正格クラス
元々、Scalaの標準コレクションには、Stream
という非正格のコレクションクラスが有りましたが、Scala 2.13より非推奨になりました(まだ削除はされていません)。代わりにLazyList
というコレクションリストが導入されました。
Scala Standard Library 2.13.8 - scala.collection.immutable.Stream
主な違いはStream
がtail
のみを非正格として扱うのに対して、LazyList
はhead
も非正格になっています。
演習問題を解く際には、LazyList
の挙動を参考にするとよいでしょう。
Scala Standard Library 2.13.8 - scala.collection.immutable.LazyList
call-by-value, call-by-name, call-by-need
Scalaの解説を見れば、call-by-valueと、call-by-nameについては詳しく解説されていますが、評価戦略としてもう一つのcall-by-needについては、非正格なライブラリを作る上で必要になってくる考え方です。以下のWikipediaのエントリが参考になるでしょう。
Evaluation strategy - Wikipedia
Stream.applyの利用上の注意
本文中で解説されているStream.apply
の評価方法については注意する必要があります。詳しくはFP in ScalaのWikiに書かれています(しばらく挙動が意図通りなのか悩んでしまいましたが、ちゃんと書かれていました)。
Chapter 5: Strictness and laziness · fpinscala/fpinscala Wiki · GitHub
ヘルパー関数の用意
Streamの要素がどのタイミングで評価されたか確認するために以下のようなコードがサンプルにも出てきます。
lazy val a = { println("42"); 42}
ただ、これを要素数分書いていると疲れてくるのと、単にprintln
を呼び出しただけでは、出力に埋もれがちです。
そこでAirframe-Loggerを使ったユーティリティ関数をテストコードの冒頭で作っておくと便利です。
import wvlet.log.Logger def[A] v(v: A): A = info(v) v
こんな感じで定義します。
val s = Stream.cons(v(1), Stream.cons(v(2), Stream.cons(v(3), empty)))
遅延評価なので、上記の定義の段階では何もログには出力されませんし、take
も要素の中身には関知しません。しかし、最後のtoList
の呼び出しにより、以下のように実際に評価を行う関数を通すと...
val l = s.take(2) info("start evaluation") l.toList shouldBe List(1, 2) info("end evaluation")
このようにわかりやすくログにでてくれます。
2022-01-30 20:07:48.780+0900 info [StreamSpec] start evaluation - (StreamSpec.scala:32) 2022-01-30 20:07:48.780+0900 info [StreamSpec] 1 - (StreamSpec.scala:11) 2022-01-30 20:07:48.781+0900 info [StreamSpec] 2 - (StreamSpec.scala:11) 2022-01-30 20:07:48.781+0900 info [StreamSpec] end evaluation - (StreamSpec.scala:34)
便利ですね。大きな演算や、外部リソースを参照するような実際に時間がかかる計算を用意してもいいのですが、あくまで評価されるタイミングが知りたいだけなので、これで十分です。
unfold関数
後半出てくるunfold
関数ですが、条件が成立する間、要素を生成していく、という面白い発想の関数です。無限に要素を生成する、なってこともできます。
unfold
関数、Scala 2.13から標準コレクション入りしているので、標準コレクションの関数を使って挙動を確認できます。関数のシグニチャも同じですね。
以下のブログの解説がわかりやすいです。