例外によるエラー処理の問題点

  • 副作用である
  • 型チェックできない(Scalaは非チェック例外しかない)
  • try catch構文はめんどくさい(catchしてまた例外を投げたり、DRYに書けないことも多い)
  • 例外が起きてほしくない場所がある(Futureの中など)

直和型を使って解決したい

直和型とは

  • 複数の型のうちどれかを表す型
  • たとえば 何もない or Aの型のデータがある → Option[A]
  • たとえば Aの型のデータ or Bの型のデータがある → Either[A, B]
  • たとえば エラーの集合 or 結果のデータがある → Validation[E, A]

直和型をエラー処理に使うとこんなに嬉しい

  • 副作用がない
  • 型でエラーが発生するかどうかがわかる&型チェックされる
  • MonadやApplicativeにより、エラーの合成やエラー処理が簡単に書ける
  • 例外が起きて欲しくないときでもエラーを値として扱えるので安全

Scala標準の直和型によるエラー処理

  • scala.util.Tryを使う
  • scala.util.control.Exceptionを使って例外をOptionやEitherに変換する
  • Scala標準の直和型でも例外を処理するには結構便利
  • しかし、汎用性に欠けたり、微妙に不便な点も…

scala.util.Try

  • 便利だけど、エラーがThrowableに限定される

Option

  • nullのかわりにOptionを使おう!
  • nullの代替としてはいいけど、エラー情報は持てない

Either

Monadとして使おうとすると、rightメソッドでRightProjectionに変換しなければいけない

for {
  a <- e1.right
  b <- e2.right
} yield a + b

使いづらい…

さらに微妙な問題がある

Eitherの不思議な挙動 その1

for {
  a <- Right(1).right
  b = a + 1
} yield b

error: value map is not a member of Serializable with Product
with scala.util.Either[Nothing,(Int, Int)]
a <- Right(1).right

!?

Eitherの不思議な挙動 その2

for {
  a <- Right(1).right
  b <- Right(2).right
  if a > 0
} yield a

type mismatch;
found   : Option[Int]
required: scala.util.Either[?,?]
b <- Right(2).right

!?

Eitherの不思議な挙動の理由

  • EitherはMonadではなく、rightメソッドで変換されるRightProjectionにflatMapなどがある
  • ところがRightProjectionが返すのはEither
  • 上で述べたとおりEitherにはmapやforeachなどのメソッドがない
  • というわけで前述のような不思議な挙動が発生する

EitherがMonadになればこんな問題は起きないのに!

scalaz.\/

  • EitherはだいたいRightが正常値、Leftがエラーなどの異常値として使われる
  • じゃあEither全体を右の値のコンテナとしてMonadにすればいいんじゃね?
  • そういう発想(right-bias)として作られたのがScalaz.\/ (Either)
  • Scala標準のEitherとは違い、right-biasによりMonadになっている

scalaz.\/

val e1 = 1.right[String]
val e2 = 2.right[String]
for {
  a <- e1
  b <- e2
  c = a + b
  if c > 0
} yield c

短く問題なく使えて嬉しい!

余談:right-biased Eitherにまつわるあれこれ

  • HaskellのEitherもright-biased
  • Scala標準のEitherの作者であるTony Morrisさんもright-biasedにしたかった
  • Scala標準のEitherもright-biasedにしようという議論がたびたび起きるが、Eitherは左右両方を平等に扱うべきという意見により却下され続けている
  • right-biased Eitherであってもswap、leftMapなどのメソッドを使い左側の値を扱うことができるというのがScalazの主張
  • Scalaz.\/を使おう!

Validation

  • Eitherは便利だが、エラー値が一つしか扱えない
  • 最初にエラーが起きたらそこで処理を中断してしまう
  • 一通り実行してエラーを集めるようなコンテナもあったら便利じゃね?
  • e.g. HTMLのフォームのエラー処理
  • e.g. プログラミング言語のエラーチェック
  • そこでValidationですよ!

