2012年2月17日金曜日

Scala Tips / Either (5) - Exception

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 件のコメント:

コメントを投稿