2012年2月8日水曜日

Scala Tips / Option (10) - Exception

Optionから値を取り出すイディオムです。

Optionから値を取り出す際に、以下の表の演算を行う方法についてみてきました。

条件結果
Option[A]に有効な値が入っているSome[B]
Option[A]に無効な値が入っているNone
Option[A]がNoneNone

今回は「Option[A]に無効な値が入っている」の判定で例外を使用するケースです。例外を使うと、プログラミングにも少なからぬ影響が出てきます。

以下では、Option[String]からOption[Int]への変換を例に考えてみます。Optionに入っているStringがIntに変換できる場合は有効となり、Some[Int]が処理結果となります。一方、変換できない場合は無効となりNoneが処理結果となります。

ここで、StringからIntへの変換ができない事の判定に例外を使用します。

(分類の基準)

Java風

if式でOption#isDefinedを使って値の有無を判定します。

文字列が整数値に合致しなかった場合String#toIntメソッドがNumberFormatExceptionをスローするので、これをtry/catch文でキャッチしています。

引数がNoneの場合とNumberFormatExceptionをキャッチした時がNone、toIntで整数値が得られた場合はSome[Int]が演算結果となります。

def f(a: Option[String]): Option[Int] = {
  if (a.isDefined) {
    try {
      Some(a.get.toInt)
    } catch {
      case e: NumberFormatException => None
    }
  } else None
}

Scala風

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

def f(a: Option[String]): Option[Int] = {
  a match {
    case Some(b) => {
      try {
        Some(a.get.toInt)
      } catch {
        case e: NumberFormatException => None
      }
    }
    case None => None
  }
}

Scala

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

def f(a: Option[String]): Option[Int] = {
  try {
    a.map(_.toInt)
  } catch {
    case e: NumberFormatException => None
  }
}

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

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

def f(a: Option[String]): Option[Int] = {
  a.flatMap { b =>
    try {
      Some(b.toInt)
    } catch {
      case e: NumberFormatException => None
    }
  }
}

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

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

import scala.util.control.Exception._    

def f(a: Option[String]): Option[Int] = {
  a.flatMap { b =>
    catching(classOf[NumberFormatException]).opt(b.toInt)
  }
}

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

import scala.util.control.Exception._    

def f(a: Option[String]): Option[Int] = {
  a.flatMap { b =>
    allCatch.opt(b.toInt)
  }
}

Scalaz

Scalaz流のエレガントな書き方はないと思います。

汎用的な方法ではありませんが、今回の例題である文字列を数値に変換する処理に関しては、ScalazのparseIntメソッド(parseFloatその他もあります)が利用できます。

def f(a: Option[String]): Option[Int] = {
  a.flatMap(_.parseInt.toOption)
}

parseIntメソッドは数値への変換の成否をValidationオブジェクトとして通知します。ValidationのtoOptionメソッドでOptionに変換します。

ノート

例外を関数型プログラミングの中でどう扱っていくのかというのは、ちょっと悩むところですが、Java由来のOOPとのハイブリッドであるScalaでは避けて通ることはできません。

例外処理の考え方はいずれイディオムとしてまとめる予定ですが、今の所、できるだけ早い段階でOption, Either, Validationにして関数合成を軸としたMonadic演算のラインに乗せる方向が、よいのではないかと考えています。

そういう意味で、Scala 2.8から入ってきたscala.util.control.Exceptionは重要な機能ということができます。

現段階では、Optionまでしか取り上げていないので、Either, Validationが終わったたところで改めて考えることにします。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

1 件のコメント:

  1. このコメントは投稿者によって削除されました。

    返信削除