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での|@|の話題は以下のページが参考になります。

2012年9月27日木曜日

Scala Tips / Scala 2.10味見(15) - Try(7) パイプライン・プログラミング

モナドを中心としたパイプライン指向のプログラミング・スタイルを本ブログではモナディック・プログラミングと呼んでいます。

モナディック・プログラミングでは、モナドを使ったパイプラインが幹で、これを束ねてプログラムを構築していきます。

Try(4) 基本フォームTry(5) 基本フォーム2Try(6) 終了方法でTryモナドを使ったパイプラインを構築方法を整理しました。

終了方法として「Tryのまま返す」を選択した場合、該当するパイプラインは、より大きなパイプラインの部品として使用することができます。

準備

Tryモナドを返す関数としてTry(6) 終了方法で導入した以下のものを使います。

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

直列

関数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)
}

正常系の実行結果は以下になります。

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

scala> g(1000, 5)
res92: scala.util.Try[Int] = Success(1)

異常系の実行結果は以下になります。

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

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

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

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

並列

複数のパイプラインの結果を持ち寄って最終結果を計算する処理にはfor式を使うのが定番です。

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)

  for { 
    x <- f1(a)
    y <- f2(a)
    z <- f3(a)
  } yield x + y + z
}

正常系の実行結果は以下になります。

scala> g(1000, 4)
res97: scala.util.Try[Int] = Success(751)

scala> g(1000, 5)
res98: scala.util.Try[Int] = Success(300)

異常系の実行結果は以下になります。

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

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

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

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

諸元

  • Scala 2.10.0-M7

2012年9月26日水曜日

Scala Tips / Scala 2.10味見(14) - Try(6) 終了方法

Scala 2.10味見(12) - Try(4) 基本フォーム」で説明した通り、Tryを使った処理は以下の4種類の終了方法が考えられます。

  • 副作用のある処理を行なって完了
  • Tryのまま返す
  • 値を返す
  • 値を返すか例外を投げる

副作用のある処理を行なって終了

前回に使った例題は、「副作用のある処理を行なって終了」のカテゴリになります。

def f(a: Int, b: 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) match {
    case Success(v) => println(v)
    case Failure(e) => println(e)
  }
}

match式を使ってSuccessとFailureを切り分け、それぞれの処理を記述します。

Tryのまま返す

Tryのモナドを性質を活用することを考えると、できるだけ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)
}

この関数の呼び出し側は、異常状態に対して何らかの処理を行うタイミングで、Tryに対して「副作用のある処理を行なって終了」といった具体的な処理を行います。

値を返す

正常状態でも異常状態でも、何らかの結果を返すケースもあります。

def f(a: Int, b: Int): 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).getOrElse(-1)
}

正常系の値はそのまま返すとして、問題は例外発生時の異常系です。

ここでは、Optionでもよく使うgetOrElseメソッドを使ってみました。

値を返すか例外を投げる

関数型プログラミングとしてはちょっと先祖返りの感もありますが、場合によっては処理の終わりに例外を投げるケースもあるでしょう。

@throws(classOf[ArithmeticException])
def f(a: Int, b: Int): 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) match {
    case Success(v) => v
    case Failure(e) => throw e
  }
}

Tryでこのケースのための機能はないようなので、match式を用いてSuccessとFailureを判定し、Failureの時に例外をthrowします。

ノート

プログラムの全体構造は途中はすべてTryのまま返していって、最後に副作用のある処理を行なって終了、がひとつの形です。

以下の図はオブジェクト指向プログラミングと関数型プログラミングの接続方法を説明したものですが、この図の右側・関数型プログラミングではTryモナドの計算文脈で計算を行い、左側・オブジェクト指向プログラミングに返ってきた後に副作用のある処理(本ブログの例ではprintln関数によるコンソール出力)を行うのが、Tryを使う時の基本的な考え方として有効ではないかと思います。

諸元

  • Scala 2.10.0-M7

2012年9月25日火曜日

Scala Tips / Scala 2.10味見(13) - Try(5) 基本フォーム2

前回はTryの基本フォームとして以下のものを考えました。

def f(a: Int, b: Int) {
  Try { // Try処理の本体
    a / b
  } map { // 例外が発生しない処理
    x => x + 1
  } flatMap { // 例外が発生する処理
    x => Try {
      x / (b - 1)
    }
  } match { // 終了処理
    case Success(v) => println(v)
    case Failure(e) => println(e)
  }
}

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

scala> f(100, 0)
java.lang.ArithmeticException: / by zero

scala> f(100, 2)
51

scala> f(100, 1)
java.lang.ArithmeticException: / by zero

TryのmapメソッドやflatMapメソッドといったコンビネータをこのようにつないで一種のパイプラインをつくるのが、モナドらしい使い方です。

ただし、このプログラムのように関数リテラルをそのまま記述するようにすると、プログラムの見通しが悪くなるという問題があります。

また、関数型言語における関数は再利用の重要な候補です。可能であれば関数を部品化する方向に持って行きたいというニーズもあります。

内部関数

こういったニーズに適合するのが内部関数です。

前述の関数fを内部関数を用いて書き直したものが以下になります。

def f(a: Int, b: 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) match {
    case Success(v) => println(v)
    case Failure(e) => println(e)
  }
}

3つの内部関数divAB, plus1, minus100DivBを用意し、起点のTry、mapコンビネータ、flatMapコンビネータに合成しています。終了処理は普通にmatch式を用います。

効果の一つとして、Tryを起点にするパイプラインの全体像が分かりやすくなっています。

また、パイプラインを構成する書く関数に名前がついているので、プログラムとしての可読性も上がっています。処理の内容をコメントで残すより、可読性の高い関数名をつけた関数に分解して処理を合成したほうが合理的ですね。

さらに、内部関数の中で汎用性がありそうなものは、外部に出して再利用可能な部品化していくという道筋をつけることもできます。

ノート

今回の内部関数によるイディオムは、Tryに限らずモナドを使ったモナディック・プログラミングでは頻出です。そういう意味では、Tryも普通のモナディック・プログラミングの枠組みで使うのがよい感じということです。例外による異常処理をモナディック・プログラミングに載せる機構がTry、ということもいえますね。

諸元

  • Scala 2.10.0-M7

2012年9月24日月曜日

Scala Tips / Scala 2.10味見(12) - Try(4) 基本フォーム

Tryを単なるエラー情報の入れ物と見てしまうと、普通のオブジェクト指向プログラミングの範囲の利用になり、関数型プログラミングとしては冗長なコーディングをすることになってしまいます。

Tryの価値はモナドである点にあるので、モナド的な使用方法を採るのがTryを活かす道です。

基本フォーム

Tryを使った基本フォームを考えてみました。

def f(a: Int, b: Int) {
  Try { // Try処理の本体
    a / b
  } map { // 例外が発生しない処理
    x => x + 1
  } flatMap { // 例外が発生する処理
    x => Try {
      x / (b - 1)
    }
  } match { // 終了処理
    case Success(v) => println(v)
    case Failure(e) => println(e)
  }
}

プログラム内にもコメントしましたが以下の4種類の部品から構成されます。

  • Try処理の本体
  • 例外が発生しない処理
  • 例外が発生する処理
  • 終了処理

処理を複数の関数を合成して記述するのが関数型のアプローチです。例外が発生しないものはmapメソッド、例外が発生する可能性のあるものはTryでくるんでflatMapメソッドで合成していきます。

Try処理の本体から終了処理にかけてパイプライン的な流れで考えるとよいでしょう。

Try処理の本体

Try処理の本体は、例外が発生する可能性がある処理をTryで包んで実行する処理になります。この結果Tryモナドが生成されるので、これに対してモナディック、パイプライン的に関数を適用していきます。

例外が発生しない処理

例外が発生しない処理はmapメソッドを使って関数を適用します。

例外が発生する処理

例外が発生する可能性のある処理は、関数をTryで包んで実行したものをflatMapメソッドを使って適用します。この結果Tryモナドが入れ子になりますが、flatMapの効果で自動的に一つにまとめられます。

終了処理

Tryを使った処理は以下の4種類の終了方法があります。

  • Tryのまま返す
  • 値を返す
  • 副作用のある処理を行なって完了
  • 値を返すか例外を投げる

今回はこの中の「副作用のある処理を行なって完了」にしてみました。

諸元

  • Scala 2.10.0-M7

2012年9月21日金曜日

Scala Tips / Scala 2.10味見(11) - Try(3) 機能の整理

Tryクラスの機能を整理してみました。

Futureクラスの機能と比較してみるとモナド+エラー処理を行うクラスの作り方の相場が見えてきて面白いと思います。

基本機能

Tryを使う上で必須の基本機能として以下の4つのメソッドをピックアップしました。

  • isFailure
  • isSuccess
  • get
  • getOrElse

Tryの典型的な使い方はTryのサブクラスであるSuccess, Failureをmatch式で取り出す処理になるので、isFailure, isSucces, getの3つのメソッドを実際に使うことは多くはなさそうです。

汎用コンビネータ

"いつもの"コンビネータです。Tryはモナドなので、Scalaのモナドとして綺麗に動くようなコンビネータ群が提供されています。

  • foreach
  • flatMap
  • map
  • filter
  • orElse

