2012年2月21日火曜日

Scala Tips / Either (7) - for

Eitherを成功/失敗の文脈で使用する方法のイディオムです。

Eitherでは、値を取り出す処理として以下の2つのコーディングパターンがあります。

  • Either[A, B]からEither[A, C]に変換
  • Eigher[A, B]からCに変換

今回は、前者についてfor式で取り出すイディオムについて見ていきます。

EitherEither (2) - flatMapでEitherを成功/失敗の文脈として使用する方法について説明しました。

Eitherは成功/失敗の文脈を切り替えない以下の演算:

条件結果
Either[A, B]がRight[A, B]Right[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

Either (2) - flatMapは成功/失敗の文脈を切り替える以下の演算です。

条件結果
Either[A, B]のRight[A, B]に有効な値が入っているRight[A, C]
Either[A, B]のRight[A, B]に無効な値が入っているLeft[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

今回はこの2つの演算をfor式を使って書いてみます。

文脈切替なし

Eitherで取り上げた成功の文脈と失敗の文脈の切り替えが発生しない演算です。以下の表に示す演算になります。

条件結果
Either[A, B]がRight[A, B]Right[A, C]
Either[A, B]がLeft[A, B]Left[A, C]
Scala

Either[Exception, Int]からEither[Exception, String]へ変換は以下のようになります。Eitherそのものはモナドではないためfor式のジェネレーターに指定することはできません。右側が成功の文脈になるので、Either#rightメソッドでRightProjectionを取得し、これをジェネレーターに指定します。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  for (b <- a.right) yield b.toString
}

Eitherで使用したmapメソッドと同様に、Either[Exception, Int]がLeft(Exception)だった場合の処理を書く必要がありません。

Scalaz

Scalazでは、Eitherが右側を成功文脈とするモナドに拡張されるのでfor式のジェネレーターに直接指定することができます。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  for (b <- a) yield b.toString
}

文脈切替あり

Either (2) - flatMapで取り上げた成功の文脈と失敗の文脈の切り替えが発生する演算です。以下の表に示す演算になります。

条件結果
Either[A, B]のRight[A, B]に有効な値が入っているRight[A, C]
Either[A, B]のRight[A, B]に無効な値が入っているLeft[A, C]
Either[A, B]がLeft[A, B]Left[A, C]
Scala

Intは0以上のものが有効という条件付きのEither[Exception, Int]からEither[Exception, String]へ変換は以下のようになります。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  def g(c: Either[Exception, Int]): Either[Exception, Int] = {
    c.right flatMap { d =>
      if (d >= 0) Right(d)
      else Left(new IllegalArgumentException("bad"))
    }
  }
  for (b <- g(a).right) yield b.toString
}

Optionの場合と違って、Eitherの場合はif句で成功文脈から失敗文脈への切り替え条件を指定することはできません。if句を指定すると計算文脈がEitherからOptionに切り替わってしまうからです。

そこで、ジェネレータに指定する前に成功文脈から失敗文脈への切り替え判定をしておくようにプログラミングしてみました。この処理をfor式内に直接記述するとプログラムの見通しが悪くなるので、ローカル関数gを作成し、これを呼び出すようにしています。

それでもプログラムの見通しはかなり悪くなってしまっていて、ちょっと無理がある感じですね。このためEitherを計算文脈として成功文脈から失敗文脈への切り替えを持つ処理をfor式で書くのはあまり得策ではないようです。直接flatMapメソッドを使ったほうがよいでしょう。

Scalaz

Scalazの場合も、基本的にはScalaの場合と同じでfor式を使うのは無理があるようです。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  def g(c: Either[Exception, Int]): Either[Exception, Int] = {
    c >>= { d =>
      (d >= 0) either new IllegalArgumentException("bad") or d
    }
  }
  for (b <- g(a)) yield b.toString
}

ノート

Optionのfor式で説明したとおり、Scalaのfor式はモナドによる演算の文法糖衣となっています。

このためEitherのRightProjectionまたはLeftProjectionをジェネレーターに指定することができます。また、ScalazではEitherが右側を成功文脈とする成功/失敗のモナドに拡張されるので直接ジェネレータに指定できるようになります。

Eitherをfor式で使用するときの鬼門はif句で、計算文脈をEitherからOptionに切り替えてしまうので、この条件に合致する場合でないと使うことができません。

このため、成功文脈から失敗文脈への切替ではif句を使わない方法を使用する必要があります。本記事では、ジェネレータに指定する段階で成功の文脈から失敗の文脈に切り替える方式で対応してみました。 本記事の例題のような簡単な処理の場合は、処理のバランス上、回避処理が大げさになってしまいます。for式を使うより、直接flatMapメソッド(Scalazの場合は>>=メソッド)で記述したほうがよいでしょう。

ただし、より複雑なMonadic演算をする場合には、本記事で行った回避策を用いてfor式を使ったほうがよい場合もあります。この話題はいずれ取り上げる予定です。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