2015年3月2日月曜日

[scalaz-stream] シーケンス番号とエンドマーク

プロダクトの開発中にわりと難しい処理がscalaz-streamでうまくさばけたのでメモ。

近い将来Buzzwordになってきそうな「Functional Reactive Programming」ですが、monadicなstreaming系のアプローチが筋がよさそうということで、scalaz-streamを実プロダクト開発に導入して試行錯誤しているところです。大規模データ系のいろいろな処理に適用していますが、かなりいい感触を得ています。

そのような応用の一つである大規模メール配信処理で出てきた要件が以下のものです。

  • 大規模なデータ列を一つの固まりとしてストリーム上に流したい
  • 事前にデータ列の総量は分からない
  • データ規模が大きいので復数パケットに分割する必要がある
  • 性能向上のため、可能な範囲で1パケットに復数データ列を格納したい

ネットワーク系のプログラムではわりとよく出てくる処理だと思います。

やろうとしていることはそれほど難しくないのですが、プログラムを組んでみると思いの外、複雑なものになってしまうといった系統の処理です。

こういった処理をいかに簡単に書くことができるようにするのかというのがプログラミング・モデルの重要なテーマの一つです。OOPであれば専用フレームワーク的なものを用意する感じですが、FPの場合はComposableな関数のCompositionでさばいてみたいところです。

サンプルロジック

上記の要件をscalaz-streamで実現することができるかを検証するためにサンプルプログラムをつくってみました。

以下の要件を実装しています。

  • 事前にすべてデータが読み込まれていることは前提としない
  • 3データごとに1つのパケットに集約
  • シーケンス番号をつける
  • 最終パケットにエンドマークをつける

scalaz-streamのProcessモナドを使用しています。

package sample

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

object Main {
  case class Packet(seqno: Int, end: Boolean, content: String)

  val source: Process[Task, Int] = {
    Process.range(0, 10)
  }

  def sink: Sink[Task, Packet] = {
    io.channel((a: Packet) => Task.delay { println(a) })
  }

  def main(args: Array[String]) {
    execute(source, sink)
  }

  def execute(source: Process[Task, Int], sink: Sink[Task, Packet]) {
    import process1._
    val pipeline = source.
      chunk(3).
      pipe(zipWithNext).
      pipe(zipWithIndex).
      map(toPacket).
      to(sink)
    val task = pipeline.run
    task.run
  }

  def toPacket(x: ((Seq[Int], Option[Seq[Int]]), Int)): Packet = {
    val ((current, next), index) = x
    val content = current.mkString("-")
    Packet(index + 1, next.isEmpty, content)
  }
}
Source

ストリームの起点となるProcessモナドをSourceと呼びます。

例題なのでProcess#range関数で0から10の数値のストリームを生成しています。

val source: Process[Task, Int] = {
    Process.range(0, 10)
  }
Sink

ストリームの終端となるProcessモナドをSinkと呼びます。

例題なのでscalaz.stream.io#channel関数を使って、コンソール出力するSinkを定義しています。

def sink: Sink[Task, Packet] = {
    io.channel((a: Packet) => Task.delay { println(a) })
  }
ストリームの構築

SourceとSinkはサンプル用のものなので、ここからが本題です。

ストリームの構築は、以下のようにProcessモナドが提供するコンビネータをつないでいく形になります。

FunctorやMonadが定義している基本コンビネータであるmapやflatMap以外にProcessモナドが提供するpipeといったコンビネータを多用することになるのが特徴です。

val pipeline = source.
      chunk(3).
      pipe(zipWithNext).
      pipe(zipWithIndex).
      map(toPacket).
      to(sink)

ストリームを処理するパイプラインはSourceから始まってtoコンビネータで指定するSinkが終端になります。途中、chunk、pipe、mapの各メソッド/コンビネータがパイプラインを構成しています。

chunk

ストリーム内を流れるデータを1つのチャンクにまとめる処理はchunkメソッドを使います。チャンクにまとめる個数の3を引数に指定します。

ストリームにはsourceからデータであるIntが流れてきますが、これがSeq[Int]に変換されます。

pipe

pipeコンビネータはProcessモナドをストリーム処理のパイプライン部品として埋め込みます。

