2012年2月29日水曜日

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

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

今回は、「Either:AND」×「値:Monoid」を考えます。

前回は、「Either:AND」×「値:任意の関数で計算」だったので、値の計算方法を「任意の関数で計算」から「Monoid」に変えたものになります。モノイド(Monoid)は、結合的な二項演算と単位元という性質を持つオブジェクトで、関数型プログラミングで頻出します。

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

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

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

lhs/rhsともRight
Monoid
lhs/rhsともLeft
lhs側を使う

Scala標準ライブラリではMonoidは提供されていないので、この組み合わせが可能なのはScalazの場合です。以下では、比較の目的でJava風、Scala風、Scalaの項では(Monoidではなく)Int型に対して「+」を適用するコードを示します。そして、Scalazの項でMonoidを使った実装を行います。

(分類の基準)

Java風

if式を使って、4つの場合を記述します。Scalaの標準クラスライブラリにはモノイドがないので、ここではInt型の引数に対する二項演算は「+」の決め打ちにします。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int]): Either[Throwable, Int] = {
  if (e1.isRight && e2.isRight) {
    Right(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つの場合を記述します。やはり、Int型に対する二項演算は「+」の決め打ちにします。

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

Tupleを用いてネストしない書き方の場合も、二項演算に「+」を決め打ちします。

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

Scala

RightProjectionのflatMapメソッドとmapメソッドを使うのがScala的なコーディングです。この場合も、二項演算に「+」を決め打ちします。

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

for式を使うと2つのEitherに対する二項演算を簡潔に記述することができます。この場合も、二項演算に「+」を決め打ちします。

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

Scalaz

Scalaの標準ライブラリではMonoidは提供されていません。ここまでの説明では、Monoidを使わずInt型に対する「+」を決め打ちで使用しました。

ScalazではMonoidを使うことができるので、Monoidを使って実装していきます。また、Int型だけでなくMonoidであるオブジェクトではすべて同じロジックを適用するので、引数の型を型パラメータTとしました。型パラメータTは、コンテキスト・バウンドを使って「T: Monoid」としていますが、これは型パラメータTがMonoid型であることを指定しています。

ScalazではMonoid同士の加算演算として、演算子「|+|」を用意しているので、これを使います。

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

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  e1 >>= (e1r => e2.map(e2r => e1r |+| e2r))
}

Scalazでは、多くのオブジェクトがMonoid型として定義されているので、Monoidに対するロジックをそのまま適用することができます。以下は実際に動いている様子です。

scala> f(1.right, 2.right)
res64: Either[Throwable,Int] = Right(3)

scala> f("one".right, "two".right)
res65: Either[Throwable,java.lang.String] = Right(onetwo)

scala> f(List(1, 2).right, List(3, 4).right)
res66: Either[Throwable,List[Int]] = Right(List(1, 2, 3, 4))

scala> f(true.right, true.right)
res69: Either[Throwable,Boolean] = Right(true)

scala> f(BigInt("1").right, BigInt("2").right)
res71: Either[Throwable,scala.math.BigInt] = Right(3)

scala> f((1, 2).right, (3, 4).right)
res74: Either[Throwable,(Int, Int)] = Right((4,6))

scala> f(Map(1 -> 10, 2 -> 20).right, Map(3 -> 30, 4 -> 40).right)
res75: Either[Throwable,scala.collection.immutable.Map[Int,Int]] = Right(Map(3 -> 30, 4 -> 40, 1 -> 10, 2 -> 20))
for

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

Monoid同士の加算演算として、オペレータ「|+|」を使います。

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  for {
    e1r <- e1
    e2r <- e2
  } yield e1r |+| e2r
}
Applicative Functor

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

Monoid同士の加算演算として、演算子「|+|」を使います。演算子「|+|」を使う場合には、「(e1 |@| e2)(f)」といった形でメソッド名のみを指定する省略形は使えないので、引数を指定する必要があります。

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  (e1 |@| e2)(_ |+| _)
}

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

ノート

モノイド(monoid)は数学の代数学由来の性質です。モノイドは数学の色々な場所で使われる重要な性質のようで、数学的な観点できちんとみていくと非常に難解です。モノイドというと耳慣れないせいもあってなんだか仰々しですが、そういう意味で仰々しいに足る理由はあるわけですね。

ただし、関数型プログラミングにおいては、それほど恐れることはありません。オブジェクト指向プログラミングにおけるIteratorやFactoryと同様にイディオム、パターンの名前と気楽に考えておくとよいでしょう。プログラミングのテクニックとしては難しいものではありません。

ただし、その応用範囲は非常に広く、関数型プログラミングをする上では必須のパターンということができます。

ScalazにおけるMonoidは、加算的な演算一般を意味し、演算子は本文にも出てきた「|+|」や数学記号の「⊹」を用います。本文では「|+|」を使って色々なオブジェクトに対して共通のロジックを適用することができました。

また、Monoidは単位元を持っているという性質もあり、この性質を用いたテクニックもあります。たとえば、「Option (8)」ではMonoidの単位元をデフォルト値として使用しました。

型クラスとコンテキスト・バウンド

Monoidバージョンのプログラムでは、Int型ではなくて、コンテキスト・バウンドの記述方法[T: Monoid]を用いてMonoid型のオブジェクトを処理対象として宣言しました。

Scala言語そのものは、型クラスという文法は持っていませんが、暗黙パラメタを使ったConceptパターンという手法で、型クラスの機能が実現可能になっています。Scalazはこの手法を用いた型クラスのクラスライブラリというわけです。

コンテキスト・バウンドを使わず、暗黙パラメタを使って、本文の処理を定義すると以下のようになります。

def f[T](e1: Either[Throwable, T], e2: Either[Throwable, T])(implicit m: Monoid[T]): Either[Throwable, T] = {
  (e1 |@| e2)(_ |+| _)
}

こちらが本来の書き方で、コンテキスト・バウンドはその文法糖衣です。どちらの書き方も意味は変わらないので好きな方を用いるとよいでしょう。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