2015年1月8日木曜日

Operationalモナド

おぼろげな記憶ではFreeモナドが話題になったのが一昨々年、Operationalモナドは一昨年だったと思うので、いまさらの感もありますが、ScalazでOperationalモナドが簡単に使えることが判明したのでメモしておきます。

Freeモナドは、FunctorをMonad化することができるモナドですが、以下の性質を持ったインタープリターのDSLを簡単に作れることで知られています。

  • トランポリン化によりjoin演算時の再帰呼び出しによるスタックオーバーフローを回避
  • インタープリターの実行エンジンが疎結合になっており取替え可能
  • 副作用を実行エンジン側に分離(2015-01-14追記)

Operationlモナドは、Coyonedaにより型パラメータが1つの1階カインド型のデータ(Scala的にはcase classで実装。以下case class。)をFunctor化する事が可能な性質を利用して、case classから直接Freeモナド化したものです。

ざっくりいうとcase classでインタープリターのオペレーションを定義すれば、ここから簡単にインタープリターの(monadicな)DSLを作ることができるわけです。

プロダクトではScalaz 7.0.6を使っているのですが、このバージョンでOperationalモナドを使おうとするとかなり込み入った呪文を唱えないといけません。

Operationalモナドは非常に便利そうなんですが、呪文が必要になるとすると、あまり気軽に使えなくなってしまいます。

この問題がScalaz 7.1.0で解消されていることが判明したわけです。(Scalaz 7.1.0のリリースが昨年8月のようなのでいまさらですが。)

ConsoleService

Scalaz 7.1.0の機能を使用したOperationalモナドの実装は以下になります。

package sample

import scalaz._, Scalaz._
import scalaz.concurrent.Task

object ConsoleService {
  sealed trait ConsoleOperation[_]
  case class PrintLine(msg: String) extends ConsoleOperation[Unit]
  case object ReadLine extends ConsoleOperation[String]

  def printLine(msg: String) = Free.liftFC(PrintLine(msg))
  def readLine: Int = Free.liftFC(ReadLine)

  val interpreter = new (ConsoleOperation ~> Id) {
    def apply[T](c: ConsoleOperation[T]): Id[T] = {
      c match {
        case PrintLine(msg) => println(msg)
        case ReadLine => scala.io.StdIn.readLine()
      }
    }
  }

  def run[T](f: Free.FreeC[ConsoleOperation, T]): T = {
    Free.runFC(f)(interpreter)
  }

  val interpreterTask = new (ConsoleOperation ~> Task) {
    def apply[T](c: ConsoleOperation[T]): Task[T] = {
      Task.delay {
        c match {
          case PrintLine(msg) => println(msg)
          case ReadLine => scala.io.StdIn.readLine()
        }
      }
    }
  }

  def runTask[T](f: Free.FreeC[ConsoleOperation, T]): Task[T] = {
    Free.runFC(f)(interpreterTask)
  }
}
case classの定義

まずインタープリターのコマンドとなるcase classを定義します。

ここではコンソールに文字列を出力するPrintLineとコンソールから文字列を入力するReadLineを定義しています。この2つのcase classの親クラスとしてConsoleOperationトレイトを定義しています。このConsoleOperationトレイトが型パラメータを1つ取る構造になっているためCoyonedaによるOperationalモナド化が可能になっています。

定義時のイメージとしてはcase classの引数にコマンドの引数、case classの親クラスであるConsoleOperationの型パラメータにコマンドの返り値を定義します。

sealed trait ConsoleOperation[_]
  // PrintLine(msg: String): Unit
  case class PrintLine(msg: String) extends ConsoleOperation[Unit]
  // ReadLine(): String
  case object ReadLine extends ConsoleOperation[String]
ユーティリティ関数

次にFreeモナドとして使いやすくするためのユーティリティ関数を定義します。

定義は簡単で、scalaz.FreeのliftFC関数に(条件に合致した)case classを渡すだけです。

Scalaz 7.0.6ではこのliftFC関数がなかったために呪文が必要だったわけです。

