Magnolia Tech

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

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

blog.magnolia.tech

引き続き第8章「プロパティベースのテスト」の演習問題を解いていきます。

この章の後半、割と急に難易度が上がりますが、一つ一つ解いていきましょう。

プロパティベースのテスト

本書では全般的に「まずどんなインタフェースとするか?」という問いかけから始まります。そのため、この章も具体的なジェネレータが出てくるまでちょっと理解が難しいところがありますが、一旦読み進んでまた戻る、といった進め方が良いでしょう。

ジェネレータの定義

Aという型の値を生成するジェネレータは以下の通り定義されています。

case class Gen[A](sample: State[RNG, A])

要はState[RNG, A]に対するラッパークラスです。RNGを渡すと、新しいRNGと、A型の値が一つ返ってきます。もともとState[RNG, A]自体が、RNG => (A, RNG)という型のラッパーになっているので、ここでは二重にラップされていますが、インタフェースを設計していった結果、たまたま実装が単なるラッパークラスになっているだけなので、最初から機能に着目しすぎない、という例ですね。

ただ、このままでは階層構造が深すぎます。

以下のように定義しておくことで、階層が減らせます。

opaque type State[S, +A] = S => (A, S)

object State:
  extension [S, A](underlying: State[S,A])
    def run(s: S): (A, S) = underlying(s)

def apply[S, A](f: S => (A, S)): State[S, A] = f
opaque type Gen[A] =  State[RNG, A]

object Gen:
  def apply[A](f: State[RNG, A]): Gen[A] = f

  extension[A] (self: Gen[A])
    def sample(s: RNG) = self.run(s)

Gen[A]のインスタンスに対して、sampleメソッドをRNGを引数に渡して呼び出すことで、新しいシード値と、値が得られます。

test("boolean") {
  val r = RNG.Simple(42)
  val (i, r1) = Gen.boolean.sample(r)
  r shouldNotBe r1
...

以降、8.4.2は並列処理、8.5は高階関数のテストと今後の展望、8.6はジェネレータの法則と続きますが、8.4.2はテストの最小化、8.5はCoGen、8.6はmap関数の共通性といった深い内容が短いページに詰め込まれていて、記載されている内容だけを追いかけていっても理解が全然追いつきません。それぞれ関連するトピックを調べ、行き来しながら進めていくと良いと思います。