2012年2月22日水曜日

Scala Tips / Either (8) - for, getOrElse

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

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

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

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

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

Either (3) - getOrElseEither (4) - getOrElse, flatMapでEitherを成功/失敗の文脈として使用し、値を取り出す方法について説明しました。

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

条件結果演算
Either[A, B]がRight[A, B]CBからCを計算
Either[A, B]がLeft[A, B]Cデフォルト値

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

条件結果演算
Either[A, B]がRight[A, B]で有効な値が入っているCBからCを計算
Either[A, B]がRight[A, B]で無効な値が入っているCデフォルト値
Either[A, B]がLeft[A, B]Cデフォルト値

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

文脈切替なし

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

条件結果演算
Either[A, B]がRight[A, B]CBからCを計算
Either[A, B]がLeft[A, B]Cデフォルト値
Scala

Either[Exception, Int]からEither[Exception, String]へ変換は以下のようになります。

def f(a: Either[Exception, Int]): String = {
  (for (b <- a.right) yield b.toString) match {
    case Right(c) => c
    case Left(_) => ""
  }
}

for式でEither[Exception, Int]→Either[Exception, String]の処理を行い、結果をmatch式で取り出します。EitherはOptionのgetOrElseに相当するメソッドがないのでmatch式を使います。

Option

Eitherで説明したとおり、演算が有効でない場合にはデフォルト値を返す処理では遅かれ早かれエラー情報を捨てるので、計算文脈をOptionに切り替えて演算を進めるのが有用なテクニックです。

Optionに切り替えるタイミングは色々ありますが、演算の最後にOptionに切り替えると以下のようになります。

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

また、最初の段階でOptionに切り替えると以下のようになります。

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

こちらのほうが簡明ですね。演算途中でエラー情報を使わない場合は、最初の段階でOptionに切り替えるとよいでしょう。

Scalaz & Option

Scalazを使うとgetOrElseメソッドの代わりに「|」を使うことができます。記述がさらに簡明になります。

def f(a: Either[Exception, Int]): String = {
  (for (b <- a.right.toOption) 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]からStringへ変換をfor式を使って記述すると以下のようになります。

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) getOrElse ""
}

これは、前回のプログラムを拡張したものですが前回と同様に少し無理がありますね。

Option

Eitherの中身を値として取り出すときはOptionに変換する方法が有用です。Optionに変換してよいのであれば、for式のif句を使うことができます。

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

for式の結果がOptionで返されるのでgetOrElseメソッドで値を取り出します。

Scalaz & Option

if句を使う場合は、Scalazの場合もrightメソッドでRightProjectionを使います。

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

Scalazを使うとgetOrElseメソッドの代わりに「|」を使うことができます。記述がさらに簡明になります。

ノート

Eitherに対してfor式のif句を使用するとEitherがOptionに切り替わってしまいます。Monadic演算的にはちょっと困った感じもしますが、最終的に値を取り出す用途には都合がよい振舞いともいえます。

Eitherを使うときの技として覚えておくと便利です。

warning

for式のif句でEitherを使おうとすると以下の警告が出ます。

<console>:17: warning: `withFilter' method does not yet exist on Either.RightProjection[Exception,Int], using `filter' method instead
         (for (b <- a.right if b >= 0) yield b.toString) | ""
                      ^
f: (a: Either[Exception,Int])String

for式のif句はwithFilterメソッドを想定しているのに対して、Eitherはfilterメソッドしか用意していないため、withFilterメソッドの代替手段としてfilterメソッドを使っているという警告です。

この警告から、Eitherはあまり使われていないので、こういう細かい機能が熟(こな)れていないのではないか、という印象を受けます。

クラスライブラリのあまり熟(こな)れていない所の細かい機能を使うと何かとバグを踏んだり、仕様変更の被害を受けがちなので、Eitherに対するfor式のif句はあまり積極的には使わないほうがよいかもしれません。リスクを勘案した上で使用の有無を決めるとよいでしょう。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