2012年4月12日木曜日

Scala Tips / Either (21) - Bifunctor

EitherをOption的な成功/失敗の文脈の用途での使い方を中心に見てきました。

Eitherの本来の意味は直和であり、プログラミング的には選択(choice)を表現するオブジェクトです。

成功/失敗の文脈では、成功側と失敗側が非対称でしたが、左側と右側を対象に扱うのが本来のEitherらしい使い方と言えます。

本来のEitherらしい使い方として、ScalazのBifunctorを試してみました。

以下では、ちょっと人工的な例ですが:

  • (処理1) Either[Float, Int]をEither[Double, Double]に変換。
  • (処理2) Either[Double, Double]をEither[String, String]に変換。String変換時にEitherがRightの場合は"Right:"、Leftの場合は"Left:"の文字列を文字列の前に連結する。
  • (処理3) Either[String, String]からStringを取り出す。

のシーケンスによる関数を書いてみます。

(分類の基準)

Java風

if式を用いて(処理1)、(処理2)、(処理3)を順に処理します。ifRightの判定とright.getやleft.getの取り出しが泣き別れになってしまうのが、あまりよい感触ではありません。プログラム全体もごちゃっとした感じで可読性も低いです。

def f(a: Either[Float, Int]): String = {
  val b = if (a.isRight) Right(a.right.get.toDouble)
          else Left(a.left.get.toDouble)
  val c = if (b.isRight) Right("Right: " + b.right.get)
          else Left("Left: " + b.left.get)
  if (c.isRight) c.right.get
  else c.left.get
}

Scala風

match式を使うと以下のようになります。こちらの方が綺麗です。ただ、冗長な感じは否めません。

def f(a: Either[Float, Int]): String = {
  val b = a match {
    case Right(x) => Right(x.toDouble)
    case Left(x) => Left(x.toDouble)
  }
  val c = b match {
    case Right(x) => Right("Right: " + x)
    case Left(x) => Left("Left: " + x)
  }
  c match {
    case Right(x) => x
    case Left(x) => x
  }
}

Scala

Scala的にはEitherのfoldメソッドを使って書くのがよい感じです。簡潔で可読性もよくなります。

def f(a: Either[Float, Int]): String = {
  val b = a.fold(x => Left(x.toDouble), x => Right(x.toDouble))
  val c = a.fold(x => Left("Left: " + x), x => Right("Right: " + x))
  c.fold(identity, identity)
}

「c.fold(identity, identity)」のidentityは、引数と同じ値を返す関数で、「x => x」と同等の動きをします。どちらでもよいのですが、ここではidentityを使ってみました。

Scalaz

ScalazではBifunctorという、2つの要素から構成されるオブジェクトに対して、それぞれの要素を処理する関数を同時に記述できる機能を提供しています。EitherはLeftとRightの2つの要素を持っているので、Bifunctorを適用できます。

Bifunctorでは、左側の要素を処理する関数を「<-:」、右側の要素を処理する関数を「:->」で指定します。

今回の課題をBifunctorで記述すると以下のようになります。Bifunctorはモナディックな動きをするので、EitherからEitherへの変換になります。Eitherから最後に値を取り出す処理はfoldメソッドを用います。

def f(a: Either[Float, Int]): String = {
  val b = ((_: Float).toDouble) <-: a :-> ((_: Int).toDouble)
  val c = ("Left: " + _) <-: b :-> ("Right: " + _)
  c.fold(identity, identity)
}

プログラムの動きが分かりやすいように、途中結果を格納する変数に型を明記すると以下のようになります。

def f(a: Either[Float, Int]): String = {
  val b: Either[Double, Double] =
    ((_: Float).toDouble) <-: a :-> ((_: Int).toDouble)
  val c: Either[String, String] =
    ("Left: " + _) <-: b :-> ("Right: " + _)
  c.fold(identity, identity)
}

Bifunctorは以下のように連結することができます。このようにすると、左側右側の動きが分かりやすくなります。

def f(a: Either[Float, Int]): String = {
  ("Left: " + _) <-: ((_: Float).toDouble) <-: a :-> ((_: Int).toDouble) :-> ("Right: " + _) fold(identity, identity)
}

ただ、左右を連結していくと長くなってしまうのが難点です。以下のように各段の処理を関数化して、これをつなげていくと全体の流れが見えやすくなります。

def f(a: Either[Float, Int]): String = {
  def lds = "Left: " + (_: Double)
  def lfd = (_: Float).toDouble
  def rds = "Right: " + (_: Double)
  def rfd = (_: Int).toDouble
  val b = lds <-: lfd <-: a :-> rfd :-> rds
  b.fold(identity, identity)
}

ノート

Bifunctorは面白い機能で、Eitherでも便利に使えそうですが、Eitherに関しては「Scala」で使用したfoldメソッドを使う方法が処理も簡潔で意図も明確なのでよさそうです。

Bifunctorの場合は、Either以外にTuple2やValidation、さらにjava.util.Map.Entryも対象となっています。この他にも、型クラスインスタンスを定義すれば2つの要素を持つ任意のオブジェクトを処理対象にすることが可能です。

つまり、2つの要素を持つオブジェクトに対する共通処理を記述する場合にはBifunctorを用いる意味が出てきます。

たとえば、今回の例を拡張して以下のような関数を定義します。型パラメータを用いて、Bifunctorのオブジェクトを処理対象に指定しています。(Bifunctorには値を取り出す機能がないので、Either#foldメソッドで行っていた値を取り出す処理は省いています。)

def f[M[_, _]: Bifunctor](a: M[Float, Int]): M[String, String] = {
  def lds = "Left: " + (_: Double)
  def lfd = (_: Float).toDouble
  def rds = "Right: " + (_: Double)
  def rfd = (_: Int).toDouble
  lds <-: lfd <-: a :-> rfd :-> rds
}

EitherのRightとLeftに適用すると、以下のようになります。

scala> f(1.right[Float])
res16: Either[String,String] = Right(Right: 1.0)

scala> f(1.0F.left[Int])
res18: Either[String,String] = Left(Left: 1.0)

Tupleに適用すると、以下のようになります。

scala> f((1.0F, 1))
res19: (String, String) = (Left: 1.0,Right: 1.0)

ScalaのMapは、Tupleに対するIteratorでもあるので、以下のようにmapメソッドを用いて適用することができます。

scala> Map(1.0F -> 1, 2.0F -> 2).map(f(_))
res34: scala.collection.immutable.Map[String,String] = Map(Left: 1.0 -> Right: 1.0, Left: 2.0 -> Right: 2.0)

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