事前条件チェックの多い処理をスマートに書くには

事前条件チェックの多い処理とは、次の擬似コードのようなものだと思ってもらえれば良い。

function process(...) {
  if (事前条件1) return "Err1"
  if (事前条件2) return "Err2"
  if (事前条件3) return "Err3"
  // ここから本処理
  //   :
  //   :
  return "OK"
}

Scala では、例えば次のようなものである。
Either に関しては、Right が条件成立、Left が条件不成立を表すものとして使っている。

def process(optionCondition: Option[Int], eitherCondition: Either[String, Int], booleanCondition: Boolean): String = {
  optionCondition match {
    case None => "None"
    case Some(x) =>
      eitherCondition match {
        case Left(s) => s.toLowerCase
        case Right(y) =>
          if (! booleanCondition) "false"
          else {
            // ここから本処理
            //   :
            //   :
            (x + y).toString
          }
      }
  }
}

これに対するテストコードはこちら。

assert(process(Some(5), Right(10),    true ) == "15"   )     // 事前条件が全てOKの場合
assert(process(None,    Right(10),    true ) == "None" )     // optionCondition がNG(None)の場合
assert(process(Some(5), Left("Left"), true ) == "left" )     // eitherCondition がNG(Left)の場合
assert(process(Some(5), Right(10),    false) == "false")     // booleanCondition がNG(false)の場合

Scala では「return」を使わないことが推奨されているのであのようになるのだが、事前条件が増えるほどインデントが深くなり、非常に読みづらいコードになってしまう。

そこで、return を使うように書き換えてみると、こうなる。

def process(optionCondition: Option[Int], eitherCondition: Either[String, Int], booleanCondition: Boolean): String = {
  val x = optionCondition.getOrElse { return "None" }
  val y = eitherCondition.left.map { s => return s.toLowerCase }.merge
  if (! booleanCondition) return "false"
  // ここから本処理
  //   :
  //   :
  (x + y).toString
}

インデントが深くならず、読みやすいコードになった。
…と、ここで終わってしまってもいいのだが、せっかくなので、他の書き方がないか考えてみた。

Scalaz の Validation を使ってみる

以前から気になっていた Scalaz というライブラリを使ってみることにした。
Scalaz に含まれている Validation トレイトを使うと、次のように書ける。

import scalaz.Scalaz._

def process(optionCondition: Option[Int], eitherCondition: Either[String, Int], booleanCondition: Boolean): String = {
  (for {
    x <- optionCondition.toSuccess("None")
    y <- validation(eitherCondition.left.map { _.toLowerCase })
    _ <- if (booleanCondition) ().success else "false".fail
  } yield {
    // ここから本処理
    //   :
    //   :
    (x + y).toString
  }).either.merge
}

あるいは、次のようにすると Option、Either、Boolean それぞれに対するハンドリングの仕方が(ほぼ)統一できてすっきりする。
(Either の場合だけ success と fail の順序が逆になってしまうが…)

def process(optionCondition: Option[Int], eitherCondition: Either[String, Int], booleanCondition: Boolean): String = {
  (for {
    x <- optionCondition.fold(_.success, "None".fail)
    y <- eitherCondition.fold(_.toLowerCase.fail, _.success)
    _ <- booleanCondition.fold(().success, "false".fail)
  } yield {
    // ここから本処理
    //   :
    //   :
    (x + y).toString
  }).either.merge
}

さらに一歩進めて、「.success」の部分を省けるようにしてみたのがこちら。ついでに「.either.merge」も省けるようにした。

implicit def wrapOption[A](option: Option[A]) = new {
  def ?|[E](e: => E): Validation[E, A] = option.fold(_.success, e.fail)
}

implicit def wrapEither[A, B](either: Either[A, B]) = new {
  def ?|[E](f: A => E): Validation[E, B] = either.fold(f(_).fail, _.success)
}

implicit def wrapBoolean(boolean: Boolean) = new {
  def ?|[E](e: => E): Validation[E, Unit] = boolean.fold(().success, e.fail)
}

implicit def mergeValidation[A](validation: Validation[A, A]): A = validation ||| identity

def process(optionCondition: Option[Int], eitherCondition: Either[String, Int], booleanCondition: Boolean): String = {
  for {
    x <- optionCondition  ?| "None"
    y <- eitherCondition  ?| { _.toLowerCase }
    _ <- booleanCondition ?| "false"
  } yield {
    // ここから本処理
    //   :
    //   :
    (x + y).toString
  }
}