2012年4月11日水曜日

Scala Tips / Either (20) - Validationと相互変換

EitherとValidationの相互変換のイディオムです。

scalaz.ValidationはScalazが提供する「検証の文脈」のモナドです。「成功と失敗の文脈」としても使うことができるので、OptionやEitherと適材適所で使い分けしていくことになります。

Either編の後はValidation編に入る予定ですが、前回「Optionと相互変換」でも「o.toSuccess(e).either」という形で登場したので、少し内容を先取りして取り上げます。

(分類の基準)

Java風

Validation→Either

ValidationをEitherに変換する場合は、if式による判定を用いて、Validationの成否に対応する値をLeftまたはRightに詰めます。

Validationでは、成功または失敗の値を直接取り出す方法がないので、キャストを用いています。あまり美しくないですね。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  if (v.isSuccess) Right(v.asInstanceOf[Success[Throwable, T]].a)
  else Left(v.asInstanceOf[Failure[Throwable, T]].e)
}

キャストはちょっと筋が悪いので、美しさを追求する場合は、一度Optionに変換する方法もあります。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  if (v.isSuccess) Right(v.toOption.get)
  else Left(v.fail.toOption.get)
}
Either→Validation

EitherをValidationに変換する場合は、EitherのisRihtメソッドを用いて判定し、rightメソッドで取り出したRightProjectionまたはleftメソッドで取り出したLeftProjectionからgetメソッドで値を取り出し、SuccessまたはFailureに詰めます。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  if (e.isRight) Success(e.right.get)
  else Failure(e.left.get)
}

Scala風

Validation→Either

ValidationをEitherに変換する場合は、match式でSuccessまたはFailureへの判定を用いて、Validationの成否に対応する値をLeftまたはRightに詰めます。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  v match {
    case Success(s) => Right(s)
    case Failure(f) => Left(f)
  }
}
Either→Validation

EitherをValidationに変換する場合は、match式でLeftまたはRightへの判定を用いて、Eitherの成否に対応する値をSuccessまたはFailureに詰めます。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  e match {
    case Right(r) => Success(r)
    case Left(l) => Failure(l)
  }
}

Scala

ValidationはScalazが提供するモナドですが、ここではScala的なコーディングということでfoldメソッドを用いた方法を紹介します。

Validation→Either

Validationのfoldメソッドの第1引数に失敗側の値をLeftに変換する関数を、第2引数に成功側の値をRightに変換する関数を指定します。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  v.fold(Left(_), Right(_))
}

Scalazの機能を用いて、よりコンパクトに書くと以下のようになります。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  v.fold(_.left, _.right)
}
Either→Validation

Eitherのfoldメソッドの第1引数に左側の値をFailureに変換する関数を、第2引数に右側の値をSuccessに変換する関数を指定します。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  e.fold(Failure(_), Success(_))
}

Scalazの機能を用いて、よりコンパクトに書くと以下のようになります。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  e.fold(_.fail, _.success)
}

Scalaz

Validation→Either

Validationには、Eitherに変換するためのeitherメソッドが用意されているので、これを用います。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  v.either
}
Either→Validation

Scalazでは、validation関数でEitherをValidationに変換することができます。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  validation(e)
}
追記:2012-04-16

Either→ValidationのScalaz版の内容が間違っていたので書き換えました。

ノート

ValidationとEitherの相互変換は、ダイレクトに変換するメソッドが提供されているので、これを用いればよいのですが、Scalaでのプログラミングの考え方をおさらいするのによい例題なのでJava風、Scala風、Scalaの項目も考えてみました。

何かの処理をScalaでプログラミングする場合、以下の順番でロジックを探す事になると思います。

  1. Scalaz/Scalaでドンピシャのメソッドを見つける
  2. fold系やmap, flatMapといった高階関数を使ってロジックを考える
  3. match式を使って場合分けをする
  4. if式を使って場合分けをする

この順番は、Scala Tipsで用いている分類の基準であるScalaz、Scala、Scala風、Java風とも近しいものになっています。

個人的には(パターンマッチングや再帰呼び出し、代数的データ型の選択といったところではなく)成否判定にmatch式が出てきたら負けっぽい感じで考えています。if式はさらに負け度が大きい感じです。この場合、できるだけ2の「fold系やmap, flatMap」に持ち込むように考えていきます。

なぜ、「fold系やmap, flatMap」の方が好ましいのかというと、感覚的には、手続きではなく宣言に近づいていくから、ということになります。具体的なメリットとしては、(1)ロジックを簡潔に記述することができる、(2)部品(関数)の再利用の可能性が高まる、ということです。

OOPでは「fold系やmap, flatMap」という切り口のプログラミングはあまりないので、OOPプログラマがScala(関数型言語)を使う上で意識しておくべきコツと言うことができそうです。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