Magnolia Tech

いつもコードのことばかり考えている人のために。

『Scala関数型デザイン&プログラミング』の演習問題をScala3で解く その6

blog.magnolia.tech

引き続き気になったこと、調べたこと、忘れないようにメモしたことをまとめていきます。

正格と遅延

標準ライブラリにおける非正格クラス

元々、Scalaの標準コレクションには、Streamという非正格のコレクションクラスが有りましたが、Scala 2.13より非推奨になりました(まだ削除はされていません)。代わりにLazyListというコレクションリストが導入されました。

Scala Standard Library 2.13.8 - scala.collection.immutable.Stream

主な違いはStreamtailのみを非正格として扱うのに対して、LazyListheadも非正格になっています。

演習問題を解く際には、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 ScalaWikiに書かれています(しばらく挙動が意図通りなのか悩んでしまいましたが、ちゃんと書かれていました)。

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から標準コレクション入りしているので、標準コレクションの関数を使って挙動を確認できます。関数のシグニチャも同じですね。

以下のブログの解説がわかりやすいです。

Scala Unfold | Genuine Blog