汎用コンテナ操作

これもモナド系の処理になりますが、Tryの入れ子を一つにまとめる(join演算)処理を行います。木構造に組織化した演算群全体の成功/失敗の総合結果をまとめたい時に有効です。

  • flatten

エラー処理

Tryはエラー処理に関するメソッドが多数用意されています。他の分類にあるものでエラー処理にも分類できるものは併記してその旨を記述しています。

  • isFailure(基本機能)
  • isSuccess(基本機能)
  • recoverWith
  • recover
  • failed
  • transform
  • toOption
  • orElse(汎用コンビネータ)

諸元

  • Scala 2.10.0-M7

2012年9月20日木曜日

Scalaとオブジェクト・モデリング

最近Scalaのトレイトを使っていて 新しい切り口を思いついたのでメモしておきます。

ScalaによるObject-Functional Programmingがモデリングに対して与える影響としてOFAD(Object-Functional Analysis and Design)という切り口で幾つか考察を続けてきました。

ここまで主に関数型プログラミング(FP)という切り口で、オブジェクト・モデリングの拡張を考えてきています。

さて、ScalaではFPの導入と同時にトレイトというオブジェクト指向プログラミング(OOP)での新しい言語機能を提供しています。OOPの世界でも着実に技術革新が進んでいるわけですね。

トレイトはかなり強力な機能でOOPのやり方を大きく変えてしまうインパクトを持っているというのが使ってみての感想です。そして、そのインパクトはモデリングにまで及びそうということに気づいたのが本記事の動機になっています。

分析、設計、実装

理想論では分析段階のモデルは実装技術に対してニュートラルで、影響を受けないはずです。分析段階では、ビジネスに対してより本質的なモデルを作成し、設計段階で実装とのギャップを埋めることになります。

とはいえ、分析モデルと実装技術に大きな乖離があると、分析モデルは単なる参考情報になってしまって、設計段階で事実上、一からモデリングを作ることになってしまいます。(更にひどくなると設計モデルと実装も乖離してしまって、一度実装に入ると設計モデルも参考情報になってしまい、プログラムとデータベーススキーマにしか正しい情報が残っていないといううこともありがちです。)

また、ユビキタス言語がオブジェクト・モデリングからOOPによって構成されるオブジェクト指向開発のキモで、上流のモデルと実装言語でインピーダンス・ミスマッチをなくすことが本質的に重要となります。

そういう意味で良くも悪くも上流工程におけるオブジェクト・モデリングも実装技術からはニュートラルというわけにはいかず、実装技術の文脈の中でモデリングのバランスが決まってきます。実装にスムーズに繋がるモデル要素とその組み合わせによって構築されたメタモデル(プロファイル)を定義して現場に適用していくことがオブジェクト・モデリングの活用技術ということになります。

SimpleModeling

本ブログで提唱しているモデリング手法はSimpleModelingで、MindmapModelingもその一環になっています。

SimpleModelingでは、もちろん分析モデルは実装ニュートラルを前提としていますが、実装技術としてはJava的なクラスベースのオブジェクト指向プログラミングとRDBMSを念頭に置いており、この実装技術から乖離しない範囲でいかに実装ニュートラルな分析モデルを作成するのかという点が主眼の一つとなっていました。

Java&RDBMSで困るのはインヘリタンスの扱いです。

Javaのインヘリタンスは単一継承なので、ドメイン・モデルに多重継承が使われていると実装時に困難が生じます。

また、RDBMSはインヘリタンスは直接扱えないので、アプリケーション側で何らかのエンコーディングを行う必要があります。

どちらも本質的な問題なので、分析と実装の間を埋める設計で吸収することはできなくはありませんがかなり大変です。

この点からSimpleModelingでは:

  • インヘリタンス(汎化)はできるだけ使用しない

というアプローチを取っています。

もちろん必要な所ではインヘリタンスを使いますが、可能な場合にはインヘリタンスのかわりに:

  • パワータイプ(powertype)
  • ロール(role)

を使うようにします。

トレイトによるCakeパターン

トレイトは、ちょっと制約を強くした多重継承のメカニズムと考えると分かりやすいと思います。制約を強くすることで多重継承を可能にした、ということですね。本当の多重継承とはちょっと異なるので、その点を理解した上でミックスイン的な使い方をすることになります。

トレイトを用いることで、ミックスインによるクラス合成ができるようになります。

このメカニズムをふくらませてイディオム化したものがCakeパターンです。

コンパイル時に静的型付けの枠組みで安全にDIができる、ということでCBD(Component-Based Development)的な用途にも広げていけそうです。

Cakeパターンは本当に便利で、OOP的にもプログラミングのやり方を大きく変えてしまうのインパクトがあります。

単一継承問題

オブジェクト・モデリングにおいてJavaを実装言語とする際に発生する最大の制約は単一継承です。

Scalaでは、トレイトの導入によってこの制約を大きく緩和することができます。また、ミックスインによるクラス合成、静的型付けの枠組みでの安全なDIといった新たな技法が利用可能になっています。

SimpleModelingでは、区分(powertype)やロール(role)を用いてインヘリタンス(汎化)をできるだけ回避するという方針を取ってきていましたが、Scalaを主力の実装言語とすることで、この方針を採る必要がなくなる可能性が出てきました。

さらに、ミックスインを積極的に活用したドメイン・モデルという可能性も見えてきました。

ロールの編み込み

単一継承問題の有り無しにかかわらず、ロール(role)はドメイン・モデルの重要な構成要素です。ロールを用いないと、物語(business use case)や出来事(event entity)から道具(resource entity)へのつなぎがうまく機能しないのでMindmapModelingでも役割(role)として基本構成要素にしています。

トレイトはロールの実装方式としても有力です。ロールをエンティティにトレイトを使って編み込むというような使い方です。

実装言語でロールが簡単に取り回しできるようになると、ドメイン・モデルでも採用しやすくなりますし、モデル駆動でのプログラム生成でも活用できます。またトレイトでの実装を意識したロールのモデリングの最適化も重要なモデリング技術になってきます。

トレイトでロールを編み込むというアプローチを推し進めていくとDCI(Data, context and interaction)アーキテクチャといった方向性も見えてきます。

このあたりも含めて、トレイトがオブジェクト・モデリングに与える影響はかなり大きそう、というのが現時点での見積りです。

関数型データアクセス

RDBMSへのアクセス法について考える際に、関数型言語の導入で影響がありそうなのが、関数型プログラミングとSQLの相性のよさです。

SQLやその他のQLも一種の関数型言語と考えると、アプリケーション側で関数型プログラミングをしている以上、ORMでSQLを隠すよりSQLを生に使って関数型プログラミングし、アプリケーション側の関数型プログラミングとシームレスにつなげてしまえばよいのではないか、というアプローチが有力に思えます。

このアプローチを採ると、OOPのインヘリタンスとRDBMSのインピーダンスミスマッチ問題は大幅に緩和されます。OOPとRDBMSの接続点で、ORMという枠組みの制約を受けることがなくなりますし、RDBMS側のデータをSQLで臨機応変に、ドメイン・オブジェクトにマップすることができるからです。(この「臨機応変」というところで、オブジェクト・モデリング的に面白い話題があるのですが、これはまた別途。)

つまりRDBMSでの実装を前提としたオブジェクト・モデリングの大きな制約になっていた単一継承問題を解決することができます。

SQLで関係演算をしてドメイン・オブジェクトの情報にマップするというアプローチは、「トレイトによるミックスインによるクラス合成」ともセマンティクス的に近しいものがありそうな感触もありますし、合わせ技で一本取れるかもしれません。

トレイト&関数型データアクセス

以上の議論をまとめると、単一継承問題に関して、プログラミング言語側の制約はトレイトで、RDBMSへの接続は関数型データアクセスで解決できる可能性が見えてきました。

つまり、この2つのボトルネックの解消により、ドメイン・モデルの構築時に(多少の工夫は必要でしょうが)インヘリタンスを禁忌扱いしなくてもよくなります。加えてCakeパターン的なクラス合成を積極的に活用するアプローチも可能になりそうです。

まとめ

これまでFPの切り口でオブジェクト・モデリングの拡張について考えてきましたが、Scalaによって導入されるトレイトも非常にインパクトのある機能です。トレイトがオブジェクト・モデリングに与える影響の可能性について、単一継承問題、ロールの編み込みといったところを考えました。

また、関数型データアクセスと合わせると、ドメイン・モデル構築時にインヘリタンスを禁忌扱いする必要がなくなることに加えて、Cakeパターン的なクラス合成を活用するアプローチも見えてきました。

ScalaによるOOP&FP&トレイトの効果は絶大で、オブジェクト・モデリングにも重大な影響を与えることになりそうです。

2012年9月19日水曜日

MindmapModeling「LCCがもたらしたのは、価格破壊だけではない」

9月15日(土)に横浜モデリング勉強会(facebook group)を行いました。また、会場には(株)アットウェア様の会議室をお借りしました。参加された皆さん、アットウェア様、どうもありがとうございました。