def printLine(msg: String) = Free.liftFC(PrintLine(msg))
  def readLine: Int = Free.liftFC(ReadLine)
インタープリター

ConsoleServiceの利用時には、Freeモナドの内部構造としてPrintLineやReadLineの列が構築されますが、これを解釈して実行するインタープリターを定義します。

これはScalazのNaturalTransformation(自然変換)を用いて実装します。NaturalTransformation自体は1階カインド型間の変換(圏論的には関手間の射?)の機能ですが、ここではモナド間の変換に適用します。

まずConsoleOperationモナド(Operationalモナド化したもの)をIdモナドに変換する自然変換によるインタープリターの実行エンジンです。

val interpreter = new (ConsoleOperation ~> Id) {
    def apply[T](c: ConsoleOperation[T]): Id[T] = {
      c match {
        case PrintLine(msg) => println(msg)
        case ReadLine => scala.io.StdIn.readLine()
      }
    }
  }

Idモナドは実行結果をモナドにくるまず直接取得する時に使用するモナドです。

変換先としてIDモナドを指定することで、インタープリターの実行結果の値を取得できるようになります。

実行ユーティリティ

NaturalTransformationとして実装されたインタープリターはscalaz.FreeのrunFC関数で実行することができます。

Scalaz 7.0.6ではこのrunFC関数もなかったために、ここでも呪文が必要でした。

def run[T](f: Free.FreeC[ConsoleOperation, T]): T = {
    Free.runFC(f)(interpreter)
  }
Task版インタープリターと実行ユーティリティ

scalaz.concurrent.Taskは、scala.util.Tryとscala.concurrent.Futureの機能を併せ持ったようなモナドです。例外のキャッチ、遅延評価、並列実行といった目的で汎用的に使用することができます。

scala.util.Tryとscala.concurrent.FutureはScalaz 7.1.0的にはモナドとして定義されていないので、Monadicプログラミングをする上ではTaskの方が都合がよいです。

ここではFreeモナドConsoleOperationをTaskに変換するインタープリターを実装しました。正確にはインタープリターの実行を行う関数が束縛されたTaskが返ってきます。

実行はID版と同様にscalaz.FreeのrunFC関数を用います。

val interpreterTask = new (ConsoleOperation ~> Task) {
    def apply[T](c: ConsoleOperation[T]): Task[T] = {
      Task.delay {
        c match {
          case PrintLine(msg) => println(msg)
          case ReadLine => scala.io.StdIn.readLine()
        }
      }
    }
  }

  def runTask[T](f: Free.FreeC[ConsoleOperation, T]): Task[T] = {
    Free.runFC(f)(interpreterTask)
  }

Operationalモナドを使う

OperationalモナドによるConsoleServiceの使用方法は以下になります。

package sample

import ConsoleService._

object App {
  def main(args: Array[String]) {
    console_id
    console_task
  }

  def console_id {
    val program = for {
      msg <- readLine
      _ <- printLine(msg)
    } yield Unit
    run(program)
  }

  def console_task {
    val program = for {
      msg <- readLine
      _ <- printLine(msg)
    } yield msg
    val task = runTask(program)
    task.run
  }
}
IDモナド

console_idメソッドではConsoleServiceのrunメソッドを使用して、プログラムの実行を行います。IDモナドを使っているため、(処理を行う関数が束縛された)モナドが返されるのではなく処理が直接実行されます。

Taskモナド

console_taskメソッドではConsoleServiceのrunTaskメソッドを使用してプログラムの実行を行います。

runTaskメソッドは処理を行う関数が束縛されたTaskモナドを返すので、さらにrunメソッドでプログラムの実行を行います。

まとめ

Operationalモナドは敷居が高いので今まで使っていなかったのですが、Scalaz 7.1.0で簡単に利用できるようになっているのが分かりました。

プロダクトで使っているライブラリのバージョンを上げるのは勇気が要るところですが、Operationalモナド目的で、そろそろ上げておくのがよさそうです。

0 件のコメント:

コメントを投稿