Eitherを成功/失敗の文脈で使用する方法のイディオムです。
前回はEitherから値を取り出す際に、以下の表の演算を行う方法について考えました。
条件 | 結果 |
---|---|
Either[A, B]のRight[A, B]に有効な値が入っている | Right[A, C] |
Either[A, B]のRight[A, B]に無効な値が入っている | Left[A, C] |
Either[A, B]がLeft[A, B] | Left[A, C] |
今回は「Right[A, B]に無効な値が入っている」の判定で例外を使用するケースです。
以下では、Either[Exception, String]からEither[Exception, Int]への変換を例に考えてみます。Eitherに入っているStringがIntに変換できる場合は有効となり、Right[Exception, Int]が処理結果となります。一方、変換できない場合は無効となりLeft[Exception, Int]が処理結果となります。
ここで、StringからIntへの変換ができない事の判定に例外を使用します。(String#toIntメソッドがNumberFormatExceptionをスロー。)
(分類の基準)
Java風
if式でEither#isRightを使って値の有無を判定します。
文字列が整数値に合致しなかった場合String#toIntメソッドがNumberFormatExceptionをスローするので、これをtry/catch文でキャッチしています。
引数がLeftの場合とNumberFormatExceptionをキャッチした時がLeft(Exception)、toIntで整数値が得られた場合はRight(Int)が演算結果となります。
def f(a: Either[Exception, String]): Either[Exception, Int] = { if (a.isRight) { try { Right(a.right.get.toInt) } catch { case e: NumberFormatException => Left(e) } } else a.asInstanceOf[Either[Exception, Int]] }
Scala風
match式を使うと以下のようになります。try/catch文が入るとプログラムの見通しはJava風とそれほど変わらなくなります。
def f(a: Either[Exception, String]): Either[Exception, Int] = { a match { case Right(b) => { try { Right(b.toInt) } catch { case e: NumberFormatException => Left(e) } } case Left(b) => Left(b) } }
Scala
Eitherのrightメソッドから得られるRightProjectionのmapメソッドを使うのがScala的なコーディングです。ただし、今回のケースでは例外をキャッチする処理を行う必要があります。
def f(a: Either[Exception, String]): Either[Exception, Int] = { try { a.right.map(_.toInt) } catch { case e: NumberFormatException => Left(e) } }
mapメソッドの外側でtry/catchを使って例外をキャッチするのは、ちょっと美しくありません。例外に依存するロジックは部品化の妨げになるので、できるだけ局所化して隠蔽したいところです。
このためには、mapメソッドの中で例外をキャッチしたいわけですが、この場合は例外をキャッチしたときに、成功の文脈から失敗の文脈への切り替えを行う必要があるので、Either (2)で取り上げたEitherProjection#flatMapメソッドを使用します。
def f(a: Either[Exception, String]): Either[Exception, Int] = { a.right.flatMap { b => try { Right(b.toInt) } catch { case e: NumberFormatException => Left(e) } } }
scala.util.control.Exception
例外をキャッチしてEitherに変換する専用の関数がオブジェクトscala.util.control.Exceptionに用意されています。よく使うのがcatching関数とallCatch関数です。
catching関数は、キャッチする例外を列挙して指定することができます。catching関数から返されるCatchオブジェクトのeitherメソッドで例外の発生をEitherに変換することができます。
import scala.util.control.Exception._ def f(a: Either[Exception, String]): Either[Exception, Int] = { a.right.flatMap { b => catching(classOf[NumberFormatException]).either(b.toInt) .asInstanceOf[Either[Exception, Int]] } }
ただ、残念なのがcatching関数の返す型がEither[Throwable, U]であるため、Either[Exception, Int]を返すと不整合を起こしてしまいます。そこで、ここではasInstanceOf[Exception, Int]を用いて回避するという残念な結果になりました。
allCatch
多くの場合はNumberFormatException以外の例外が発生しても関数のエラーとして返して大丈夫だと思われるので、すべての例外をキャッチするallCatching関数を使う方法も有力です。
import scala.util.control.Exception._ def f(a: Either[Exception, String]): Either[Exception, String] = { a.right.flatMap { b => allCatch.either(b.toInt) .asInstanceOf[Either[Exception, String]] } }
この場合も、Either[Throwable, U]をasInstanceOf[Exception, Int]を用いて回避しています。
Scalaz
Scalaz流のエレガントな書き方はないと思います。
ノート
Eitherがかなり扱いにくいオブジェクトであることを説明しましたが、例外処理においてもその問題が顕在化しています。
Eitherの問題は、scala.util.control.Exceptionのcatchingメソッド、allCatchメソッドで返されるCatchオブジェクトのeitherメソッドのシグネチャがThrowable固定になっていることに起因しています。あまりEitherが使われていないために、クラスライブラリにおけるEitherの扱いが熟(こな)れていないという印象を受けます。
eitherメソッドがEither[Throwable, U]が返すためにasInstanceOf[Exception, Int]を用いて対応しています。asInstanceOf[Exception, Int]は明らかに美しくないので回避するために、いろいろな方法(withApplyメソッドなど)を試してみましたが結局よい方法が見つかりませんでした。よい回避方法があれば教えてもらえるとうれしいです。
Eitherの使い方のテクニックとしては、Eitherを成功/失敗の計算文脈として使用する場合には、Eitherの左側、失敗文脈にはExceptionではなくThrowableを入れるという慣習にしておくとよいですね。
諸元
- Scala 2.9.1
- Scalaz 6.0.3
0 件のコメント:
コメントを投稿