2015年2月16日月曜日

Scalaz 7.1の非互換項目

プロダクトで使っているScalazをscalaz 7.0.6からScalaz 7.1に上げたわけですが(Operational Monad)、その際に判明したScalaz 7.1の非互換というか仕様変更についてのまとめです。

非互換対応している時に気付いたものが中心なので、仕様変更項目の網羅にはなっていません。(公式仕様変更項目)

ここに出ている項目以外は、スムーズに対応できました。(他の非互換項目にも遭遇していると思いますが、記憶に残らないレベル)

Validation

Scalaz 7.1からValidationがMonadではなくなってしまいました。deprecatedのコメント「flatMap does not accumulate errors, use `scalaz.\/` or `import scalaz.Validation.FlatMap._` instead」からすると、エラーをMonoidとして蓄積する処理がモナド則を破る、ということが判明したのではないかと思われます。

このことでエラー処理戦略のバランスが少し変わってしまいました。

このことを加味した上でのエラー処理戦略に関する最新の状況は以下がよくまとまっていると思います。(この記事は2014年2月のものなので(Scalaz 7.1のリリースは2014年8月)、リリース版に対応した記事ではないと思いますが、ValidationがMonadでないことが前提になっているので、7.1の開発バージョンで仕様を先取りした記事と思われます。)

ValidationをMonadとして使っていたコードに対する当面の回避策としては:

import scalaz.Validation.FlatMap._

として互換機能を有効にすることですが、本格対応としては

"10".parseInt.disjunction

といった感じで「\/」(disjunction)モナドに変換するのがひとつの形です。

Validationは引き続きApplicativeとしては有効なので:

("10".parseInt.toValidationNel |@| "20".parseInt.toValidationNel)(_ + _)

といった形を基本線に、for式で使いたい場合はdisjunctionメソッドで\/モナドに変換して使う戦略となりそうです。

for {
  a <- "10".parseInt.disjunction
  b <- "20".parseInt.disjunction
} yield a + b

ただ、Validationの価値はかなり下がったと思うので、エラー処理戦略は\/モナドを軸に考えて、エラーの積算が必要な処理のみValidationで補完というバランスがよさそうに思います。

具体的には以下のようなイメージです。

for {
  a <- ("10".parseInt.toValidationNel |@| "20".parseInt.toValidationNel)(_ + _).disjunction
+ _).disjunction // エラーの積算が必要な処理をValidation + Applicativeで記述し、Disjunctionに変換
  b <- "30".parseInt.disjunction // for式内は\/モナドで揃える
} yield a + b

Tagged type

以下のページにある通り「Scalaz 7.1 以降は明示的にタグを unwrap しなければいけなくなった」ため、かなり使いにくくなってしまいました。

元々Scala言語仕様のぎりぎりの所を使っている機能だと思うので、将来にわたっての安全性(Scalaのバージョンが上がると動かなくなる等)と利便性のバランスを考えるとプロダクションコードでは使いにくくなりました。

OptionやBooleanの振舞いをカスタマイズするのに便利だったのでちょっと残念。

PromiseとFuture

Scalaz 7.1では、Promiseモナドが非推奨になっていて、代わり(?)にFutureモナドが追加されているようです。Futureモナドは、Scalaの基本機能であるscala.concurrent.FutureをScalazのMonad化したものです。

Operational Monad」では「scala.concurrent.FutureはScalaz 7.1.0的にはモナドとして定義されていない」と書きましたが、間違いだったので訂正します。

Scala Tips / Scala 2.10味見(4) - Future(2)」で書いた通り、ほとんど同じ機能であるPromiseとFutureが(Scalaz上の扱いが異なる状態(PromiseはMonad、Futureは非Monad)で)並立している状況が懸念点だったので、この点が払拭されたのは喜ばしいと思います。

今後は安心してFutureを軸に並列プログラミングの戦略を考えていくことができます。

また、並列プログラミングの部品となるモナドとしてはscalaz.concurrent.Taskは引き続き有効です。(注: scalaz.concurrent.Taskが内部的に使用している部品にscalaz.concurrent.Futureがありますが、scala.concurrent.Futureと紛らわしいこともありここでは触れないことにします。本稿ではscala.concurrent.FutureをFutureと呼びます。)

FutureとTaskはざっくりいうと以下のような性質の違いがあると理解しています。

Future
スレッドを積極的に生成して処理を行う
Task
スレッドはできるだけ生成しない戦略

I/Oバウンドの処理はFutureが有効です。

一方CPUバウンドの処理に関しては、100コア級のメニーコア時代になればFutureを使うのがよさそうですが、せいぜい4コア級、AWSのローエンドは1コアの状況では、Futureで細粒度の並列プログラミングをするのは時期尚早の感じです。そういう意味で、当面はTaskもFutureと同様の重み付けの選択肢と考えています。

諸元

  • Scala 2.10.3
  • Scalaz 7.1.0