2012年2月24日金曜日

Scala Tips / Either (10) - 二項演算, AND

Rightを成功とする、成功/失敗文脈におけるEitherに対する二項演算です。

今回は、「Either:AND」×「値:任意の関数で計算」を考えます。

EitherのANDは以下の演算になります。

EitherのAND
lhsrhs結果Rightの値Leftの値
RightRightRight二項演算-
RightLeftLeft-rhs
LeftRightLeft-lhs
LeftLeftLeft-二項演算

値に対する二項演算は、lhs/rhsともRightだった場合と、Leftだった場合があります。

値に対する二項演算は、以下のものが考えられます。

lhs
lhs側を使う
rhs
rhs側を使う
  • f(lhs, rhs) :: 任意の関数で計算
  • lhs |+| rhs :: Monoidで計算
  • lhs <+> rhs :: Plusで計算

値に対する二項演算は以下の組合せとします。

lhs/rhsともRight
任意の関数で計算
lhs/rhsともLeft
lhs側を使う

(分類の基準)

Java風

if式を使って、4つの場合を記述します。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  if (e1.isRight && e2.isRight) {
    Right(f(e1.right.get, e2.right.get)) // Rightの二項計算
  } else if (e1.isRight && e2.isLeft) {
    e2
  } else if (e1.isLeft && e2.isRight) {
    e1
  } else { // e1.is Left && e2.isLeft
    e1 // Leftの二項演算
  }
}

Scala風

match式を使って、4つの場合を記述します。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e1 match {
    case Right(e1r) => e2 match {
      case Right(e2r) => Right(f(e1r, e2r)) // Rightの二項計算
      case Left(_) => e2
    }
    case Left(_) => e1 // Leftの二項演算
  }
}

match式のネストが気に入らない場合は以下のようにすればネストしない方式で記述することもできます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  (e1, e2) match {
    case (Right(e1r), Right(e2r)) => Right(f(e1r, e2r)) // Rightの二項計算
    case (Right(_), Left(_)) => e2
    case (Left(_), Right(_)) => e1
    case (Left(_), Left(_)) => e1 // Leftの二項計算
  }
}

後者(Tuple方式)は、Tupleを導入しているのとパターンマッチングの回数が増えるので性能的には不利ですが、プログラムの見通しはよくなります。フレームワークで使う場合には性能重視で前者(ネスト方式)、アプリケーションで使う場合には可読性重視で後者(Tuple方式)という選択も考えられます。

Scala

RightProjectionのflatMapメソッドとmapメソッドを使うのがScala的なコーディングです。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e1.right.flatMap(e1r => e2.right.map(e2r => f(e1r, e2r)))
}

「lhs/rhsともLeftの場合はlhs側を使う」という選択を行うと、失敗の文脈の処理をflatMapメソッドに任せることができるようになります。このため、アプリケーションロジックはflatMapに渡す関数のみを考えればよいわけです。

for

for式を使うと2つのEitherに対する二項演算を簡潔に記述することができます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  for {
    e1r <- e1.right
    e2r <- e2.right
  } yield f(e1r, e2r)
}

for式はモナドに対する文法糖衣で、実際の動きは前述の以下のものと同じです。

e1.right.flatMap(e1r => e2.right.map(e2r => f(e1r, e2r)))

for式を使うと簡潔に記述できるのでうまく活用したいですね。

Scalaz

Scalazでは、RightProjectionだけではなくEitherも成功/失敗文脈のモナドとして使えるのと、flatMapメソッドとして>>=メソッドを使うことができるので、以下のようになります。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e1 >>= (e1r => e2.map(e2r => f(e1r, e2r)))
}
for

for式でもrightメソッドでRightProjectionを取り出す処理は省略できます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  for {
    e1r <- e1
    e2r <- e2
  } yield f(e1r, e2r)
}
Applicative Functor

Scalzでは、Applicative Functorを使って、2つ(またはそれ以上)のEitherに対して二項演算(N項演算)することができます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  (e1 |@| e2)(f)
}

e1とe2が共にRightの場合、関数fの第1引数にe1(Right)の値、第2引数e2(Right)の値を適用して評価し、その結果をRightに詰めて返すという動作をします。

ノート

Eitherを純粋な意味で直和(disjoint union)、選択(choice)として使う場合はLeftの場合もRightの場合も値に対する二項演算を考えることになります。

しかし、ここではEitherを成功/失敗文脈として使うケースに絞っています。この場合には、アプリケーションロジックは成功文脈の上で実行することになり、成功文脈であるRight側が軸となります。

それに対して失敗文脈であるLeftは定型的な処理で簡単に流すこと、理想的にはアプリケーション側に処理を意識させないことが重要になります。その目的で、今回は「lhs/rhsともLeftの場合はlhs側を使う」、すなわちエラーが発生した時点でエラーモードになって後の演算は行わないという扱いにしました。これは、Eitherに対する二項演算という観点ではLeftを偽とする短絡評価AND、値に対する二項演算という観点では短絡評価ORの動きともいえます。

flatMap

Either (2) - flatMapでは、成功の文脈と失敗の文脈を切り替える目的でflatMapメソッドを使用しました。

今回は、成功の文脈と失敗の文脈の切替えは行っていませんが、flatMapを使っています。つまり、flatMapには文脈を切り替える以外の利用方法があるということですね。

flatMapには、2つのモナドを結合(join)するという重要な機能があります。

RightProjection(Scalazの場合はEither本体も同様)のflatMapメソッドは以下のような実装になっています。

def flatMap[AA >: A, Y](f: B => Either[AA, Y]) = e match {
      case Left(a) => Left(a)
      case Right(b) => f(b)
    }

RightProjectionの結合ロジックは、(1)Leftだったら何もせずそのままLeftを返す(関数fが作るはずのEitherは捨てる)、(2)Rightだったら関数fに新しいEitherを作ってもらったものをそのまま返す(自分自身は捨てる)、となります。このようにして、どちらかを捨てるというロジックで2つのEitherを結合します。この結合ロジックがアプリケーションロジックの意図と同じ場合には、flatMapメソッドを利用することができるわけです。

「lhs/rhsともLeftの場合はlhs側を使う」という選択は、RightProjection#flatMapメソッドの動きを意識してものでした。

Applicative Functor

Applicative Functorに関する話題は次回に説明する予定です。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