for式の中のEitherでifを使いたい

scala 2.12ではJava8対応の変更が多く、派手な変更は多くはありませんでした。そんな中でEitherがright-biasedになったのは割とキャッチーなのではないでしょうか。Eitherには今までflatMapなどのメソッドがなかったので、for式の中で使うときなど、いったんRightProjectionに変換する必要があったのですが、それが必要なくなりました。

for {
  x <- Right(3).right
} yield x

これが

for {
  x <- Right(3)
} yield x

こう書けるようになりました。

これは便利ですね。まあrightが取れるだけなんですが、私もよく .right つけろよとコンパイラに怒られていたので嬉しいです。

さて、これでscalaのEitherが実用的になったという声も聞こえるのですが、実際 .right が不要になったから実用的かというと疑問で、どちらかというとscalaのEitherで困るのはfor式の中でifが使えないことだと思います。私はwithFilter問題と勝手に呼んでいてRightProjectionにwithFilterがないせいなんですが、この問題は2.12.xでもまだ健在です。

scala> for {
     |   x <- Right(3)
     |   if x % 2 == 1
     |   y = x + 1
     | } yield y
<console>:13: error: value withFilter is not a member of scala.util.Right[Nothing,Int]
         x <- Right(3)
                   ^

ほら、if使えないでしょ。これは困りますね。そこでEitherに限った方法ではないんですが、ちょっとヘルパーを定義します。

def when[E](p: Boolean)(e: => E): Either[E, Unit] =
  if (p) Right(()) else Left(e)

このwhenはpの条件を満たせばRightを返すのでそれ以下のfor式も実行されます。満たさなければLeftを返してエラーにします。

scala> val result = for {
     |   x <- Right(3)
     |   _ <- when(x % 2 == 1)("error")
     |   y = x + 1
     | } yield y
result: scala.util.Either[String,Int] = Right(4)

scala> val result = for {
     |   x <- Right(3)
     |   _ <- when(x % 2 == 0)("error")
     |   y = x + 1
     | } yield y
result: scala.util.Either[String,Int] = Left(error)

これでEitherでもifっぽいことをできるようになり、めでたしです。

追記

@gakuzzzzさんにEither.condの存在を教えられた。

def when[E](p: Boolean)(e: => E): Either[E, Unit] =
  Either.cond(p, (), e)