この勉強会で、浅海が作成したモデルを紹介します。モデルはMindmapModelingの手法で作成しました。(勉強会で使用したチュートリアル)

ワークショップの流れ

モデリング勉強会はワークショップ形式で以下の作業を行います。

  • 雑誌記事から情報システムの企画書、提案書、RFPの元ネタとなるモデルを作成する。

その上で、「要求仕様確認、実装可能性確認、開発のベースとなるプログラムを自動生成するモデルを目指」します。詳細は「ワークショップの進め方 (2012-06-16)」になります。

テーマ

モデリングの対象は、日経ビジネス誌の記事「LCCがもたらしたのは、価格破壊だけではない」です。

用語の収集と整理

まず用語の収集と整理します。

MindmapModelingに慣れてくると、用語がだいたいどこの枝に収まるのかわかるようになるので、用語を拾いながらラフなモデルを作っていきます。



今回の記事は、チケットに関しては常識の範囲の普通の記述なので焦点が絞りにくい感じでした。逆に規則(business rule)についての記述は多いのですが、これも制約事項的なものなので直接オブジェクト・モデルに活かせる感じではありません。

クラス図

この段階でのマインドマップをSimpleModelerでクラス図化したものが以下になります。




まだ、個々のクラスはばらばらで組織化されていません。

物語

次の作業は「物語」です。

モデルは中心軸がないと単なる「用語」の集りなのでまとまりがでてきません。何らかの目的を実現するための構造を抽出したいわけですが、この「目的」と「目的を実現するための構造」を掬いとるためのツールとして有効なのが「物語」です。オブジェクト・モデリングの概念ではビジネス・ユースケースということになります。

「物語」を中心軸と定め、「物語」のスコープで用語を取捨選択、組織化し、足りない用語を補っていきます。

その手順は:

  1. 物語の名前をつける。目的(goal)が明確になる名前がよい。
  2. 物語の主人公、相手役、脇役などの登場人物を定める。
  3. 物語で使用する道具を定める。
  4. 出来事または脚本の列として脚本を記述する。

となります。2の登場人物と3の道具は最初から完全なものはできないので暫定的なものを定め、4の脚本の作業を通して洗練させていきます。



モデルの焦点を絞るために物語として利用者がチケットを購入する物語とキャリアが価格設定をする物語を、物語の枝に上げました。

物語を作ることで頭の中が整理されたこともあり、モデリングの焦点の絞り方として以下の2つのアプローチがあることが見えてきました。

  • 利用者がキャリアをまたがって最適なチケットを購入するためのシステム
  • キャリアが利用者からより高額な購入価格を設定するためのシステム

いずれの場合も、OR的な最適解を求めるアルゴリズムは記事のスコープ外ですが、そういったアルゴリズムを機能させるためのドメイン・モデルを分析するのがマインドマップ・モデルのターゲットになります。

今回は「利用者がキャリアをまたがって最適なチケットを購入するためのシステム」のアプローチを取ることにしました。 

クラス図

この段階でのマインドマップをSimpleModelerでクラス図化したものが以下になります。




ビジネスユースケースを追加した他は、モデルに手を入れていないのでまだ一人っ子クラスが沢山あります。

最終型

前述の方針でさらに洗練を進めたモデルが以下になります。

「利用者がキャリアをまたがって最適なチケットを購入するためのシステム」の観点からチケット周りのモデルを精密化しました。



ポイントとなるのは、チケットとチケット仕様を分けた点です。チケットは利用者が購入したチケットのインスタンスに対応するクラスで、チケット仕様はチケットの料金設定などのメタ情報です。データベース的には前者がトランザクションデータ、後者がマスターデータということになるでしょう。

システム的には、チケット仕様に対してチケットがどのように売れたのかという点を記録しておき、後付けで分析して次回の価格設定に活かしたり、リアルタイムで分析して価格設定にフィードバックをかけたりするようなシステムを想定しています。

まだ、物語(ビジネスユースケース)まで手が回っていませんが、時間があれば物語に情報をフィードバックして、物語側の精度を上げていきたいところです。

クラス図

最終型のマインドマップをSimpleModelerでクラス図化したものが以下になります。



ビジネスユースケースとクラス間の関係がかなり整理されてきました。

経験則的には区分(powertype)の抽出数がモデルの充実度の指標になりますが、その点でも良い感じになっていることが確認できます。

ノート

ファウラーのアナリシスパターンに情報をoperationとknowledgeの二層に分けるパターンが出てきますが、knowledge的なメタ情報は普通のシステムでも普通に出てきます。マスターデータとして管理している情報の多くは、このknowledgeとしてモデル化できると思いますし、意識的にそうしていくことでより汎用性の高いモデルに洗練させていくことができると思います。

そういう意味で、SimpleModelingやMindmapModelingのメタモデル(=プロファイル)を考える上で、knowledge的なメタ情報を入れるのかどうかが常に悩みどころになっていました。何でもかんでも入れてしまうとメタモデルが複雑になってしまうので、最小限に絞る方向でモデル要素の取捨選択を行なっています。

クラウドアプリケーションの場合、単一ノードRDBMSの一本足打法でなくなるため、データごとのデータストアの選択やデータの配備方法が非常に重要になってきます。このためknowledge情報をメタモデル上も一級市民にして、方法論全体の枠組みの中できちんと扱って行かないといけないかな、と今回モデリングしながら感じました。

現在のところ、SimpleModelingには入れて、MindmapModelingには入れない、というのが落とし所かなぁと考えたりしています。

次回

次回は10月20日(土)です。

今回と同じく「ワークショップの進め方 (2012-06-16)」の手順で、「雑誌記事から情報システムの企画書、提案書、RFPの元ネタとなるモデルを作成する」を行う予定です。

2012年9月18日火曜日

SimpleModeler 0.4.0-RC2

モデルコンパイラSimpleModeler 0.4.0-RC2をリリースしました。

これは、土曜日に開催した横浜モデリング勉強会用に先行リリースした0.4.0-RCのバグ修正版です。0.4.0正式版は新機能の動作確認が取れた後リリース予定です。

0.3.3に引き続き基本的にはマインドマップ(XMind)とCSVからクラス図を生成する処理が実用フェーズになっています。

機能

Simplemodeler 0.3.3では以下のオプションを提供しました。

オプション機能状況
projectプロジェクト生成α
importモデル移入α
convertモデル変換試験的
html仕様書生成α
javaJava生成α
androidAndroid生成α
diagramクラス図生成
buildプロジェクトビルド試験的
gaejGoogle App Engine Java生成試験的
gaeGoogle App Engine Python生成試験的
gaeoGoogle App Engine Oil生成削除予定
grailsGrails生成試験的
g3g3生成試験的
asakusaAsakusa生成試験的

基本的にはマインドマップ(XMind)とCSVからクラス図を生成する処理が実用フェーズになっています。その他の機能はα版または試験的実装の状態です。

インストール

プログラムの配布は、Scala用のプログラム配布ツールconscriptを使っています。conscriptのインストール方法は以下のページに詳しいです。

Linux, Macであれば、以下のようにすればインストール完了です。

$ curl https://raw.github.com/n8han/conscript/master/setup.sh | sh

conscriptをインストールした後、以下のようにしてSimpleModelerをインストールします。

$ cs asami/simplemodeler

以下のコマンドがインストールされます。

sm
SimpleModelerコマンド
$ sm -version
Copyright(c) 2008-2012 ASAMI, Tomoharu. All rights reserved.
SimpleModeler Version 0.4.0-RC2 (20120918)
graphviz

-diagramオプションでクラス図を生成する場合は、graphvizのインストールが必要です。graphvizのインストール方法は以下を参照してください。

各プラットフォーム向けパッケージ管理ツールでもインストールできるようです。

Mac
http://www.macports.org/
Windows
http://sourceware.org/cygwinports/

使い方

マニュアルはまだありません。以前のバージョン用のものがありますが、機能が色々変わってしまったので一から見直す予定です。

リファレンスマニュアルとユーザーガイドの元ネタをこのブログで随時書いていきます。

クラス図生成

CSVまたはXMind(マインドマップ)からクラス図を生成することができます。

以下のCSVファイルをsample.csvとして用意します。

#actor,base
顧客
個人顧客,顧客
法人顧客,顧客
#resource,attrs,powers
商品,商品名;定価(long),商品区分(第1類;第2類;第3類)
#event,parts
購入する,顧客;商品

SimpleModelerを以下のように実行します。

$ sm -diagram sample.csv

以下のクラス図の画像が生成されます。




モデル記述に使用するCSVの文法は近いうちに説明する予定です。

2012年9月15日土曜日

SimpleModeler 0.4.0-RC

モデルコンパイラSimpleModeler 0.4.0-RCをリリースしました。

これは、本日の横浜モデリング勉強会用に先行リリースしたものです。0.4.0正式版は新機能の動作確認が取れた後リリース予定です。

0.3.3に引き続き基本的にはマインドマップ(XMind)とCSVからクラス図を生成する処理が実用フェーズになっています。

機能

Simplemodeler 0.3.3では以下のオプションを提供しました。