ここではscalaz.stream.process1にある以下の関数を使って生成したProcessモナドをパイプラインに埋め込んでいます。

  • zipWithNext
  • zipWithIndex

zipWithNext関数が返すProcessモナドは、処理中のデータに加えてその次のデータをOptionとして渡してきます。処理中のデータが最終データの場合は、「その次のデータ」はNoneになります。これを使用すると、次のデータの有無、次のデータの内容によって処理を変更することができるわけです。

ここまでの処理でストリームからSeq[Int]が流れてきますが、この段でTuple2[Seq[Int], Option[Seq[Int]]]に変換されます。

zipWithIndex関数が返すProcessモナドは、処理中のデータにインデックスをつけます。

ここまでの処理でストリームからSeq[Int]が流れてきますが、この段でTuple2[Tuple2[Seq[Int], Option[Seq[Int]]], Int]変換されます。

map

mapコンビネータで以下のtoPacket関数をパイプラインに組み込んでいます。

def toPacket(x: ((Seq[Int], Option[Seq[Int]]), Int)): Packet = {
    val ((current, next), index) = x
    val content = current.mkString("-")
    Packet(index + 1, next.isEmpty, content)
  }

ここまでの処理でストリームからTuple2[Tuple2[Seq[Int], Option[Seq[Int]]], Int]が流れてきますが、この段でPacketに変換されます。

toPacket関数の処理は簡単でストリームから流れてきた情報を元に:

インデックス
ストリーム上の情報は0起点なので1起点に変換
エンドマーク
Nextがない場合は最終パケットなのでtrue

の情報をPacketに設定しています。

サンプルコード内で自前のアルゴリズムらしい物を書いているはここだけです。

ストリームの実行

ストリームの実行はrunメソッドで行います。

今回はTaskモナドを使う指定をしているので、ProcessモナドのrunメソッドではTaskモナドが返ってきます。

Taskモナドのrunメソッドを実行するとストリームが動作します。

val task = pipeline.run
    task.run

実行

実行とすると以下の結果が出力されました。

10個のデータ列が4つのパケット列として出力され、最後のパケットにはエンドマークがついています。

Packet(1,false,0-1-2)
Packet(2,false,3-4-5)
Packet(3,false,6-7-8)
Packet(4,true,9)

受信側の処理

このパケット列を受け取った側の処理としては:

  • エンドマークがついているパケットまでパケットを読み込む
  • シーケンス番号を監視して、欠損があれば再送を依頼する

というような処理になるかと思います。

こういった処理をscalaz-streamのProcessモナドで実装可能か、というのも面白そうなテーマなので機会があれば試してみたいと思います。

まとめ

大規模データのパケット分割処理ですが、自前のロジックはtoPacket関数のものぐらいで、後はscalaz-streamの用意する部品を組み合わせるだけで構築できました。

パケット分割や復元は自分でロジックを組むとそれなりにバグが出る所なので、既存部品を組み合わせるだけで、ほとんどの処理が組み上がって、最後の仕上げだけ自前のロジックを差し込むことができるのはほんとうに楽です。

また、OOP的なフレームワークだと:

  • パイプラインの構成をXMLなどの外部DSLで定義
  • 自前ロジックをパイプラインに組み込むためのボイラープレイト作成

といった作業が必要になるので、それなりに大変です。

それに対して、scalaz-streamの方式はmonadic programmingの作法に則っていれば通常の関数型プログラミングでOKなのが非常に使いやすいところです。具体的にはOOP的なフレームワークアプローチに対して以下のメリットがあります。

  • パイプラインを外部DSLで構築すると、型安全でなくなる、デバッグがしづらいといった問題も出るが、いずれの問題もなくなる。
  • 自前ロジックを通常の関数で記述すればよく、特別なボイラープレイトのコードは必要ない。
いいことずくめのようですが:

  • monadic programmingの習得
  • processモナドの理解

といった難点があるので、これはこれで一定のハードルがあります。

こういったハードルをcoding idiomやdesin patternといった技法でクリアすることができれば、大規模データ処理にはなかなか有力なアプローチだと思います。

注意

本稿の「大規模」は処理対象としてメモリに一度に乗らない規模のデータ量を指していて、最大1GB程度のものを想定しています。

