Scalazでmonadic programmingする上で困っているのがTryモナドの問題です。
scala.util.TryはScalaで例外処理をmonadicに処理するためにScala 2.10で導入されたScalaの基本機能です。Scalaのモナドとしての要件(flatMapメソッドが定義されている等)は満たしているのでfor式で使用することができます。
しかし、ScalazのMonadではないのでScalazの提供する各種機能の恩恵を得ることができません。このことがScalazを軸としたmonadic programmingの阻害要因になっています。
例外をハンドリングするためにはTryを使うのが自然ですが、Tryを使うとScalazのmonadic programmingがやりづらくなる、という構図です。
問題
TryがScalaz Monadとなっていない理由は、Tryがモナド則の1つであるleft identityを満たしていないとされているからのようです。
この理由は:
の記事の解説によると:
def foo[A, B](a: A): Try[B] = throw new Exception("oops") foo(1) // exception is thrown Try(1).flatMap(foo) // scala.util.Failure
ということなので:
- foo関数が例外を返す場合、TryのflatMapコンビネータにfoo関数を適用した時にflatMapコンビネータが例外を返さないとMonad則のleft identity law違反
ということだと思います。
さて、それではTryのflatMapコンビネータで例外を返すようにすればよいかというと、これはOOP的には困ってしまう仕様です。
OOP的にはTryは例外を包んで外に出さないことを期待したいところです。というのは、flatMapコンビネータが例外を返す可能性があるとすると、Tryを使っているにもかからわずTryも例外を出す可能性があるということなので、Tryの外側をTryで包まなければならなくなります。
これはプログラミング的にも煩雑ですし、Tryをネストさせなければならない条件をプログラマが意識しないといけないのでバグの出やすいインタフェースです。
こういう事情もありScalaの基本ライブラリは、あえて現在の仕様を選択しているようです。
解決策
本稿の趣旨ですが、Try Monad問題を解釈変更で解決するという試みです。
left identityの解釈
Scalaのようなハイブリッドな関数型言語で純粋関数型の計算を行う場合、いくつかの紳士協定を前提とします。
代表的なものとしては以下のようなものがあります。
- var変数は用いない
- mutableなコレクションは用いない
- eqメソッドは用いない
つまりScalaにおける純粋関数型の演算は紳士協定が前提なので、Tryのleft identity問題も紳士協定で解決すればよいのではないかということです。
具体的には返却値にTryを返す:
f(a: A): Try[B]
というシグネチャの関数では例外をスローしない、という紳士協定を導入するというアプローチはどうでしょうか。もちろん絶対ということはないので、例外をスローしたらプログラムが致命的状態(e.g. バグ発生、メモリ不足)に入ったとして扱うという形になります。
プログラミング的には以下をコーディングの基本形にするということなので、特に煩雑な点はありません。
f(a: A): Try[B] = Try { ... }
この紳士協定上ではTryをScalaz Monad化しても問題ないように思います。
実行タイミングの解釈
FPでは参照透過性が重要な要件になっていて、関数を評価した時の内部処理の実行タイミングや実行順序は結果に影響を与えないことになっています。
TryのようなFunctor系のコンテナの場合、コンテナの内部情報を取り出すメソッドの実行前の任意の時点で評価が行われていればよいわけです。
TryのflatMapコンビネータでも、同様のことが言えるはずです。つまりflatMapコンビネータ内で必ずしもMonadのbind演算をする必要はなく、もっと後のタイミングでしても大丈夫ではないかということです。と考えると、flatMapコンビネータが例外をスローすることは必須ではなく、このことがMonad則違反というのは必ずしも真とは限らないというわけです。
整理すると、以下になります。
- Tryの内部実行がコンビネータの呼び出しと同時に行われていなければならないという計算モデルではMonad則違反
- Tryの内部実行がコンテナの内部を取り出すメソッドの実行前の任意の時点で評価が行われていればよいという計算モデルではMonad則OK
Tryの計算モデルを後者であると解釈すると、Monad則が成り立っていると考えてよいのではないかと思います。
実際にScalazのFuture MonadやTask Monadも同様の問題があるはずですが、いずれもScalaz Monadとして定義されています。
Futureの場合は、関数型的な意味での遅延評価ではなくて、別スレッドで非同期実行されるためflatMapで例外を返すようにはできないのだと推測されます。
Taskの場合は、(通常は)runメソッドで実行を指示されるまで実行は遅延されます。
いずれの場合もmapコンビネータやflatMapコンビネータが例外を返すことはありません。そして例外は実行結果を取り出すときに、必要に応じてスローされるようになっています。
FP的なMonadの定義としてこれが許されるならTryについてもgetメソッドで例外のスルーは行われるわけですから、flatMapコンビネータで例外を返さなくてもよいと考えることは十分可能と思います。
まとめ
考察の結果left identityの解釈、実行タイミングの解釈のどちらか一つでもOKであればScala TryをScalaz Monadとして定義しても問題ないのではないかという結論に落ち着きました。
個人的にはいずれの解釈もOKと思えるので、プロダクションのコードでTry Monadを使用することにしました。
Try Monadの実装としてはscalaz-outlawsを使うのが一案ですが、現状ではdeprecatedの警告が出るので諦めて、自前のライブラリ内で定義して使うことにしました。
TryをScalaz Monad化できると、さらに広い範囲でmonadic programmingを適用できるようになります。Try Monadの存在を軸に例外処理戦略を練りなおしてみたいと思います。
おまけ
Scalazによるmonadic programmingでTryを使う場合、TryがApplicativeであるだけでも、色々と応用範囲が広がります。(e.g. Traverse)
TryはApplicativeの要件は満たしていると思うので、Try Monadが不安な場合は(Validationと同じように)TryをApplicativeとして定義して使用するのも有用だと思います。
諸元
- Scala 2.10.3
- Scalaz 7.1.0
0 件のコメント:
コメントを投稿