オプション機能状況
projectプロジェクト生成α
importモデル移入α
convertモデル変換試験的
html仕様書生成α
javaJava生成α
androidAndroid生成α
diagramクラス図生成
buildプロジェクトビルド試験的
gaejGoogle App Engine Java生成試験的
gaeGoogle App Engine Python生成試験的
gaeoGoogle App Engine Oil生成削除予定
grailsGrails生成試験的
g3g3生成試験的
asakusaAsakusa生成試験的

基本的にはマインドマップ(XMind)とCSVからクラス図を生成する処理が実用フェーズになっています。その他の機能はα版または試験的実装の状態です。

インストール

プログラムの配布は、Scala用のプログラム配布ツールconscriptを使っています。conscriptのインストール方法は以下のページに詳しいです。

Linux, Macであれば、以下のようにすればインストール完了です。

$ curl https://raw.github.com/n8han/conscript/master/setup.sh | sh

conscriptをインストールした後、以下のようにしてSimpleModelerをインストールします。

$ cs asami/simplemodeler

以下のコマンドがインストールされます。

sm
SimpleModelerコマンド
$ sm -version
Copyright(c) 2008-2012 ASAMI, Tomoharu. All rights reserved.
SimpleModeler Version 0.4.0-RC (20120915)

使い方

マニュアルはまだありません。以前のバージョン用のものがありますが、機能が色々変わってしまったので一から見直す予定です。

リファレンスマニュアルとユーザーガイドの元ネタをこのブログで随時書いていきます。

クラス図生成

CSVまたはXMind(マインドマップ)からクラス図を生成することができます。

以下のCSVファイルをsample.csvとして用意します。

#actor,base
顧客
個人顧客,顧客
法人顧客,顧客
#resource,attrs,powers
商品,商品名;定価(long),商品区分(第1類;第2類;第3類)
#event,parts
購入する,顧客;商品

SimpleModelerを以下のように実行します。

$ sm -diagram sample.csv

以下のクラス図の画像が生成されます。

モデル記述に使用するCSVの文法は近いうちに説明する予定です。

横浜モデリング勉強会

作成したマインドマップモデルからクラス図を生成するのに使用します。

前回は以下のようなマインドマップモデルを作成しました。

これをSimpleModeler 0.4.0-RCでクラス図化すると以下になります。

2012年9月14日金曜日

Scala Tips / Scala 2.10味見(10) - Try(2)

TryとEitherは2つの状態による計算文脈を表現するという意味ではよく似たオブジェクトですが、Tryはファンクタ(Functor)かつモナド(Monad)であるのに対してEitherはそうではないという点が異なります。

Eitherは直和の性質を表現するために2つの状態を同じ比重で扱いますが、この点を徹底するためにファンクタやモナドにはしていないと思われます。EitherではLeftProjectionやRightProjectionと組み合わせることによって、ファンクタ的、モナド的な使い方も可能ですが使い勝手がよいとはいえません。

正常状態と異常状態の2つの状態を扱う場合、(1)出現頻度は正常状態の方が多い、(2)正常状態に対するロジックは複雑/異常状態に対するロジックは単純、(3)一度異常状態になると以降は異常状態を保持、となるケースが多いと思います。このようなケースでは、正常状態の方を主にしたファンクタやモナドにするとプログラミング的な取り回しがとても楽になります。このような機能を実現しているのがTryというわけです。

準備

準備として奇数の時に例外を投げる関数を用意します。

def even(v: Int) = {
  if (v % 2 == 1) throw new IllegalStateException("odd")
  else v
}

Tryと組み合わせた実行結果は以下のようになります。

scala> Try(even(10))
res52: scala.util.Try[Int] = Success(10)

scala> Try(even(11))
res53: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

ファンクタ

Tryはファンクタなのでmapメソッドを使った関数合成ができます。

Successに対しては格納されている値に関数が適用され、新たなSuccessが返されます。成功状態が維持されます。

scala> Try(even(10)).map(_ + 1)
res54: scala.util.Try[Int] = Success(11)

Failureに対してはそのままFailureが返されます。異常状態が維持されるわけです。

scala> Try(even(11)).map(_ + 1)
res55: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

mapメソッドで関数を連続して適用することもできます。Successの場合は関数を連続適用した結果が返されます。一方、Failureの場合はそのままFailureが返されます。

scala> Try(even(10)).map(_ + 1).map(_ * 10)
res56: scala.util.Try[Int] = Success(110)

scala> Try(even(11)).map(_ + 1).map(_ * 10)
res57: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

モナド

Tryの状態が最初のものをそのまま維持する場合はよいのですが、演算結果によって状態を変えたい場合にはモナドを使います。また、複数のTryの状態を合成したい場合もモナドですね。

scala> a.flatMap(x => c.map(_ + x))
res58: scala.util.Try[Int] = Success(22)

scala> a.flatMap(x => b.map(_ + x))
res59: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

scala> a.flatMap(x => b.flatMap(y => c.map(_ + x + y)))
res60: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

モナドの演算はfor式の文法糖衣を使って簡潔に記述することができます。上の演算をfor式で記述すると以下になります。

scala> for (x <- a; y <- c) yield x + y
res61: scala.util.Try[Int] = Success(22)

scala> for (x <- a; y <- b) yield x + y
res62: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

scala> for (x <- a; y <- b; z <- c) yield x + y + z
res63: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)

ノート

TryはFunctor則を満たしていないのでは、という問題があるようで少し前に議論になっていました。

https://issues.scala-lang.org/browse/SI-6284

Functor則を満たさないのは、関数合成の中で例外をキャッチして正常処理に戻すという特殊なケースのようなので、このままの仕様でも、仕様変更があっても大きな影響はないと思います。 

ただ、mapやflatMapなどのコンビネータを使い関数合成を軸にプログラムを構築していくスタイルの関数プログラミングでは、Functor則を満たしているということは非常に重要です。合成の順番で結果が変わってしまうとなると、プログラミングの前提が変わってしまうからです。

このため、TryがFunctor則を満たす範囲がどこまでなのかという点については常に意識して置く必要があります。議論の結果はまだフォローできていませんが、次のリリースで結果を確認したいと思います。

アプリカティブ

演算結果によってTryの状態を変えたい場合や複数のTryの状態を合成したい場合もモナドを使います。

演算結果によって状態は変えないが、複数のTryの状態を合成したい場合はアプリカティブ(Applicative)を使うことになりますが、Scalaの基本ライブラリではアプリカティブ機能は提供されていません。

必要な場合はScalazとTryをつなぐ型クラスインスタンスを定義してScalazを使うことになります。

アプリカティブが使えると便利ですが、モナドに対するfor式の文法糖衣でも相当のことができるので、なくても実用的には問題ありません。

諸元

  • Scala 2.10.0-M7

2012年9月13日木曜日

Scala Tips / Scala 2.10味見(9) - Try

プログラミング言語もバージョンが上がって新機能が追加されることによって、多かれ少なかれプログラミング・スタイルが更新されていきます。1バージョン切り替え毎に、マイナー更新レベルなのか、新しいバージョンに全面的に乗り換えて行かないといけないか選択が迫られることになります。

Scala 2.10は、InterpolationやType Dynamic、Reflection、macro(これはexperimentalですが)など内部DSLの構築方法を大きく変える機能が満載なので、全面的に乗り換えていくバージョンと考えてよいと思います。

そういった観点からも見逃せないのがscala.util.Tryです。Tryは正常状態と異常状態の2つの状態による計算文脈を提供するモナドです。

Scala 2.10.0-M7ではFutureでも計算結果通知に(元々のEitherから変更されて)Tryを使うようになっており、異常系を扱う処理ではTryが中心的なクラスになっています。

異常系処理はプログラム全体で使用する基本中の基本処理なので、ここの処理の中心にTryを据えると、今までとプログラミングスタイルが大きく変わってくることになります。

つまりTryは一見地味ですが、Scala 2.10のプログラミングスタイルを考える上で外せない機能ということです。

準備

準備として奇数の時に例外を投げる関数を用意します。

def even(v: Int) = {
  if (v % 2 == 1) throw new IllegalStateException("odd")
  else v
}

使い方

Tryの基本的な使い方は、以下のようにtry文の代わりに使います。try文ではcatchまたはfinallyが必須ですが、Tryでは実行結果がTryオブジェクトとして返ってきます。

import scala.util.Try

val a: Try[Int] = Try {
  even(10)
}

val b: Try[Int] = Try {
  even(11)
}

正常系の処理結果は以下になります。

a: scala.util.Try[Int] = Success(10)

異常系の処理結果は以下になります。

b: scala.util.Try[Int] = Failure(java.lang.IllegalStateException: odd)
allCatch & Either

例外処理はscala.util.control.ExceptionのallCatchやcachingメソッドとOptionやEitherを組合せて使うのが定番のイディオムになっていました。

allCatchとEitherを組み合わせると以下になります。

import scala.util.control.Exception.allCatch

val a: Either[Throwable, Int] = allCatch either {
  even(10)
}

val b: Either[Throwable, Int] = allCatch either {
  even(11)
}

正常系の処理結果は以下になります。

a: Either[Throwable,Int] = Right(10)

異常系の処理結果は以下になります。

b: Either[Throwable,Int] = Left(java.lang.IllegalStateException: odd)

