事前条件チェックの多い処理をスマートに書くには
事前条件チェックの多い処理とは、次の擬似コードのようなものだと思ってもらえれば良い。
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 } }