これを超えてくるデータの処理はHadoopやSpark的な並列分散処理が必要になってくるので、本稿のスコープ外です。このような並列分散処理もSparkのRDDといったものを使ったmonadic programmingが有力と思うので、いずれ取り上げてみたいと思います。

諸元

  • Scala 2.10.3
  • Scalaz 7.1.0
  • Scalaz-stream 0.6a

2015年2月16日月曜日

Scalaz 7.1の非互換項目

プロダクトで使っているScalazをscalaz 7.0.6からScalaz 7.1に上げたわけですが(Operational Monad)、その際に判明したScalaz 7.1の非互換というか仕様変更についてのまとめです。

非互換対応している時に気付いたものが中心なので、仕様変更項目の網羅にはなっていません。(公式仕様変更項目)

ここに出ている項目以外は、スムーズに対応できました。(他の非互換項目にも遭遇していると思いますが、記憶に残らないレベル)

Validation

Scalaz 7.1からValidationがMonadではなくなってしまいました。deprecatedのコメント「flatMap does not accumulate errors, use `scalaz.\/` or `import scalaz.Validation.FlatMap._` instead」からすると、エラーをMonoidとして蓄積する処理がモナド則を破る、ということが判明したのではないかと思われます。

このことでエラー処理戦略のバランスが少し変わってしまいました。

このことを加味した上でのエラー処理戦略に関する最新の状況は以下がよくまとまっていると思います。(この記事は2014年2月のものなので(Scalaz 7.1のリリースは2014年8月)、リリース版に対応した記事ではないと思いますが、ValidationがMonadでないことが前提になっているので、7.1の開発バージョンで仕様を先取りした記事と思われます。)

ValidationをMonadとして使っていたコードに対する当面の回避策としては:

import scalaz.Validation.FlatMap._

として互換機能を有効にすることですが、本格対応としては

"10".parseInt.disjunction

といった感じで「\/」(disjunction)モナドに変換するのがひとつの形です。

Validationは引き続きApplicativeとしては有効なので:

("10".parseInt.toValidationNel |@| "20".parseInt.toValidationNel)(_ + _)

といった形を基本線に、for式で使いたい場合はdisjunctionメソッドで\/モナドに変換して使う戦略となりそうです。

for {
  a <- "10".parseInt.disjunction
  b <- "20".parseInt.disjunction
} yield a + b

ただ、Validationの価値はかなり下がったと思うので、エラー処理戦略は\/モナドを軸に考えて、エラーの積算が必要な処理のみValidationで補完というバランスがよさそうに思います。

具体的には以下のようなイメージです。

for {
  a <- ("10".parseInt.toValidationNel |@| "20".parseInt.toValidationNel)(_ + _).disjunction
+ _).disjunction // エラーの積算が必要な処理をValidation + Applicativeで記述し、Disjunctionに変換
  b <- "30".parseInt.disjunction // for式内は\/モナドで揃える
} yield a + b

Tagged type

以下のページにある通り「Scalaz 7.1 以降は明示的にタグを unwrap しなければいけなくなった」ため、かなり使いにくくなってしまいました。

元々Scala言語仕様のぎりぎりの所を使っている機能だと思うので、将来にわたっての安全性(Scalaのバージョンが上がると動かなくなる等)と利便性のバランスを考えるとプロダクションコードでは使いにくくなりました。

OptionやBooleanの振舞いをカスタマイズするのに便利だったのでちょっと残念。

PromiseとFuture

Scalaz 7.1では、Promiseモナドが非推奨になっていて、代わり(?)にFutureモナドが追加されているようです。Futureモナドは、Scalaの基本機能であるscala.concurrent.FutureをScalazのMonad化したものです。

Operational Monad」では「scala.concurrent.FutureはScalaz 7.1.0的にはモナドとして定義されていない」と書きましたが、間違いだったので訂正します。

Scala Tips / Scala 2.10味見(4) - Future(2)」で書いた通り、ほとんど同じ機能であるPromiseとFutureが(Scalaz上の扱いが異なる状態(PromiseはMonad、Futureは非Monad)で)並立している状況が懸念点だったので、この点が払拭されたのは喜ばしいと思います。

今後は安心してFutureを軸に並列プログラミングの戦略を考えていくことができます。