実行結果は、結果を格納するオブジェクトがTryとEitherで違うだけで、ほとんど同じに見えます。

しかし、Eitherは直和を表現するための汎用オブジェクト、Tryは正常系/異常系の両方を取り扱う計算結果を表現する専用オブジェクトなので、使い勝手には相当の開きがあります。

次回はこのあたりの違いを調べてみる予定です。

ノート

TryとallCatchの細かい振舞いの違いの一つに受け取れる例外の種類があります。allCatchは文字通りすべて受け取るのに対して、Tryはscala.util.control.NonFatalに分類されるもののみになっています。このあたりの話題もいずれ取り上げたいと思います。

諸元

  • Scala 2.10.0-M7

2012年9月12日水曜日

goldenport-runner 0.1.0

Scalaプログラムの開発時に、開発中のプログラムを普通の環境で使いたいことがよくあります。そのたびにパッケージングして配備するのも大変ですし、javaコマンドで直接起動する場合には依存するJARファイル群にクラスパスを通す設定が困難を極めます。

この問題を解決するために作成したのがgoldenport-runnerです。

goldenport-runnerはensimeのコンフィグレーションファイル(.ensime)から、プログラムの実行に必要な情報を収集し起動するアプリケーション・ランチャーです。(.ensimeの情報を読み込むためにgoldenport-sexprを使用しています。)

インストール

以下のjarファイルをダウンロードしてlibディレクトリなどにコピーします。

使用方法

以下のようにプロジェクトのディレクトリとアプリケーションのメインとなるクラス名を指定し、続けてアプリケーションの引数を渡します。

$ java -jar goldenport-runner_2.9.2-0.1.0-one-jar.jar プロジェクトディレクトリ クラス名 引数

以下のようなスクリプトを作ってbinディレクトリに入れておくと便利です。

#! /bin/sh

JAVA_OPTS="-Xmx512m -XX:MaxPermSize=256m -Dfile.encoding=UTF-8"

java $JAVA_OPTS -jar $HOME/lib/goldenport-runner_2.9.2-0.1.0-one-jar.jar $HOME/src/mycommand com.example.mycommand.Main "$@"

使用例

sbtプロジェクトにensimeプラグインを追加したプロジェクトを用意します。

手っ取り早く試すには、昨日紹介したscala-sbt.g8を使うとよいでしょう。

$ g8 asami/scala-sbt

asami/scala-sbtのsbtプロジェクトではensimeプラグインが予め設定されています。

g8コマンドでプロジェクトを生成します。デフォルトの値で生成するとディレクトリcommandの下にプロジェクトが生成されます。(commandの部分はプロジェクト名によって変わります。)

$ cd command
$ ls
README  build.sbt project  src

プロジェクトをビルドします。

$ sbt test
...
[info] CommandSpec:
[info] Command 
[info]   should execute that 
[info]   - empty arguments
[info]   - one argument
[info] Passed: : Total 2, Failed 0, Errors 0, Passed 2, Skipped 0
[success] Total time: 4 s, completed ...

ensimeのコンフィグレーションファイル(.ensime)を生成します。

$ sbt "ensime generate"

goldenport-runnerで実行します。

$ java -jar ~/Downloads/goldenport-runner_2.9.2-0.1.0-one-jar.jar . com.example.command.Main ls
project
README
target
build.sbt
src

用途

goldenport-runnerは、元々プログラム開発時のツールとして作ってみたのですが、Scalaスクリプト的な使い方が便利ではないかと思いつきました。

Scalaはスクリプト機能を持っていますが、依存jarが一つでもあると簡単に実行することはできないため、事実上スクリプト的な使い方をすることが困難でした。scalazScala IOが使えないと不便でしかたありません。

goldenport-runnerを用いることで、sbtプロジェクトで普通に開発したScalaプログラムをスクリプト的に手軽に使えるようになります。scala-sbt.g8もこの用途を意識した構成になっています。

ちょっとした運用ツールなどスクリプト的な用途のプログラムでも、ちょっと複雑になるとテストプログラムも必要になります。また、TDDやBDD的な作り方ができると便利です。そういう意味でsbtプロジェクトとして作ってgoldenport-runnerで運用するのは、なかなか便利そうです。

ScalaスクリプトにはsbtベースのScriptsというアプローチもあるので、適材適所で使い分けするのもよいかもしれません。

2012年9月11日火曜日

scala-sbt.g8

Scala界ではプロジェクトの雛形生成器?としてgiter8が有名で、Typesafe Stack 2.0でも使われています。

Installing the Typesafe Stack 2.0.2にはscalaプロジェクトの雛形も多数用意されていますが、プレインなsbtプロジェクトとしてはscala-sbtが用意されています。

以下のようにするとsbtプロジェクトの雛形を簡単に構築できます。

$ g8 typesafehub/scala-sbt

オレ様scala-sbt

実際に自分プロジェクトで使うとプレインバニラな雛形は物足りなくなるので、各所でオレ様scala-sbtが多数作られていると思います。

ボクも自分用に作ってみたので紹介します。自分用にカスタマイズしたポイントは以下になります。(注意:個人向けなので、普通に使って便利なのかは保証の限りではありません。(笑))

プラグインの設定

sbtプラグインとして以下のものの設定を行いました。

  • ensime
  • eclipse plugin
  • conscript
  • onejar
プログラムの雛形

生成するプログラムの雛形として以下の機能を盛り込みました。ちょっとした開発ツールを作る時の土台にすることを想定しています。

  • scalaz
  • scalax.io
  • オプションのハンドリング
  • コマンドの切り分け
  • エラー処理
  • Scalatest

mainのソースコードは以下のようになります。

package com.example.command

import scala.util.control.Exception.{catching, allCatch}
import scalaz.{Resource => _, _}, Scalaz._
import scalax.io._
import scalax.file._

class Command(val args: Array[String]) {
  var verbose: Boolean = false
  var directory: Option[String] = None

  def run() {
    catching(classOf[java.io.IOException]).withApply {
      e => _error(e.getMessage)
    } apply {
      _run()
    }
  }

  private def _run() {
    _parse_options(args.toList) match {
      case "ls" :: Nil => _ls()
      case "cat" :: file :: Nil => _cat(file)
      case _ => _usage
    }
  }

  private def _error(msg: String) {
    Console.err.println(msg)
  }

  private def _parse_options(args: List[String]): List[String] = {
    _parse_options(args, Nil)
  }

  private def _parse_options(in: List[String], out: List[String]): List[String] = {
    in match {
      case Nil => out
      case "-verbose" :: rest => {
        verbose = true
        _parse_options(rest, out)
      }
      case "-dir" :: arg :: rest => {
        directory = arg.some
        _parse_options(rest, out)
      }
      case arg :: rest => _parse_options(rest, out :+ arg)
    }
  }

  private def _usage {
    println("usage: sample command args")
  }

  private def _ls() {
    for (path <- Path(directory | ".").children("[^.]*", nil)) {
      println(path.name)
    }
  }

  private def _cat(file: String) {
    println(Resource.fromFile(file).string)
  }
}

Scalatestによるテストケースは以下のようになります。

package com.example.command

import org.scalatest.WordSpec
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.junit.JUnitRunner
import org.junit.runner.RunWith

@RunWith(classOf[JUnitRunner])
class CommandSpec extends WordSpec with ShouldMatchers {
  "Command" should {
    "execute" that {
      "empty arguments" in {
        val app = new Command(Array())
        app.run()
      }
      "one argument" in {
        val app = new Command(Array("ls"))
        app.run()
      }
    }
  }
}

使い方

giter8

giter8のインストール方法はトップページにありますが、ここには載っていないMacPortsでもインストールできます。ボクはMacPortsでインストールしました。

$ sudo port install giter8
scala-sbtの雛形生成

オレ様版scala-sbtの雛形生成は以下になります。

$ g8 asami/scala-sbt

以下のようにパラメタを聞いてくるので入力すると雛形が生成されます。

organization [com.example]: 

package [com.example.command]: 

name [command]: 

scala_version [2.9.2]: 

version [0.1-SNAPSHOT]: 
コンパイル&テスト

雛形が生成されるので、コンパイル&テストしてみます。

$ cd command
$ sbt test

最後に以下のようなメッセージが出て正常終了するはずです。

[info] CommandSpec:
[info] Command 
[info]   should execute that 
[info]   - empty arguments
[info]   - one argument
[info] Passed: : Total 2, Failed 0, Errors 0, Passed 2, Skipped 0
[success] Total time: 13 s ...
ensime

ensimeの環境設定は以下になります。

$ sbt "ensime generate"
eclipse

Eclipseの環境設定は以下になります。

$ sbt eclipse

これで開発はEmacs&ensime、デバッグはEclipseの最強の組合せでScalaプログラミングできる環境を簡単に整えることができます!(笑)

2012年9月10日月曜日

goldenport-sexpr 0.1.0

とあるツールを作っていてS式のパースが必要なことが判明しました。Scalaで使えそうなものを探してみたのですが、あまりよい物はないようです。

そこでS式処理系goldenport-sexprを作ってみました。

