今回は「純粋関数型の並列処理」の前半を扱います。ここから「Part II 関数型デザインとコンビネータライブラリ」に突入します。
個人的にはこのPart IIから演習問題の難易度が格段に上がった印象でした。また、ここでの演習問題で実装したコードが後の章で使われたりするので、避けるわけにもいきません。それでも類似のライブラリや、ベースとなっているライブラリを触っていくことで次第に理解が深まっていくでしょう。
純粋関数型の並列処理
テストコード
この章の演習問題を解く上で悩ましいのはテストコードです。実際に巨大な(時間のかかる)計算をさせ、「並列だと処理時間が短くなる」を確認するのがベストですが、そこまでやるのも大変です。
一つのアイディアとして計算結果として、スレッドのIDを取得すると別スレッドで実行されたか否かが分かります。例えば、lazyUnit
とunit
は実際に計算のために別スレッドにフォークするか否かが異なるので、計算の中で取得したスレッド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
として独立しました。