また、並列プログラミングの部品となるモナドとしてはscalaz.concurrent.Taskは引き続き有効です。(注: scalaz.concurrent.Taskが内部的に使用している部品にscalaz.concurrent.Futureがありますが、scala.concurrent.Futureと紛らわしいこともありここでは触れないことにします。本稿ではscala.concurrent.FutureをFutureと呼びます。)

FutureとTaskはざっくりいうと以下のような性質の違いがあると理解しています。

Future
スレッドを積極的に生成して処理を行う
Task
スレッドはできるだけ生成しない戦略

I/Oバウンドの処理はFutureが有効です。

一方CPUバウンドの処理に関しては、100コア級のメニーコア時代になればFutureを使うのがよさそうですが、せいぜい4コア級、AWSのローエンドは1コアの状況では、Futureで細粒度の並列プログラミングをするのは時期尚早の感じです。そういう意味で、当面はTaskもFutureと同様の重み付けの選択肢と考えています。

諸元

  • Scala 2.10.3
  • Scalaz 7.1.0

2015年1月14日水曜日

Operationalモナドの合成

ScalazでOperationalモナドが簡単に使えることが分かったので、次の段階としてxuweiさんの書かれた「CoproductとInjectを使ったFree Monadの合成とExtensible Effects」を参考に、Operationalモナド(Freeモナド)の合成を試してみました。

CoproductとInjectについて、理論や動作メカニズムを把握できていないのでほぼ写経の状態です。

ConsoleService

まず最初は前回作成したConsoleServiceを合成可能にチューニングします。

プログラムの見通しをよくするため今回のテーマに関係しないinterpreterTaskとrunTaskは省いています。

package sample

import scala.language.higherKinds
import scalaz._, Scalaz._

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 = 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)
  }

  class ConsolePart[F[_]](implicit I: Inject[ConsoleOperation, F]) {
    def printLine(msg: String): Free.FreeC[F, Unit] = Free.liftFC(I.inj(PrintLine(msg)))
    def readLine: Free.FreeC[F, String] = Free.liftFC(I.inj(ReadLine))
  }

  object ConsolePart {
    implicit def instance[F[_]](implicit I: Inject[ConsoleOperation, F]): ConsolePart[F] = new ConsolePart[F]
  }
}

追加したのは、scalaz.InjectによってOperationalモナドの合成を行うための受け皿となるクラスConsolePartとそのコンパニオンオブジェクトです。

class ConsolePart[F[_]](implicit I: Inject[ConsoleOperation, F]) {
    def printLine(msg: String): Free.FreeC[F, Unit] = Free.liftFC(I.inj(PrintLine(msg)))
    def readLine: Free.FreeC[F, String] = Free.liftFC(I.inj(ReadLine))
  }

  object ConsolePart {
    implicit def instance[F[_]](implicit I: Inject[ConsoleOperation, F]): ConsolePart[F] = new ConsolePart[F]
  }

ConsoleServiceオブジェクト本体に定義されているprintLine関数、readLine関数の合成可能版を定義しています。

暗黙パラメタで渡されてきたInjectのinjメソッドを使ってcase class(PrintLine, ReadLine)をインジェクト可能にしたもの(?)をFree.liftFCでOperationalモナド化しています。

コンパニオンオブジェクトの方にはおまじないの暗黙変換関数を定義しています。

AuthService

次にConsoleServiceに合成して使用するOperationalモナドとしてAuthOperationをAuthServiceに定義します。

Operationalモナド化のターゲットとなる1階カインド型のトレイトとしてAuthOperationを、具体的なコマンドとなるcase classとしてLoginを定義してます。

インタープリターの実行エンジンはinterpreterとして定義しています。

その後ろにあるAuthPartクラスとコンパニオンオブジェクトがOperationalモナド合成のために必要な定義です。

package sample

import scala.language.higherKinds
import scalaz._, Scalaz._

object AuthService {
  sealed trait AuthOperation[_]
  case class Login(user: String, password: String) extends AuthOperation[Unit]

  val interpreter = new (AuthOperation ~> Id) {
    def apply[T](c: AuthOperation[T]): Id[T] = {
      c match {
        case Login(login, password) => println(s"$login:$password")
      }
    }
  }