S式は元々のセマンティクスに近い形で保持して、必要に応じてScalaで処理しやすい形式に変換するようなアプローチにしています。

プログラム

プログラムはとても簡単で、以下の2つです。

パーサーコンビネータを使って、ちょちょいと作れるのがScalaのよい所ですね。

使い方

使い方はこんな感じ。とあるツールで使っている範囲しかまだ整備していません。

class SExprSpec extends WordSpec with ShouldMatchers {
  import org.goldenport.sexpr.SExpr._

  "SExpr" should {
    "list" that {
      "dropWhile" in {
        var s = SExprParser("(:k 100)")
        val r = s.get.list.get.dropWhile(_ match {
          case _: SKeyword => false
          case _ => true
        })
        r should equal (List(SKeyword("k"), SNumber("100")))
      }
    }
    "keyword" that {
      "string" in {
        var s = SExprParser("""(:k "value")""")
        var v = SExpr.getKeyword[String](s.get, "k")
        v should equal (Some("value"))
      }
      "string list" in {
        var s = SExprParser("""(:k ("value"))""")
        var v = SExpr.getKeyword[List[String]](s.get, "k")
        v should equal (Some(List("value")))
      }
      "strings list" in {
        var s = SExprParser("""(:k ("value1" "value2"))""")
        var v = SExpr.getKeyword[List[String]](s.get, "k")
        v should equal (Some(List("value1", "value2")))
      }
    }
  }
}

sbt

build.sbtの設定は以下になります。

resolvers += "Asami Maven Repository" at "http://www.asamioffice.com/maven"

libraryDependencies += "org.goldenport" %% "goldenport-sexpr" % "0.1.0"

ノート

DSLをXtextなどでフルスクラッチで作るケースもあると思いますが、多くの場合は何らかのホスト言語の上にアドオンとして構築することになるでしょう。

ホスト言語としてはScalaやRubyといったプログラミング言語もありますが、XMLやJSONといったマークアップ言語も有力です。

汎用性を採るならXML、Webとの親和性ならJSONといった選択になるかと思います。また表形式のテキストだとCSVという選択もあります。

そういった選択肢の一つとしてS式も案外有力では、と思いつきました。WebブラウザならJSONですが、EmacsならS式です。S式をDSLにするとEmacsとの連携がとても楽になります。

Scalaプログラミングも、Emacs /ensime環境が強力です。この環境上でツールを整備する場合Scala側でS式を簡単に扱えるのはかなり重要な要因になりそうです。

2012年9月7日金曜日

Scala Tips / Scala 2.10味見(8) - Future(6)

scala.concurrent.Futureのコンパニオン・オブジェクトには以下のメソッドが定義されています。

  • failed
  • successful
  • apply
  • sequence
  • firstCompletedOf
  • find
  • fold
  • reduce
  • traverse

この中で、sequence, firstCompletedOf, find, fold, reduce, traverseは非同期実行した複数のFutureの同期制御を行います。今回はこれらのメソッドの動きを確認します。

なお本ブログでは、「オブジェクトで定義しているユーティリティ・メソッドは関数と呼ぶことも可」という方針にしているので、以下では関数と呼ぶことにします。

準備

例によって以下の関数gを使います。

val g = (x: Int) => {  
  Thread.sleep(x * 100)
  x  
}

実行時間計測はgo関数を用います。

def go[T](a: => T): (T, Long) = {  
  val start = System.currentTimeMillis  
  val r = a  
  val end = System.currentTimeMillis  
  (r, end - start)  
}

sequenceとtraverse

sequence関数とtraverse関数は「Scala 2.10味見(6) - Future(4)」でも取り上げましたが、Monadicプログラミングを象徴するような関数です。

seqeunce関数やtraverse関数を用いると、複数のFutureの処理の完了を一度に待ち合わせることができます。

sequence
def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  go {
    val f1 = future { g(1) }
    val f2 = future { g(2) }
    val f3 = future { g(3) }
    val r = Future.sequence(List(f1, f2, f3))
    Await.result(r, Duration.Inf)
  }
}
scala> f
res37: (List[Int], Long) = (List(1, 2, 3),302)
traverse
def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  go {
    val l = List(1, 2, 3)
    def k(x: Int) = future(g(x))
    val r = Future.traverse(l)(k)
    Await.result(r, Duration.Inf)
  }
}
scala> f
res38: (List[Int], Long) = (List(1, 2, 3),302)

sequence関数, traverse関数いずれの場合も、3つのFutureが同時に実行され最長の300msが全体の実行時間になっています。

par

今回のような単純な例では、実際のプログラミングではparメソッドによる並列Listを用いたほうが簡明です。

scala> go(List(1, 2, 3).par.map(g))
res32: (scala.collection.parallel.immutable.ParSeq[Int], Long) = (ParVector(1, 2, 3),306)

sequenceやtraverseはFutureモナドを陽に用いてプログラミングを行う際のツールと考えるとよいでしょう。

firstCompletedOf

複数のFutureの中で最初に完了したFutureの結果を使用するという"早い者勝ち"処理にはfirstCompleteOf関数が使用できます。

def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  go {
    val f1 = future { g(1) }
    val f2 = future { g(2) }
    val f3 = future { g(3) }
    val r = Future.firstCompletedOf(List(f1, f2, f3))
    Await.result(r, Duration.Inf)
  }
}
scala> f
res39: (Int, Long) = (1,102)

g(1)は100msしかウェイトしないので最初に実行が完了しますが、この結果得られる1が全体の演算結果、100msが全体の実行時間になっています。

find

複数のFutureの中で最初に条件を満たす結果を返したものを使用する処理にはfind関数が使用できます。

def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  go {
    val f1 = future { g(1) }
    val f2 = future { g(2) }
    val f3 = future { g(3) }
    val r = Future.find(List(f1, f2, f3))(_ > 1)
    Await.result(r, Duration.Inf)
  }
}
scala> f
res40: (Option[Int], Long) = (Some(2),202)

演算結果が1より大きいという条件にあうものはg(2)とg(3)ですが、実行時間はg(2)が200ms、g(3)が300msです。そこでg(2)が演算結果の条件が合うものの中で最初に実行が完了します。

全体の演算結果は、g(2)の演算結果の2、全体の実行時間はg(2)の実行時間の200msになりました。

foldとreduce

fold関数やreduce関数は、複数のFutureが全て終了した後を待ち合わせて畳み込みを行います。

fold
def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  go {
    val f1 = future { g(1) }
    val f2 = future { g(2) }
    val f3 = future { g(3) }
    val r = Future.fold(List(f1, f2, f3))(0)(_ + _)
    Await.result(r, Duration.Inf)
  }
}
scala> f
res41: (Int, Long) = (6,301)
reduce
def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  go {
    val f1 = future { g(1) }
    val f2 = future { g(2) }
    val f3 = future { g(3) }
    val r = Future.reduce(List(f1, f2, f3))(_ + _)
    Await.result(r, Duration.Inf)
  }
}
scala> f
res42: (Int, Long) = (6,302)

fold関数, reduce関数いずれの場合も、3つのFutureが同時に実行され最長の300msが全体の実行時間になっています。

ノート

Scala 2.10味見(6) - Future(4)」で触れたように、コンパニオン・オブジェクトFutureの提供する関数は、TraversableOnceとFutureを決め打ちにしているため、汎用性には問題がありますがFuture操作という観点では十分な機能を提供しています。

まずはこの実装を使いこなす事が大事ですが、技術的な流れとしてもう少し先の方向性についてもある程度予測することができます。

まず、モナドの入れ子操作は「Scala 2.10味見(6) - Future(4)」で述べたようにscalazが型クラスTravarseやApplicativeを用いて実装しているような汎用性のある形が求められることになるでしょう。

また、fold系処理ではSemigroup/Monoidを活用して、より高度な部品化を進めたいところです。scalazのfoldMapメソッドやsumrメソッド、foldReduceメソッドといった機能ですね。

現在のfold関数/reduce関数は、Futureの処理が全て完了するのを待って普通の畳込みを行なうナイーブなメカニズムになっていますが、Semigroup/Monoidを導入すれば結合法則(associative law)の性質、さらにCommutativeSemigroupやCommutativeMonoidといった型クラス(これらの型クラスはScalaz7にもまだ導入されていません)を導入すれば可換法則(commutative law)の性質を利用して、並列処理の最適化を行うことができるはずです。

ここまで確認してきたことを総合するとScalaz Promiseの機能はScala 2.10 Futureで置き換えることは可能です。Scalaの進化の方向性からも、Scalazも基本ライブラリのFutureを軸に並列プログラミングの技術体系を構築していくことになると予測します。

Scalaの基本機能でScalaz的なMonadicプログラミングができるようになるのは当面なさそうなので、そういう意味でもScalaz側でFutureを取り込んでMonadicに使えるように各種拡張が行われることを期待します。

諸元

  • Scala 2.10.0-M7
  • Scalaz 7 github 2012-09-03版

2012年9月6日木曜日

Scala Tips / Scala 2.10味見(7) - Future(5)

Futureクラスの機能を整理してみました。

基本機能

Futureを使う上で必須の基本機能として以下の5メソッドをピックアップしました。

  • onSuccess
  • onFailure
  • onComplete
  • isCompleted
  • value

