2012年9月14日金曜日

Scala Tips / Scala 2.10味見(10) - Try(2)

TryとEitherは2つの状態による計算文脈を表現するという意味ではよく似たオブジェクトですが、Tryはファンクタ(Functor)かつモナド(Monad)であるのに対してEitherはそうではないという点が異なります。

Eitherは直和の性質を表現するために2つの状態を同じ比重で扱いますが、この点を徹底するためにファンクタやモナドにはしていないと思われます。EitherではLeftProjectionやRightProjectionと組み合わせることによって、ファンクタ的、モナド的な使い方も可能ですが使い勝手がよいとはいえません。

正常状態と異常状態の2つの状態を扱う場合、(1)出現頻度は正常状態の方が多い、(2)正常状態に対するロジックは複雑/異常状態に対するロジックは単純、(3)一度異常状態になると以降は異常状態を保持、となるケースが多いと思います。このようなケースでは、正常状態の方を主にしたファンクタやモナドにするとプログラミング的な取り回しがとても楽になります。このような機能を実現しているのがTryというわけです。

準備

準備として奇数の時に例外を投げる関数を用意します。

def even(v: Int) = {
  if (v % 2 == 1) throw new IllegalStateException("odd")
  else v
}

Tryと組み合わせた実行結果は以下のようになります。

scala> Try(even(10))
res52: scala.util.Try[Int] = Success(10)

scala> Try(even(11))
res53: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

ファンクタ

Tryはファンクタなのでmapメソッドを使った関数合成ができます。

Successに対しては格納されている値に関数が適用され、新たなSuccessが返されます。成功状態が維持されます。

scala> Try(even(10)).map(_ + 1)
res54: scala.util.Try[Int] = Success(11)

Failureに対してはそのままFailureが返されます。異常状態が維持されるわけです。

scala> Try(even(11)).map(_ + 1)
res55: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

mapメソッドで関数を連続して適用することもできます。Successの場合は関数を連続適用した結果が返されます。一方、Failureの場合はそのままFailureが返されます。

scala> Try(even(10)).map(_ + 1).map(_ * 10)
res56: scala.util.Try[Int] = Success(110)

scala> Try(even(11)).map(_ + 1).map(_ * 10)
res57: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

モナド

Tryの状態が最初のものをそのまま維持する場合はよいのですが、演算結果によって状態を変えたい場合にはモナドを使います。また、複数のTryの状態を合成したい場合もモナドですね。

scala> a.flatMap(x => c.map(_ + x))
res58: scala.util.Try[Int] = Success(22)

scala> a.flatMap(x => b.map(_ + x))
res59: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

scala> a.flatMap(x => b.flatMap(y => c.map(_ + x + y)))
res60: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

モナドの演算はfor式の文法糖衣を使って簡潔に記述することができます。上の演算をfor式で記述すると以下になります。

scala> for (x <- a; y <- c) yield x + y
res61: scala.util.Try[Int] = Success(22)

scala> for (x <- a; y <- b) yield x + y
res62: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

scala> for (x <- a; y <- b; z <- c) yield x + y + z
res63: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

ノート

TryはFunctor則を満たしていないのでは、という問題があるようで少し前に議論になっていました。

https://issues.scala-lang.org/browse/SI-6284

Functor則を満たさないのは、関数合成の中で例外をキャッチして正常処理に戻すという特殊なケースのようなので、このままの仕様でも、仕様変更があっても大きな影響はないと思います。 

ただ、mapやflatMapなどのコンビネータを使い関数合成を軸にプログラムを構築していくスタイルの関数プログラミングでは、Functor則を満たしているということは非常に重要です。合成の順番で結果が変わってしまうとなると、プログラミングの前提が変わってしまうからです。

このため、TryがFunctor則を満たす範囲がどこまでなのかという点については常に意識して置く必要があります。議論の結果はまだフォローできていませんが、次のリリースで結果を確認したいと思います。

アプリカティブ

演算結果によってTryの状態を変えたい場合や複数のTryの状態を合成したい場合もモナドを使います。

演算結果によって状態は変えないが、複数のTryの状態を合成したい場合はアプリカティブ(Applicative)を使うことになりますが、Scalaの基本ライブラリではアプリカティブ機能は提供されていません。

必要な場合はScalazとTryをつなぐ型クラスインスタンスを定義してScalazを使うことになります。

アプリカティブが使えると便利ですが、モナドに対するfor式の文法糖衣でも相当のことができるので、なくても実用的には問題ありません。

諸元

  • Scala 2.10.0-M7

0 件のコメント:

コメントを投稿