2012年9月28日金曜日

Scala Tips / Scala 2.10味見(15) - Try(8) Monad化

TryはすでにScalaモナドですが、Scalaz Monadにすることも大きな意味があります。TryモナドをさらにScalaz Monad化する効用について考えてみます。

以下では、Scalaのモナドを「モナド」、Scalazのモナドを「Monad」と書くことにします。

準備

Try(7) パイプライン・プログラミング」で使用した関数を使います。

関数fはInt型2つを取ってTryモナドを返します。パイプラインの部品として使用します。

def f(a: Int, b: Int): Try[Int] = {
  def divAB = a / b
  def plus1(x: Int) = x + 1
  def minus100DivB(x: Int) = Try(x / (b - 1))

  Try(divAB).map(plus1).flatMap(minus100DivB)
}

以下の関数gは関数fを直列に連結したものです。

def g(a: Int, b: Int) = {
  def f1(x: Int) = f(x, b)
  def f2(x: Int) = f(x, b - 1)
  def f3(x: Int) = f(x, b - 2)

  f1(a).flatMap(f2).flatMap(f3)
}

この関数をMonad対応化、すなわち型クラスMonadを処理対象にする修正を施していきます。

Scalaz Monad

Tryは、今のところScalazの対象外なので、自分でMonad化する必要があります。これは、型クラスMonadのインスタンスを以下のように定義すれば完了です。

implicit val tryInstance = new Monad[Try] {
  def point[A](a: => A) = Try(a)
  def bind[A, B](fa: Try[A])(f: A => Try[B]) = fa flatMap f  
}

TryをSxalaz Monad化したことによって、以下のようにScalazらしいパイプライン流のコーディングが可能になります。

def g(a: Int, b: Int) = {
  def f1(x: Int) = f(x, b)
  def f2(x: Int) = f(x, b - 1)
  def f3(x: Int) = f(x, b - 2)

  a |> f1 >>= f2 >>= f3
}

ロジックのパラメタ化

Monad化の効用を取り込む前段階として、関数gをハードコーディングされた関数fに依存するのではなく(Int, Int) => Try[Int]型の関数を引数に取るようにします。

def g(f: (Int, Int) => Try[Int], a: Int, b: Int) = {
  def f1(x: Int) = f(x, b)
  def f2(x: Int) = f(x, b - 1)
  def f3(x: Int) = f(x, b - 2)

  f1(a).flatMap(f2).flatMap(f3)
}

実行結果は以下になります。

scala> g(f, 1000, 3)
res1: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

scala> g(f, 1000, 4)
res2: scala.util.Try[Int] = Success(8)
改良点

前述の関数gはロジックは汎用的ですが引数と返り値の型をTryに固定しているためTry以外のパラメタは当然受け付けません。

たとえばOption[Int]を返す関数foがあるとします。

def fo(a: Int, b: Int): Option[Int] = {
  def divAB = a / b
  def plus1(x: Int) = x + 1
  def minus100DivB(x: Int) = Try(x / (b - 1))

  Try(divAB).map(plus1).flatMap(minus100DivB).toOption
}

関数gに関数foを適用すると以下のようにエラーになります。

scala> g(fo, 1000, 3)
<console>:23: error: type mismatch;
 found   : Option[Int]
 required: scala.util.Try[Int]
              g(fo, 1000, 3)
                ^

ロジックのMonad化

関数gでTryの代わりにMonadを処理対象することで関数gを汎用ロジック化したものが以下になります。Scala 2.10より高カインド関数を使う場合にはhigherKindsのフィーチャをimportすることになったので、その対応もしています。

import language.higherKinds

def g[A[_]: Monad](f: (Int, Int) => A[Int], a: Int, b: Int) = {
  def f1(x: Int) = f(x, b)
  def f2(x: Int) = f(x, b - 1)
  def f3(x: Int) = f(x, b - 2)

  f1(a).flatMap(f2).flatMap(f3)
}

flatMapメソッドの代わりにScalazらしくパイプライン的に記述すると以下になります。

import language.higherKinds

def g[A[_]: Monad](f: (Int, Int) => A[Int], a: Int, b: Int) = {
  def f1(x: Int) = f(x, b)
  def f2(x: Int) = f(x, b - 1)
  def f3(x: Int) = f(x, b - 2)

  a |> f1 >>= f2 >>= f3
}

この関数gにTryを返す関数fを適用すると以下になります。

scala> g(f, 1000, 3)
res5: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

scala> g(f, 1000, 4)
res6: scala.util.Try[Int] = Success(8)

さらにOptionを返す関数foを適用すると以下になります。

scala> g(fo, 1000, 3)
res8: Option[Int] = None

scala> g(fo, 1000, 4)
res9: Option[Int] = Some(8)

TryとOptionの共通項はScalazの型クラスMonadという点ですが、Monadに対する処理を行うように改良した関数gがどちらにも適用できました。

つまりTryでもOptionでも、さらには他のオブジェクトでも、Monadでありさえすればオブジェクトの型に依存せずロジックの再利用が可能になります。

これがMonad&型クラスの威力です。Scalaのモナドでも相当のことができますが、Scalaz Monadを活用するとよりプログラムのモジュール度を高め、モジュール間の疎結合、部品の再利用の促進を図ることができます。

またScalazの型クラスは元々のクラスに始めから定義されていなくても、今回Tryに施したようにアプリケーション側の都合で後付けで定義できることも、クラスインヘリタンスに対する大きなアドバンテージになっています。

for式

for式を使っているロジックも同様にMonad化の恩恵をうけることができます。

def g[A[_]: Monad](f: (Int, Int) => A[Int], a: Int, b: Int) = {
  def f1(x: Int) = f(x, b)
  def f2(x: Int) = f(x, b - 1)
  def f3(x: Int) = f(x, b - 2)

  for { 
    x <- f1(a)
    y <- f2(a)
    z <- f3(a)
  } yield x + y + z
}
アプリカティブ

Scalaz MonadはApplicativeでもあるので、Applicativeの操作を適用することができます。

def g[A[_]: Monad](f: (Int, Int) => A[Int], a: Int, b: Int) = {
  def f1(x: Int) = f(x, b)
  def f2(x: Int) = f(x, b - 1)
  def f3(x: Int) = f(x, b - 2)

  (f1(a) |@| f2(b) |@| f3(a))(_ + _ + _)
}

Applictiveになると色々な技が使えるようになるので、これもMonad化の効用となります。

諸元

  • Scala 2.10.0-M7
  • Scalaz 7.0.0-M2

Scalaz 7.0.0-M3は後で出てくる|@|周りでScalaz 6と挙動が違うらしいので、少し古い7.0.0-M2を使いました。結局|@|周りの挙動はScalaz 6のものに戻ったらしいので、本記事のコードは(7.0.0-M3でも動くと思いますが)Scalaz 7.0.0-M4でも有効だと思われます。

Scalaz 7.0.0-M3での|@|の話題は以下のページが参考になります。

0 件のコメント:

コメントを投稿