残念ながら、Futureの完了待ち合わせの機能はメソッドとしては用意されておらず、Awaitオブジェクトを使う必要があります。

汎用コンビネータ

"いつもの"コンビネータです。Futureはモナドなので、Scalaのモナドとして綺麗に動くようなコンビネータ群が提供されています。

  • foreach
  • map
  • flatMap
  • filter
  • withFilter
  • collect
  • zip
  • andThen

エラー処理

Futureはエラー処理に関するメソッドが多数用意されています。基本機能の中でエラー処理にも分類できるものは併記してその旨を記述しています。

  • onFailure (基本機能)
  • onComplete (基本機能)
  • failed
  • transform
  • recover
  • recoverWith
  • fallbackTo

並列プログラミングの論点の一つは、非同期実行の結果発生したエラーをどのようにハンドリングするのか、という点です。

scala.parallel.Futureでは、問答無用に例外を送出する仕様になっていましたが、これでは事実上(1)アプリケーション全体をアボートさせる、(2)ログに残しておく処理などをした後、例外を無視する、といった大雑把な例外処理しかできません。

本格的な応用ではより精密なエラーハンドリングが必要になりますが、scala.concurrent.Futureはこのあたりのメカニズムが整備されています。

Scalaz 6 Promiseと比較しても、例外処理機能の充実はScala 2.10 Futureのアドバンテージといえます。

並列処理

並列処理を扱うメソッドとしては以下のものが当てはまります。

  • either
  • zip (汎用コンビネータ)
  • fallbackTo (エラー処理)

Futureの同期は、Futureの内部処理とAwaitオブジェクトが行うこともあり、ここに分類できるFutureのメソッドはそれほど多くありません。Futureクラスにあるのはコンビネータとして実現すると便利なものになります。

その他

以上のどれにも分類できないメソッドです。

  • mapTo

諸元

  • Scala 2.10.0-M7
  • Scalaz 6.0.4

2012年9月5日水曜日

Scala Tips / Scala 2.10味見(6) - Future(4)

Scala 2.10のFutureはモナドとして定義されていて、モナド的ないろいろな技が使えるようになっています。(参考:「Scala Tips / Validation (11) - モナド」)

また、Futureのコンビネータ、ユーティリティメソッドまわりは有用そうな機能が満載で、本来はこのあたりの基本機能から入るべきですが、個人的な趣味で今回はScalaz的なMonadicプログラミング向けの機能を取り上げます。

準備

例によって以下の関数を使います。

val g = (x: Int) => {  
  Thread.sleep(x * 100)
  x  
}

REPL上で以下の設定が行われているものとします。

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.util.{Success, Failure}
import scala.concurrent.util.Duration

sequence

モナドを使い出すと、モナドの入れ子をどうさばくのかというのがプログラミング上のテクニックとなってきます。

そこで登場するのがScalazのsequenceメソッド。「Scala Tips / Validation (22) - sequence」など、このブログでも再三取り上げました。sequenceメソッドはモナドの入れ子(正確にはTraverseとApplicative)を扱う場合の定番イディオムとなっています。

たとえば、ListとSomeの入れ子は以下のように操作することができます。

scala> List(1.some, 2.some, 3.some).sequence
res309: Option[List[Int]] = Some(List(1, 2, 3))
Scala 2.10 Future

seqeunceのFuture版です。FutureのListから関数のList全体をFuture化しています。

scala> val a = List(future(g(1)), future(g(2)), future(g(3)))
a: List[scala.concurrent.Future[Int]] = List(scala.concurrent.impl.Promise$DefaultPromise@58276cfe, scala.concurrent.impl.Promise$DefaultPromise@464c4e9, scala.concurrent.impl.Promise$DefaultPromise@73bb9f3f)

scala> val b = Future.sequence(a)
b: scala.concurrent.Future[List[Int]] = scala.concurrent.impl.Promise$DefaultPromise@1b45fed7

scala> Await.result(b, Duration.Inf)
res26: List[Int] = List(1, 2, 3)
Scalaz 6 Promise

参考にScalaz 6のPromiseです。Scala 2.10 Futureとほとんど同じになります。

scala> val a = List(promise(g(1)), promise(g(2)), promise(g(3)))
a: List[scalaz.concurrent.Promise[Int]] = List(<promise>, <promise>, <promise>)

scala> val b = a.sequence
b: scalaz.concurrent.Promise[List[Int]] = <promise>

scala> b.get
res306: List[Int] = List(1, 2, 3)

traverse

traverseメソッドsequenceメソッドの親玉になるメソッドで、モナドの入れ子に対してより汎用的な処理を行うことができます。

scala> List(1, 2, 3).traverse(x => (x + 1).some)
res315: Option[List[Int]] = Some(List(2, 3, 4))
Scala 2.10 Future

traverseのFuture版です。Listに対してfuture化した処理を適用して、関数のList全体をFuture化しています。

scala> val a = List(1, 2, 3)
a: List[Int] = List(1, 2, 3)

scala> val b = Future.traverse(a)(x => future(g(x)))
b: scala.concurrent.Future[List[Int]] = scala.concurrent.impl.Promise$DefaultPromise@1d9b0076

scala> Await.result(b, Duration.Inf)
res27: List[Int] = List(1, 2, 3)
Scalaz 6 Promise

参考にScalaz 6のPromiseです。Scala 2.10 Futureとほとんど同じになります。

scala> val a = List(1, 2, 3)
a: List[Int] = List(1, 2, 3)

scala> val b = a.traverse(g.promise)
b: scalaz.concurrent.Promise[List[Int]] = <promise>

scala> b.get
res307: List[Int] = List(1, 2, 3)

ノート

まず、Futureのコンビネータ、続けてユーティリティメソッドを取り上げるつもりだったのですが、ユーティリティメソッドにsequenceとtraverseを見つけてしまい、先にこれを取り上げることにしました。

Scalazではsequenceやtraverseは、型クラスTraverseとApplicativeに対する処理でざっくりいうとTraverse[Applicative[A]]をApplicative[Traverse[A]]に変換する処理を行います。この処理が、モナドの入れ子操作が頻出するMonadicプログラミングでは非常に便利なわけです。

Scala 2.10 Futureでは、このsequenceとtraverseをTraversableOnceとFutureへのハードコーディングで実現しています。TraversableOnceはすべてのコレクションクラスの総親分なので、コレクション全体を対象にしているので実用的な意味での適用範囲は広いですが、Scalazのような型クラスを用いたアプローチより汎用性が制限されることになります。

Futureのsequenceやtraverseを見て感じたのは、モナドを効率よく操作するためにはこういったScalaz的(Haskell的)な機能は必要ということです。Futureだけのことを考えると、今回の実装のようなハードコーディングで凌ぐことができますが、これから陸続と新しいモナドがクラスライブラリに追加されてくることを考えると、いずれ破綻してしまうのは明らかです。

こうなった場合の対応策は、Scalazが採っているHaskell流の型クラス方式ということになるでしょう。Scalaの基本ライブラリがScalaz的なアプローチを取り入れるのか、Scalazが事実上の基本ライブラリとして併存するようになるのか、分かりませんが、モナドの利用が進むに従って型クラス方式の傾倒が進むのではないか。というのが、sequenceメソッド、traverseメソッドを見ての感想です。

諸元

  • Scala 2.10.0-M7
  • Scalaz 6.0.4

2012年9月4日火曜日

Scala Tips / Scala 2.10味見(5) - Future(3)

Futureのようなフレームワーク的な機能を開発する場合、関連する各種機能をどうやって疎結合にしていくのかという点が設計のポイントとなります。

future/promise機能の場合は、実行環境のスレッドメカニズムや同期実行のスケジューリングポリシーをどのように外付けにしていくのかという点がポイントとなります。以下では実行環境のスレッドメカニズムや同期実行のスケジューリングポリシーを実行コンテキストと呼ぶことにします。

その観点でScala 2.9までのscala.parallel.Futureやscala.actors.Futureを見てみると、実行コンテキストはハードコーディングされているようにみえます。

Scalaz 6 Promise

Scalaz 6のPromiseでは、実行コンテキストを表現するトレイトStrategyが導入されています。以下のStrategyが用意されており利用者が選択できるようになっています。

Executor
JavaのExecutorServiceを利用。
Naive
実行毎にスレッドを新しい起動。
Sequential
現在のスレッドを使用(並行処理しない)。
Identity
実行しない。
SwingWorker
SwingWorkerで実行。
SwingInvokeLater
SwingのDispatching threadで実行。

基本的にはExecutorを使用してJavaのExecutorServiceの枠組みで非同期処理を行います。

Naive, Sequential, Identityは性能測定や動作確認で使用すると便利そうです。SingWorker, SwingInvokeLaterはSwingのフレームワーク上で非同期実行させたい時に使用します。

このように性能測定/動作確認用途や他フレームワーク(この場合はSwing)上のスケジューリングを行いたい場合があるので、実行コンテキストをfuture機能の外付けにして、利用者に選択してもらうメカニズムは必須といえます。

2.10 Future