  class AuthPart[F[_]](implicit I: Inject[AuthOperation, F]) {
    def login(user: String, password: String): Free.FreeC[F, Unit] = Free.liftFC(I.inj(Login(user, password)))
  }

  object AuthPart {
    implicit def instance[F[_]](implicit I: Inject[AuthOperation, F]): AuthPart[F] = new AuthPart[F]
  }
}

この例では、プログラムの意図を見やすくするためAuthService本体ではユーティリティ関数を定義していませんが、実応用時にはAuthService本体とAuthPartの両方で定義して、合成非使用時、合成使用時のどちらのケースでも動作可能にしておくことになります。

Utility

Operationalモナド合成のために必要なユーティリティ関数orを定義します。これは「CoproductとInjectを使ったFree Monadの合成とExtensible Effects」にある関数をそのまま持ってきています。

package sample

import scala.language.higherKinds
import scalaz._, Scalaz._

object Utility {
  def or[F[_], H[_], G[_]](
    fg: F ~> G, hg: H ~> G
  ): ({ type f[x] = Coproduct[F, H, x]})#f ~> G = {
    new (({type f[x] = Coproduct[F,H,x]})#f ~> G) {
      def apply[A](c: Coproduct[F,H,A]): G[A] = c.run match {
        case -\/(fa) => fg(fa)
        case \/-(ha) => hg(ha)
      }
    }
  }
}

2つの自然変換(NaturalTransformation)を合成した自然変換を作成する関数のようです。

積(product)の双対であるCoproduct(余積)は直和と同等ということらしく、型レベルのEither(Disjoint union)と考えてよさそうです。Scalazの内部実装もScalazの「\/」を使っています。

合成後の自然変換はCoproductとして渡されてきた型が合成された2つの自然変換のどちらに該当するかを判定して、該当する自然変換によって変換を行う、というロジックだと思います。

ConsoleAuthService

さていよいよ合成です。

ConsoleServiceとAuthServiceを合成したConsoleAuthServiceを定義してみました。

package sample

import scalaz._, Scalaz._

object ConsoleAuthService {
  import ConsoleService._, AuthService._

  type ConsoleAuth[A] = Coproduct[ConsoleOperation, AuthOperation, A]
  val interpreter = Utility.or(ConsoleService.interpreter, AuthService.interpreter)
  def run[T](f: Free.FreeC[ConsoleAuth, T]) = Free.runFC(f)(interpreter)
}

定義はとても簡単で以下の3行だけです。

  • CoproductによるOperationalモナドの合成
  • 実行エンジンの合成
  • runメソッド
CoproductによるOperationalモナドの合成

Coproductを使って2つのOperationalモナド(ConsoleOperation, AuthOperation)を合成します。

type ConsoleAuth[A] = Coproduct[ConsoleOperation, AuthOperation, A]

正確には、2つのOperationalモナドのいずれかを保持するCoproductを定義する、ということになろうかと思います。

実行エンジンの合成

Utilityのor関数でConsoleServiceの実行エンジンとAuthServiceの実行エンジンを合成します。実行エンジンは自然変換なので、汎用の自然変換の合成機能で合成することができます。

val interpreter = Utility.or(ConsoleService.interpreter, AuthService.interpreter)

意味的には、「2つのOperationalモナドのいずれかを保持するCoproduct」をターゲットの関手に変換する自然変換、ということになろうかと思います。

runメソッド

runメソッドでは、「2つのOperationalモナドをCoproductを使って合成したもの」をFreeモナド化したものをパラメタで受取り、「2つのOperationalモナドをCoproductを使って合成したもの」の実行エンジンをFree.runFC関数で適用して処理を実行しています。

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

App

2つのOperationalモナドを合成したConsoleAuthServiceの使用方法は以下になります。

package sample

import scala.language.higherKinds
import scalaz._, Scalaz._

object App {
  import ConsoleService.ConsolePart, AuthService.AuthPart

  def program[F[_]](implicit C: ConsolePart[F], A: AuthPart[F]) = {
    import C._, A._
    for {
      password <- readLine
      _ <- login("user", password)
    } yield ()
  }

  def main(args: Array[String]) {
    ConsoleAuthService.run(program)
  }
}

