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)が演算結果となります。

  1. def f(a: Either[Exception, String]): Either[Exception, Int] = {  
  2.   if (a.isRight) {  
  3.     try {  
  4.       Right(a.right.get.toInt)  
  5.     } catch {  
  6.       case e: NumberFormatException => Left(e)  
  7.     }  
  8.   } else a.asInstanceOf[Either[Exception, Int]]  
  9. }  

Scala風

match式を使うと以下のようになります。try/catch文が入るとプログラムの見通しはJava風とそれほど変わらなくなります。

  1. def f(a: Either[Exception, String]): Either[Exception, Int] = {  
  2.   a match {  
  3.     case Right(b) => {  
  4.       try {  
  5.         Right(b.toInt)  
  6.       } catch {  
  7.         case e: NumberFormatException => Left(e)  
  8.       }  
  9.     }  
  10.     case Left(b) => Left(b)  
  11.   }  
  12. }  

Scala

Eitherのrightメソッドから得られるRightProjectionのmapメソッドを使うのがScala的なコーディングです。ただし、今回のケースでは例外をキャッチする処理を行う必要があります。

  1. def f(a: Either[Exception, String]): Either[Exception, Int] = {  
  2.   try {  
  3.     a.right.map(_.toInt)  
  4.   } catch {  
  5.     case e: NumberFormatException => Left(e)  
  6.   }  
  7. }  

mapメソッドの外側でtry/catchを使って例外をキャッチするのは、ちょっと美しくありません。例外に依存するロジックは部品化の妨げになるので、できるだけ局所化して隠蔽したいところです。

このためには、mapメソッドの中で例外をキャッチしたいわけですが、この場合は例外をキャッチしたときに、成功の文脈から失敗の文脈への切り替えを行う必要があるので、Either (2)で取り上げたEitherProjection#flatMapメソッドを使用します。

  1. def f(a: Either[Exception, String]): Either[Exception, Int] = {  
  2.   a.right.flatMap { b =>   
  3.     try {  
  4.       Right(b.toInt)  
  5.     } catch {  
  6.       case e: NumberFormatException => Left(e)  
  7.     }  
  8.   }  
  9. }  
scala.util.control.Exception

例外をキャッチしてEitherに変換する専用の関数がオブジェクトscala.util.control.Exceptionに用意されています。よく使うのがcatching関数とallCatch関数です。

catching関数は、キャッチする例外を列挙して指定することができます。catching関数から返されるCatchオブジェクトのeitherメソッドで例外の発生をEitherに変換することができます。

  1. import scala.util.control.Exception._      
  2.   
  3. def f(a: Either[Exception, String]): Either[Exception, Int] = {  
  4.   a.right.flatMap { b =>   
  5.     catching(classOf[NumberFormatException]).either(b.toInt)  
  6.              .asInstanceOf[Either[Exception, Int]]  
  7.   }  
  8. }  

ただ、残念なのがcatching関数の返す型がEither[Throwable, U]であるため、Either[Exception, Int]を返すと不整合を起こしてしまいます。そこで、ここではasInstanceOf[Exception, Int]を用いて回避するという残念な結果になりました。

allCatch

多くの場合はNumberFormatException以外の例外が発生しても関数のエラーとして返して大丈夫だと思われるので、すべての例外をキャッチするallCatching関数を使う方法も有力です。

  1. import scala.util.control.Exception._      
  2.   
  3. def f(a: Either[Exception, String]): Either[Exception, String] = {  
  4.   a.right.flatMap { b =>  
  5.     allCatch.either(b.toInt)  
  6.             .asInstanceOf[Either[Exception, String]]  
  7.   }  
  8. }  

この場合も、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 件のコメント:

コメントを投稿