2.10 FutureにおいてScalaz PromiseのStrategyに相当する実行コンテキストはscala.concurrent.ExecutionContextです。

前回、前々回にScala 2.10のFutureの例として以下のようなプログラムを使いました。

def f1: Int = {
  import scala.concurrent._
  import scala.concurrent.util.Duration
  import ExecutionContext.Implicits.global

  val f = future { g(1) }
  Await.result(f, Duration.Inf)
}
def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  val f1 = future { g(1) }
  val f2 = future { g(2) }
  val f3 = future { g(3) }
  go {
    val f = for {
      x <- f1
      y <- f2
      z <- f3
    } yield x + y + z
    Await.result(f, Duration.Inf)
  }
}

「scala.concurrent._」をimportしているのは当然として、「scala.concurrent.ExecutionContext.Implicits.global」をimportしています。scala.concurrent.ExecutionContext.Implicits.globalは、scala.concurrent.ExecutionContextを暗黙変数として定義したものです。

scala.concurrent.futureメソッドは、実行コンテキストであるExecutionContextを暗黙パラメタとするので、結果としてscala.concurrent.ExecutionContext.Implicits.globalとして定義されたExecutionContextがFutureに渡っていきます。

現在のところExecutionContextは、java.util.concurrent.ExecutorServiceやjava.util.concurrent.Executor版のものが用意されているだけです。scala.concurrent.ExecutionContext.Implicits.globalにも、この版のExecutionContextが設定されています。

まだまだ道具立てが充実している感じではないですが、いずれScalaz Promiseが提供しているような用途別のものが提供されてくることになるでしょう。

ノート

Scalaで使用する実現方式はおおむね「Real-World Scala: Dependency Injection (DI)」にある以下の方式に分類されます。

  • Cake Pattern
  • Structural typing
  • Implicit declarations
  • DI frameworks (e.g. Google Guice)

また、最近ではImplicit declarationsを活用した型クラスも選択肢に入ってくるでしょう。

Futureのような機能の場合は、Implicit declarationsの暗黙パラメタを使うのが定番となっているようです。Scalaz PromiseもScala Futureも暗黙パラメタを使っています。

諸元

  • Scala 2.10.0-M7
  • Scalaz 6.0.4

2012年9月3日月曜日

Scala Tips / Scala 2.10味見(4) - Future(2)

Scala 2.10で導入されたFutureはscala.concurrent.Futureです。

一方Scala 2.9段階ですでに2つのFuture、scala.actors.Futureとscala.paralell.Futureが導入されています。

scala.actors.FutureはActor用のFutureです。Scalaが元々提供していたActorは、Akkaに置き換わっていく予定なのでそれに伴いフェードアウトしていくと予想されます。

Scala 2.10のFutureはAkka(Scala Actorの後継)のFutureを包含した機能を提供し、Akkaと併用することを想定していると考えられます。

scala.paralell.FutureはScala 2.9で導入されたのですが、2.10では早くもdeprecatedになっており、scala.concurrent.Futureの利用が推奨されています。

scala.paralell.Futureは(Actorとは切り離された)一般的なfuture機能を提供することが目的だったと考えられますが、こちらの用途もscala.concurrent.Futureが一手に担うことになります。

つまり、actor用(Akka用)futureと一般用途向けfutureを一つのFutureとして提供するのがscala.concurrent.Futureの目的の一つと考えられます。

actorとfutureの関係は別途検討することとして、ここでは一般用途向けfutureという観点で調べていきます。

scala.paralell.Futureは機能的にはJavaのjava.util.concurrent.Futureと同等で、非同期実行の結果をapplyメソッドで取得(Java Futureの場合はgetメソッド)します。非同期実行をオブジェクトで隠蔽したものといえます。

それに対して2.10のscala.concurrent.Futureは何が違っているのかというと:

  • モナド化された
  • 実行コンテキストを指定できるようになった

の2点が拡張されました。いずれもScalaz Promiseの特徴的な機能で、Scalaz Promiseの機能をScala本体が取り込んだのではないかと思います。(他のフレームワークからも色々機能を取り込んでいる可能性がありますが未確認です。)

ここでは、モナド化の側面からScala Futureを見ていきましょう。

準備

準備として引数の数値×100msウェイト後、引数の数値をそのまま帰す関数gを定義します。(前回と同様)

val g = (x: Int) => {  
  Thread.sleep(x * 100)
  x  
}

性能測定用に関数goを定義します。(参考)

def go[T](a: => T): (T, Long) = {  
  val start = System.currentTimeMillis  
  val r = a  
  val end = System.currentTimeMillis  
  (r, end - start)  
}

for-comprehension

並列処理などの複雑な振舞いがモナド化されることで、いろいろな技が使えるようになると期待できますが、直接的なメリットとしてはfor式で簡単に並列処理の合成ができるようになることが挙げられます。(言うまでもありませんがScalaのfor式はモナドを簡単に操作できるようにするための文法糖衣です。)

例として、関数gを引数1, 2, 3で3回実行し、その結果の和を計算します。

まず逐次実行の場合です。この場合は計算結果が6、実行時間は約600msになりました。

scala> go { g(1) + g(2) + g(3) }
res6: (Int, Long) = (6,602)

この処理をfuture化します。

次の関数fは3つのfutureを生成し、それぞれの結果を加算したものを結果としています。前述の単純な加算に比べると複雑な構造になりますが、for式を使うことでかなり見通しよく記述できています。

def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  val f1 = future { g(1) }
  val f2 = future { g(2) }
  val f3 = future { g(3) }
  go {
    val f = for {
      x <- f1
      y <- f2
      z <- f3
    } yield x + y + z
    Await.result(f, Duration.Inf)
  }
}

実行結果は以下になります。この場合は計算結果が6、実行時間は約300msになりました。関数gの最大実行時間は引数が3の場合の300msですが、これが関数fの実行時間となっています。つまり、関数gを引数1, 2, 3で3回実行していますが、この3つの実行は並列に行われたことになります。

scala> f
res4: (Int, Long) = (6,301)
もう少し複雑な例

以下のような計算を考えます。計算結果は21、実行時間は約2103msです。

scala> go { (g(1) + g(2) + g(3)) + (g(4) + g(5) + g(6)) }
res7: (Int, Long) = (21,2103)

これをFuture化すると以下になります。

def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  val f1 = future { g(1) }
  val f2 = future { g(2) }
  val f3 = future { g(3) }
  val f4 = future { g(4) }
  val f5 = future { g(5) }
  val f6 = future { g(6) }
  go {
    val fx = for {
      x <- f1
      y <- f2
      z <- f3
    } yield x + y + z
    val fy = for {
      x <- f4
      y <- f5
      z <- f6
    } yield x + y + z
    val fz = for  {
      x <- fx
      y <- fy
    } yield x + y
    Await.result(fz, Duration.Inf)
  }
}

実行結果は以下になります。この場合は計算結果が21、実行時間は約600msになりました。関数gの最大実行時間は引数が6の場合の600msですが、これが関数fの実行時間となっています。つまり、関数gを引数1, 2, 3, 4, 5, 6で6回実行していますが、この6つの実行は並列に行われたことになります。

for式で得られたFutureをさらにfor式で合成しても、並列処理の性質は損なわれることなく無事並列動作することが確認できました。

scala> f
res5: (Int, Long) = (21,601)

ノート

Scala 2.9のscala.paralell.Futureはオブジェクト指向的なfutureですが、モナドでないため関数型プログラミングとの相性が今ひとつでした。その問題を解決したのがScalaz 6のPromiseです。

Scala 2.10のscala.concurrent.Futureは、モナドとして実装されたfutureで関数型プログラミングで効果的に使えるようになっています。Scalaz 6 PromiseをScala本体が取り込んだという見方もできるかと思います。

逆にScala 2.10上でScalaz Promiseをどう扱っていくのかというのが新たな論点となってきます。

そういう目でScalaz 7を眺めてみると、Promiseは残っているもののcoreからは分離されconcurrentパッケージとして提供されておりオプション的な扱いになっています。特に象徴的なのは、coreのscalaz.Function1Wにあったpromiseメソッドがscalaz.syntax.std.Function1Opsには残っていない点です。(kleisliメソッドはそのまま残っています。)Scalaz Promiseの入っているconcurrentパッケージはJARも別になるので、意識して設定しないと使えない機能になっています。

このような状況なので、Scalaz 7はScala 2.10 Futureを並列プログラミングの基本機能として併用することを想定しているのではないかと推測できます。ただ、前述した通りscalaz.Function1Wにあったpromiseメソッドがなくなっており、またscalaz.MAで定義されていたparMap, parBind, parZiWithメソッドもありません。そういう意味では、Scalaz 7とScala 2.10 Futureとのシームレスな連携は今後の課題ということになります。

Scala 2.10 Futureはモナドなので、つなぎコードを整備すればScalazのkleisli, applicative functor, traverse, monoidといった機能を連携させることは容易と考えられます。Scala 2.10が落ち着いて、Scalaz 7がこういった部分の整備が一段落したところが、Scala 2.10+Scalaz 7への移行のタイミングの目安と考えたいと思います。

諸元

  • Scala 2.10.0-M7
  • Scalaz 7 github 2012-09-03版