program関数の実装はConsoleServiceやAuthServiceを単独で使う場合と同様にfor式によるものですが以下の点が異なっています。

  • 暗黙パラメタとしてscalaz.Injectを使った合成用のクラスConsolePartとAuthPartを受け取っている。
  • ConsoleServiceとAuthServiceのユーティリティ関数として暗黙パラメタで受け取ったクラスのメソッドを使用している。

このあたりをおまじないとして考えると、ConsoleServiceやAuthServiceを単独で使う場合とほとんど変わらない手間で、ConsoleServiceとAuthServiceを合成したConsoleAuthServiceを使用できています。

まとめ

Scalaz 7.1.0でOperationalモナド(Freeモナド)の合成が簡単にできることが確認できました。

モナドを合成できるのもよいですが、モナドの実行エンジンも自然変換を使って簡単に合成できるのも好感触です。

今まではモナドは合成できないためモナド変換子(e.g. WriteT)を都度作るアプローチだったと思いますが、Operationalモナド/Freeモナドを使うことで合成によるアプローチを取ることも可能になったと理解してよいのかもしれません。

どこまで実用性があるかは未知数ですが、この検証も含みにアプリケーションやフレームワークで定義するモナドはOperationalモナドを第1候補に考えてみたいと思っています。

諸元

  • Scala 2.11.4
  • Scalaz 7.1.0

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モナド目的で、そろそろ上げておくのがよさそうです。

2014年7月28日月曜日

Scalaz勉強会しました

APPAREL CLOUDアパレルウェブが提供しているアパレル向けのクラウドサービスです。ボクが所属するEverforthの提供するEverforthプラットフォーム上に構築されています。
Everforthプラットフォームは、O2Oやオムニチャネル向けのクラウド・プラットフォームです。Everforthプラットフォームでは、サービスを実行するクラウド実行基盤と同時にO2Oやオムニチャネル・ビジネスをサービスに結び付けるための業務分析、システム分析を包含したサービス開発体系の提供も予定しています。
「サービス開発体系」は、オブジェクト指向ビジネス・モデリング、オブジェクト指向分析設計の技術をベースに、Everforthプラットフォームをターゲットとしたアプリケーション開発の方法論として整備していく予定です。
一般的なオブジェクト指向分析設計方法論が複雑で分かりづらいものになっているのは、どの業務分野、どの実装技術、どの実行基盤に対してもニュートラルな形で整備されているため、(1)汎用性を担保するために複雑化する、(2)ターゲットの業務分野、実装技術、実行基盤むけにカスタマイズが必要、という要因があると思います。
逆に、業務分野、実装技術、実行基盤を確定した上でカスタマイズすればかなりスリムなものにできるはずです。
Everforthサービス開発体系では、業務分野はO2O/オムニチャネル、(バックエンドの)実装技術はScalaによるOFP(Object-Functional Programming)、実行基盤はEverforthクラウドプラットフォームとすることで、この問題を解決しようとしています。

勉強会

Everforthサービス開発体系を業務に載せるためには、エンジニアの技術教育が非常に重要になってきます。
そこで上流のビジネス・モデリングからScalaでの実装にいたるまでの一連の技術の入門編を企画しました。これらの技術は連動しており、業務分析からオブジェクト指向分析/設計を経由してScalaでの実装まで一気通貫の体系になっています。
  • 実務者のためのかんたんScalaプログラミング
  • 実務者のためのかんたんScalaz(第1回)
  • 実務者のためのかんたんScala入出力プログラミング
  • 実務者のためのかんたんScala設計
  • 実務者のためのかんたんオブジェクト指向分析/設計
  • 実務者のためのかんたん業務分析
第1回は7月7日に札幌で開催しました。
札幌が東京と並んでEverforthの開発拠点になっているので、まず札幌で勉強会として開始しています。
今回は技術体系をざっくりおさらいするのが目的で、Eveforthプラットフォームに依存した部分もないので、オブジェクト指向技術やScalaの普及という目的もあり、一般の方も参加できる形にしています。

Scalaz

第1回「実務者のためのかんたんScalaz」の資料は以下になります。


