Magnolia Tech

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

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

blog.magnolia.tech

今回は「純粋関数型の並列処理」の前半を扱います。ここから「Part II 関数型デザインとコンビネータライブラリ」に突入します。

個人的にはこのPart IIから演習問題の難易度が格段に上がった印象でした。また、ここでの演習問題で実装したコードが後の章で使われたりするので、避けるわけにもいきません。それでも類似のライブラリや、ベースとなっているライブラリを触っていくことで次第に理解が深まっていくでしょう。

純粋関数型の並列処理

テストコード

この章の演習問題を解く上で悩ましいのはテストコードです。実際に巨大な(時間のかかる)計算をさせ、「並列だと処理時間が短くなる」を確認するのがベストですが、そこまでやるのも大変です。

一つのアイディアとして計算結果として、スレッドのIDを取得すると別スレッドで実行されたか否かが分かります。例えば、lazyUnitunitは実際に計算のために別スレッドにフォークするか否かが異なるので、計算の中で取得したスレッドIDがテストを実行したスレッドIDと比較すると結果が変わってきます。

lazyUnitの挙動を確認する

info(f)は、AirFrame-Loggerで生成されたFutureオブジェクトの内容をダンプしている。テストを実行すると、java.util.concurrent.FutureTaskが生成されていることがわかる。

test("lazyUnit") {
  val es = Executors.newFixedThreadPool(4)
  val p = Par.lazyUnit({ (Thread.currentThread.getId, 42) })

  // 新しいスレッドが起動され、異なるスレッドの中で計算が実行される
  val f = Par.run(es)(p)
  info(f)
  val r = f.get
  r._1 shouldNotBe Thread.currentThread.getId
  r._2 shouldBe 42
}

unitの挙動を確認する

こちらでは、生成されたFutureオブジェクトがUnitFutureであることがわかる。

test("unit") {
  val es = Executors.newFixedThreadPool(4)
  val p = Par.unit({ (Thread.currentThread.getId, 42) })

  // 新しいスレッドは結局起動されず、同じスレッドの中で計算が実行される
  val f = Par.run(es)(p)
  info(f)
  val r = f.get
  r._1 shouldBe Thread.currentThread.getId
  r._2 shouldBe 42
}

その先の演習問題で出てくるsequenceも、すべての計算が別スレッドで実行されていることを以下のテストコードで確認できます。

sequenceの挙動を確認する

test("sequence") {
  val es = Executors.newFixedThreadPool(4) // 指定するスレッド数が、リストの数より大きくする(再利用されないため)
  val l = List( Par.lazyUnit( { Thread.currentThread.getId } ), Par.lazyUnit( { Thread.currentThread.getId } ), Par.la
zyUnit( { Thread.currentThread.getId } ) )
  val p = Par.sequence(l)

  val f = Par.run(es)(p)
  info(f)
  val r = f.get

  // すべて異なるスレッドで実行されている
  r(0) shouldNotBe r(1)
  r(1) shouldNotBe r(2)
  r(2) shouldNotBe r(0)
}

Parの構造について

そもそもこの章、初見だと少し意図が分かりづらいというか、意図した構造がよく理解できませんでしたが、先ほどのテストコードを書き、生成されたFutureの内容を見比べてみてようやく同じFutureというインタフェースであえて別スレッドで実行しないunitと、FutureTaskを使って別スレッドで実行するlazyUnitを提供している、ということが理解できました。やはり、コードを書いて、実行してみないとわからないですね(実際には本書にそう書かれているんですが...)。

map2とmap、unit...そして色々な法則

map2と、unitを使ってmapを作り出すパターンが出てきました。繰り返し、プリミティブなコンビネータの組み合わせが出てくるので、パターンを覚えておきましょう。

また、法則に関する説明が延々と続く箇所がありますが、まさにこの本の一番の肝とも言える部分であり、特に「コードに関する法則と証明はなぜ重要か」について、よく読んでおくと良いでしょう。

scala-collection-parallel

Scala 2.13以降では標準コレクションクラスから並列処理に関するクラスが分離され、scala-parallel-collectionsとして独立しました。

GitHub - scala/scala-parallel-collections: Parallel collections standard library module for Scala 2.13+