scalaz.Validation

  • Eitherと同じようにエラーと正常値の直和型
sealed abstract class Validation[+E, +A]
final case class Success[A](a: A) extends Validation[Nothing, A]
final case class Failure[E](e: E) extends Validation[E, Nothing]

scalaz.Validation

  • Semigroupとしてエラーを集約する
  • MonadではなくApplicativeである
def ap[EE >: E, B](x: => Validation[EE, A => B])
    (implicit E: Semigroup[EE]): Validation[EE, B] = (this, x) match {
  case (Success(a), Success(f))   => Success(f(a))
  case (e @ Failure(_), Success(_)) => e
  case (Success(f), e @ Failure(_)) => e
  case (Failure(e1), Failure(e2)) => Failure(E.append(e2, e1))
}

Validationの使い方

case class Person(name: String, age: Int)

def validateName(name: String) =
  if (name.length > 1) name.successNel
  else "invalid name".failureNel

def validateAge(age: Int) =
  if (age >= 0) age.successNel
  else "invalid age".failureNel

Validationの使い方

scala> (validateName("Yoshida") |@| validateAge(27))(Person)
res0: scalaz.Validation[scalaz.NonEmptyList[String],Person]
      = Success(Person(Yoshida,27))

scala> (validateName("") |@| validateAge(-1))(Person)
res1: scalaz.Validation[scalaz.NonEmptyList[String],Person]
      = Failure(NonEmptyList(invalid name, invalid age))

EitherとValidationの違い

  • Validationはエラーを集約する? じゃあ常にValidationを使えばいいのでは?
  • しかし先に述べたようにValidationはMonadではなくApplicativeである

Monadが使いたいケース

case class Person(name: String, age: Int, job: String)

def validateJob(job: String, age: Int) =
  if (6 <= age && age <= 15 && job != "Student") "invalid job".failureNel
  else job.successNel

じゃあ、こう書きたい!

for {
  n <- validateName(name)
  a <- validateAge(age)
  j <- validateJob(job, a)
} yield Person(n, a, j)

でも書けない(◞‸◟)

EitherとValidationを使いわける

  • いっぺんにたくさんvalidationをしたい場合はValidationを使おう
  • Monadを使って複雑な合成をしたい場合はEitherを使おう

余談:ValidationはMonadになれないのか

apメソッドでエラーを集約してるところが問題になる

case (Failure(e1), Failure(e2)) => Failure(E.append(e2, e1))
  • MonadのほうがApplicativeより強い
  • flatMap(bind)を使ってapを実装することができる(逆はできない)
def ap[A, B](fa: => F[A])(f: => F[A => B]): F[B]
  = bind(f)(map(fa)) = f flatMap (fa map _)

余談:ValidationはMonadになれないのか

ValidationがMonadだと仮定する

def flatMap[EE >: E, B](f: A => Validation[EE, B]): Validation[EE, B] =
  self match {
    case Success(a) => f(a)
    case e @ Failure(_) => e
  }

OptionやEitherと同じようにflatMapを定義すると以上のようになる

余談:ValidationはMonadになれないのか

FailureのapメソッドにFailureを適用してみる

Failure(e1) ap Failure(e2) === Failure(e1 |+| e2)

flatMapで作られたapメソッドにFailureを適用してみる

Failure(e1) ap Failure(e2)
  === Failure(e2) flatMap (Failure(e1) map _)
  === Failure(e2) // エラーが集約されていない!

違う結果になる

→ flatMapでapを作ることができない

→ ValidationはMonadになることはできない

余談:ValidationはMonadになれないのか

ちなみにこのようなflatMapも定義されていて

import scalaz.Validation.FlatMap._

とすれば使うことができるがエラーが集約されないので注意

Eitherとほぼ同じなのでEitherを使いましょう

<Thank You!>

Important contact information goes here.