最初のテーマとしてScalazを選んだのは、社内チャットでScalazについての質問があったのがきっかけになっていますが、関数型プログラミング導入の狙いの説明を最初にしておくのが有益ではないかという判断もあります。Scalazは「純粋」関数型プログラミングのためのライブラリなので、関数型プログラミングの方向性や特性をより際立った形で取り上げることができます。
かんたん?
今回の資料で「かんたん」なのは、スライドの右上に「かんたん」のマークがついている所で、以下の2つの要素です。
  • Scalazの便利機能
  • Monoid
それ以外は、なんとなく聞いておいてもらえばよい、というレベルの情報として考えています。
逆に「Monoid」についてはこの機会にプログラミングに取り入れて欲しい、という意図から「かんたん」の範囲に含めています。「Monoid」というととっつきにくく感じますが、プログラミングのイディオムとしては簡単で利用範囲の広い汎用テクニックであり、関数型プログラミングの考え方をマスターする起点にもなるという、一粒で二度も三度もおいしいものです。
Scalazを本格的に取り上げる場合は、Traversable・Foldable、Promise・Task、Free、Treeといった所もテーマにしたいのですが、今回は「かんたん」ということで断念しました。機会があればこの辺りも加味した、Object-Function Programmingという切り口でまとめてみたいと思います。

次回以降の予定

第2回は「実務者のためのかんたんScala設計」を札幌で予定しています。多分9月の初旬頃になると思います。
東京(or 横浜)でも準備ができしだい開催したいと思います。

2014年7月3日木曜日

関数型プログラミング技術マップ2014

来週の月曜(7月7日)に札幌で「実務者のためのかんたんScalaz」という勉強会を予定しています。
この勉強会は、クラウドアプリケーション開発の基本技術の情報展開を目的としたもので、以下の勉強会をシリーズで行っていく予定です。
  • 実務者のためのかんたんScalaプログラミング
  • 実務者のためのかんたんScala入出力プログラミング
  • 実務者のためのかんたんScala設計
  • 実務者のためのかんたんオブジェクト指向分析/設計
クラウドアプリケーションの開発技法を整備する上では、業務アプリケーション開発の基盤技術であるオブジェクト指向分析/設計、オブジェクト指向プログラミングの土台の上に関数型言語をどうのように接合していくのか、という点が焦点の一つと思います。
この議論を深めるためには、関数型言語の技術的な背景、文脈を整理することが必要ということで2012年に「関数型言語の技術マップ」を作成しました。
今回、勉強会向けにこの技術マップを改良してみました。
改良点は以下の2つです。
  • 純粋関数型言語の性質
  • モナドと圏論の位置付け

純粋関数型言語の性質

純粋関数型言語の性質として以下のものを参考に補足しました。
  • 不変 (immutable)
  • 副作用なし (no side effect)
  • 参照透過性 (referential transparency)
  • 置換モデル (substitution model)

モナドと圏論の位置付け

圏論の入門書を幾つかチェックしましたが、モナドについてはほとんど記載がなく、モナドは圏論の基本概念ではないらしいということが分かりました。
また、"圏論の中に(基本構成要素として?)モナドがあるのは違和感がある"というコメントもいただき、技術マップの中で圏論やモナドをどのように位置付けるかが懸案事項になっていました。
その後、色々と調べてみて分かったのですが、圏論については、種々存在する数学や情報科学の各分野を統一的に記述するための「普遍言語」と考えるのが適切のようです。
代数的構造だけでなく、プログラミング言語の構造や論理学も圏論で記述することが可能という関係になります。図にはこの理解を反映しました。
たとえば、位相空間の圏であるトポスで数理論理学のモデルを記述することができるようです。またプログラミング言語の圏としてHaskellのHask圏が有名です。
その上で「モナド(monad)」ですが、これは「プログラミング言語の圏で有効な概念」でこれを情報科学的に記述する時に圏論を用いる、という関係と考えるとよさそうです。「プログラミング言語の圏で有効な概念」なので、圏論を知らなくてもプログラミングに便利に使える概念のはずです。
圏論もモナドも難解な概念であるため、クラウドアプリケーションの開発者がどこまで理解しておく必要があるのか、という点が重要な論点になります。
この2つの概念を黒帯レベルで理解することが必須ということになると、現場への展開は事実上不可能になってしまいます。とはいえ全く知らないで良いということでもないと思うので、その線引が非常に重要になってきます。
勉強会では、ボクがこの辺りがよいのではと思っているラインについて、Scala言語での具体的なアプローチ方法も含めて、私案を展開したいと思います。

2014年6月30日月曜日

実務者のためのかんたんScalaz

一昨年からEverforthに参画してScalaを使ってとあるシステム開発を行いつつ、クラウドアプリケーションの開発技法について整備してきましたが、ある程度方向性が見えてきたので、札幌の開発部隊に情報展開するための勉強会を企画しました。

クラウドアプリケーションの開発は、ちょうど業務アプリケーション開発にオブジェクト指向技術が導入された時と同じようなパラダイムシフトが起きることになると予想されます。

新しいパラダイムには、難解な理論的な支柱が背景に控えているわけですが、難解な理論を正確に理解することが業務アプリケーション開発を行う必要条件というわけではないのは今も昔も同じです。

難しい部分はフレームワークで吸収したり、イディオム(公式)として覚えてしまう、という方法で多くのケースは対応できるでしょう。

実際のビジネスを展開する上では、新しいパラダイムの上で実案件をこなすことができるエンジニアの層を厚くすることが極めて重要です。

今回は、新しいパラダイムの全体像をつかみやすくすることを目的として、あえてエッジの技術であるScalazをテーマにしてみました。

せっかくなので、Everforth関係者だけでなく、この分野に興味をお持ちの一般のエンジニアの方も参加可能な形にしたいと思います。

平日の昼間なのと告知期間も短く、内容もマニアックなので、あまり外部参加者は多くないと予想されます。

このため、ATNDのようなイベント開催ツールは使わないことにします。興味のある方はお気軽にお越しください。

勉強会の内容

タイトル:

実務者のためのかんたんScalaz

概要:

難しい理論は抜きにしてScalazを用いた、クラウドアプリケーション開発に対応した関数型プログラミングを行うテクニックを解説します。

合わせて、クラウドアプリケーション開発の新しいプログラミング・パラダイムの全体像についての情報共有を行います。

勉強会の構成:

  • 前半(1:30〜3:30): 講義
  • 後半(4:00〜7:00): ハンズオン

場所:

  • 13:30〜17:00 札幌エルプラザの環境研修室1 (開場 13:00)
  • 17:00〜19:00 札幌カフェ5F

場所が連続して確保できなかったので途中で移動が入ります。ちょうどハンズオンの途中になりますが、このタイミングで参加終了しても大丈夫です。逆にハンズオンのみの参加も歓迎です。

環境:

  • コンセント(不明)
  • 無線LAN(不明)

持参頂くもの:

ハンズオンでScalaプログラミングをしていただきますので、以下の環境を整えておいてください。セットアップは、当日他の参加者に手伝ってもらうことができますが、各種モジュールのダウンロードに時間がかかることが予想されるので、必要なモジュールのダウンロードはしておくとよいと思います。

  • PC
  • Scalaコンパイラ
  • sbt
  • Scalaプログラムを編集するエディタ

参加資格:

Everforthの開発に参加しているエンジニア向けですが、一般のエンジニアの参加も歓迎です。

当日会場に直接おいでください。

懇親会:

Scala, Scalaz, 関数型プログラミング、クラウドアプリケーションについての情報交換もかねて懇親会を予定しています。ご都合のよい方はぜひご参加ください。

今後の予定

今回のScalazに続いて以下の勉強会を予定しています。

  • 実務者のためのかんたんScalaプログラミング
  • 実務者のためのかんたんScala入出力プログラミング
  • 実務者のためのかんたんScala設計
  • 実務者のためのかんたんオブジェクト指向分析/設計

シリーズを通して、クラウドアプリケーション開発の土台となる新しいプログラミング・パラダイムの概要をオブジェクト指向分析/設計からのトップダウン、関数型プログラミングからのボトムアップの両面から解説していきます。

全体像をつかむのと同時に、実案件にすぐに入れる基本技術(イディオムなど)の習得も目的としています。

シリーズの全体テーマは、以下のページにあるOFADについての2012年頃の考察を、実システム開発で実践した上でのフィードバックをベースに内容を深化させたものを、実務者向けの切り口で整備したものになります。