2016年1月31日日曜日

[SDN] 例外とNothing

try/catchで例外処理を書く場合、以下のように例外に対する共通処理を羅列する形になることがあります。

すべての例外に対して同じ処理を行う場合には、大本のjava.lang.Exception(場合によってはjava.lang.Throwable)でキャッチすればよいですが、以下のように例外毎に少しずつ処理が異なる場合には羅列せざるを得ません。

def doSometing(): String = {
  try {
    doSomesing()
  } catch {
    case e: FileNotFoundException =>
      log("file not found")
      throw e
    case e: InterruptedIOException =>
      log("intterupted io")
      throw e
    case e: IOException =>
      log("io")
      throw e
    case NonFatal(e) =>
      log("non fatal")
      throw e
  }
}

上記のプログラムでは以下の処理が例外の共通処理になります。

log("file not found")
throw e

「例外毎に異なったメッセージをログに出力して、受け取った例外を再送出する処理」ですが、ログに出力するメッセージの文言が例外毎に変わってきます。

共通処理化

「例外毎に異なったメッセージをログに出力して、受け取った例外を再送出する処理」を関数化して再利用するために以下の関数を作ってみました。

この処理は例題なので単純なものになっていますが、製品開発ではもっと細かくて複雑な処理が必要になってくる可能性があります。また、システム共通のエラー処理を変更する場合には、エラー処理のロジックが一箇所に集まっていた方が取り回しが楽ということもあります。

def handleError(message: String, e: Throwable) {
  log(message)
  throw e
}

この関数は以下のように例外処理に組み込みます。

try {
    doSomesing()
  } catch {
    case e: FileNotFoundException =>
      handleError("file not found", e)
    case e: InterruptedIOException =>
      handleError("intterupted io", e)
    case e: IOException =>
      handleError("io", e)
    case NonFatal(e) =>
      handleError("non fatal", e)
  }

一見これでよいように思いますが、実は以下のようなコンパイルエラーが出てしまいます。

...
[error]  found   : Unit
[error]  required: String
[error]         handleError("file not found", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("intterupted io", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("io", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("non fatal", e)
[error]                    ^
[error] four errors found

その理由は、handleError関数の返却値がUnitであるため、例外処理を行っているdoSomething関数の返却値Stringと型が合わないためです。

この問題の対策が今回のテーマです。

Stringを返す

利用者側の関数の返却値とhandleError関数の返却値が合わない問題を解決する方法として、返却する型を明示したhandleError関数を用意する方法があります。

たとえば以下のようなhandleErrorString関数を用意する方法です。

def handleErrorString(message: String, e: Throwable): String = {
  log(message)
  throw e
}

しかし、この方法を採ると必要な型ごとに1つ関数を用意する必要があります。用途によっては考えられる解決策ですが、汎用的に適用できる方法ではありません。

型パラメータ

1つの解としては以下のように型パラメータを使う方法があります。

def handleError[T](message: String, e: Throwable): T = {
  log(message)
  throw e
}

handleError関数の使用元が必要とする型が自動的に型Tに設定されhandleError関数の返却値となります。このためコンパイルエラーになりません。

Nothing

前述の型パラメータを使う方法でもよいのですが、Scalaにはこのような目的に使用できるNothingという型があります。

Nothingはすべての型のサブクラス、という特殊な位置付けの型です。一種のワイルドカードのような型です。

以下のようにhandleError関数の返却値をNothingとすると、doSomething関数がどのような型を返すことになっても、そのまま利用することができます。

def handleError(message: String, e: Throwable): Nothing = {
  log(message)
  throw e
}

この例のように最後に例外を送出する処理を行う関数は、実際には値は返すことはないので、返却値の型をNothingしても問題はなく、利用範囲は大きく広がります。

今回の問題に対しては前述の型パラメータを使う方法でも対処可能ですが、Nothingを使った方がより意図が明確になると思います。

参考

未実装のメソッドを定義するのに便利な「???」メソッドも返却値にこのNothingを使っています。「???」メソッドの定義は以下のようになっています。

def ??? : Nothing = throw new NotImplementedError

諸元

  • Java 1.7.0_75
  • Scala 2.11.7

2015年12月30日水曜日

MindmapModeling 2015

MindmapModelingの最新状況についてのご紹介です。

MindmapModelingはマインドマップを使ってモデリングを行う手法です。マインドマップを使って自由にモデルを書くというのではなく、UMLメタモデルによるモデルの表記方法としてマインドマップを用いるというアプローチです。

wakhok時代(2007年ごろ)に教育用として考案してモデリングの授業で使用していました。雑誌連載したものを書籍としてもまとめました。

MingmapModelingの比較的最近の記事としては2012年のものがあります。

その後、活動の中心がクラウドサービスプラットフォームの開発になったため情報発信は減りましたが、モデルコンパイラSimpleModelerなど他のモデリング技術と同時に開発技術として内部利用してきました。

クラウドアプリケーションとMindmapModeling

前述した通りMindmapModelingは元々教育用に開発したものですが、クラウドアプリケーション開発では実用技術として使えるのではないかと考えています。

モデリング技術全体に対するアプローチは以下で取り上げました。

クラウドサービスプラットフォームとScalaによるDSL指向Object-Functional Programming(OFP)のコンボによってアプリケーション開発におけるアプリケーションロジックのプログラミング量は劇的に減らすことができると考えています。

そうなると開発技術の焦点は以下の2つになります。

  1. UIやその他の体験(リコメンドの精度など)を統合したUX(User eXperience)
  2. ビジネスとのつなぎ込み

その中で(2)ビジネスとのつなぎ込みはまさにモデリング技術によって解決する分野ですが、ここにMindmapModelingがぴったりはまるのではないかと考えています。(1)のUXもUI的なチューニングではない、上記では「その他の体験」としている部分はアプリケーションが提供するフィーチャーそのものなので、機能要件あるいは非機能要件としてモデル化する必要があります。この目的でもMindmapModelingを使用することができます。

ビジネスとのつなぎ込み

MindmapModelingはビジネスとのつなぎ込みのモデルとして使用するため以下の2つの要件を満たすことを指向しています。

  • ビジネスサイドのサービス企画担当者と開発担当者が共有して議論のベースとして使用できるモデル
  • システム開発のオブジェクト(+関数)モデルにシームレスに連携できるモデル

MindmapModelingでは記述方式にマインドマップを使うことと、メタモデルを絞り込むことでこの要件を満たすこと意図しています。

従来的なシステム開発プロセスは以下のようなモデルの連携になります。

  • ビジネス設計-システム要求-分析-設計-実装

MindmapModelingはこの中のビジネス設計の後半、システム要求、分析の前半ぐらいまでをざっくり(教育向けに)カバーすることを目的にしていました。

そして、クラウド時代になるわけですが、ここではクラウドサービスプラットフォーム&Scalaの組合せにより、以下の形に持ち込めるのではないかという仮説を立てています。

  • ビジネス設計-システム要求-実装

この中のビジネス設計の後半とシステム要求をMindmapModelingで行い、そのまま直接Scalaプログラミングにつなげてしまうというのが新しい方程式です。ベースとなるクラウドサービスプラットフォームが固定化されていることとScalaのDSL機能、この2つの技術を組み合わせることで分析と設計のアクティビティを省いてしまう(i.e. プログラマが暗算で行う)ことが可能ではないかというのが仮説の眼目です。実際にApparelCloudの開発はこの方針にそって行っています。

ビジネス設計

ビジネス設計の後半からシステム要求はMindmapModelingで行うとして、次に必要になるのは、一つ上流にあるビジネス設計(の前半)です。

ここでいうビジネス設計はビジネスの意図、ゴール、メカニズムをオブジェクト・モデル化したものを意図しています。(具体的にはEriksson-Penkerをベースにしたメタモデルをイメージしています。)

EverforthではApparelCloudのビジネス領域とプラットフォーム向けのメタモデルを開発中です。(来年中にはご紹介できると思います。)

また、より汎用的な用途のメタモデルとしては匠Methodも選択肢になります。

ビジネス設計の技法としてEverforth方式、匠Method、あるいはその他のメソッドを目的に合わせて選んでも、ビジネスサイドとエンジニアとの情報共有にはMindmapModelingを使用することで、開発側へのインパクトを最小限に抑えることができます。

MindmapModeling 2015

MaindmapModeling 2015年版です。

雛形

雛形の図は以下になります。


MaindmapModeling 2015年半の例としてECサイトをモデリングしてみました。


ざっくりとイメージはつかんでいただけると思います。

改良点

マインドマップはオリジナルから以下の点を改良しています。

  • 設計向けにチューニング
  • クラウド向けのモデル
設計向けにチューニング

wakhok時代(2008年ごろ)に考案したMindmapModelingはモデリングへの導入を意図していたので「登場人物」や「道具」といったメタファを使っていましたが実務で使う上では逆にオブジェクト・モデリングとの連続性が分かりづらいという問題がありました。

この問題に対応するために、枝の名前を「登場人物→Actor」というように英語の正式用語化しました。

また、要求、分析レイヤーのモデルを指向していたので設計時に必要なモデル要素は意図的に省いていました。用途に応じて設計向けのモデルも記述できるようにTraitやPowertypeといった枝を追加しています。

クラウド向けのモデル

MindmapModelingのメタモデルに以下のモデル要素を追加しました。

Dataflow
データフロー。
Summary
サマリーエンティティ。
Document
データ表現/転送用途の不変オブジェクト。

SummaryとDocumentは元々SimpleModelingにはメタモデルとして組み込んでいましたが、分析目的、教育目的にはモデルが複雑になりすぎるのでMindmapModelingには導入していなかったモデル要素です。

Dataflow

クラウドアプリケーションでは、CQRSアーキテクチャに見られる通り、バックエンドでの非同期大規模計算がシステムの基本機能になってきます。

この処理を記述するためのモデルとしてデータフローを用意しました。

MindmapModelingでは、厳密なデータフロー・モデルを記述することは目的としていません。

Summary

クラウドアプリケーションでは、問い合わせに必要なデータ集計等を事前計算しておいて専用のテーブルなどに格納しておくことが重要な処理になります。

ざっくりとは業務システムで使われているバッチ処理と考えてよいと思います。

この目的で使用するテーブルなどのデータストアを表現するためのエンティティとしてSummaryを導入しました。

前述のDataflowで作成した事前計算結果をSummaryに格納するという位置付けになります。

Document

Documentは伝票を記述するモデルです。以下の利用方法を想定しています。

  • データをプログラム内で持ちまわる容れ物
  • XML, JSONの形式で転送可能になっている
  • 代数的データ型

Scalaではplay-jsonなどでJSONとの相互変換を担保したcase classで実装することを想定しています。

このレイヤーのモデルを分析段階でどこまで記述するのかは判断の別れるところですが、MidnmapModelingの方針として設計指向に寄せたので、その一環でメタモデルとして取り入れました。

このため、必要に応じて使用するという温度感のモデルになります。

設計への流れ

通常のモデルはMindmapModelingから直接Scalaプログラミングしても問題ありませんが、ある程度複雑なモデルの場合や新しく取り組む業務分野などの場合はメモ的に設計モデルをつくるのが効率的です。また外部連携が必要な処理では、きっちりとした仕様書が必要になります。

逆にいうとモデリング作業のすべてをMindmapModeling上で完結させることは目的としていません。基本はMindmapModeling上で行い、必要に応じてオブジェクト・モデリングやDOAなどの技法を適材適所で使う形を想定しています。

ロバストネス図

MindmapModelingで作成したモデルを実装に落とし込むための中間モデルとしてはロバストネス図が有力です。

前述のECサイトのモデル向けのロバストネス図の例を以下になります。


このロバストネス図をベースに、必要に応じてコンポーネント図やコミュニケーション図(コラボレーション図)を作成するとよいでしょう。

まとめ

MaindmapModelingの最新状況についてご紹介しました。

2008年ごろからクラウドアプリケーションの開発技法について考えてきましたが商用システムの開発を通して色々と部品建てが揃ってきたので、2016年は具体的な開発方法論として整備していければと思っています。

2015年11月30日月曜日

Scala Tips/Map + Monoid

ちょっと忘れがちですが、ScalazではMapもMonoidとして定義されています。

これが結構便利なのを再認識したのでメモしておきます。

カウンター

サイトごとのPV数をカウントするような用途でMonoidが便利に使えます。

以下のMapを考えます。Mapの値側の型がIntでありMonoidなので、Map全体もMonoidとして機能します。

MapをMonoidとして使用する場合もMapの作成は通常と同じです。

scala> var c = Map[String, Int]("www.abc.com" -> 1)
c: scala.collection.immutable.Map[String,Int] = Map()

サイトごとのカウンタをMapで管理しています。このカウンタを上げる処理は以下のように書くことができます。

scala> c = c.get("www.abc.com") match {
     | case Some(s) => c + ("www.abc.com" -> (s + 1))
     | case None => c + ("www.abc.com" -> 1)
     | }

この処理をMonoidを使って書くと以下になります。Monoidの演算子|+|で、サイトと足したいカウンタ数の組によるMapを足し込みます。

scala> c = c |+| Map("www.abc.com" -> 2)
c: scala.collection.immutable.Map[String,Int] = Map(www.abc.com -> 3)

scala> c = c |+| Map("www.abc.com" -> 5)
c: scala.collection.immutable.Map[String,Int] = Map(www.abc.com -> 8)

別のサイトのMapを足し込むと、Mapの別のエントリとして管理されます。

scala> c = c |+| Map("www.xyz.com" -> 3)
c = c |+| Map("www.xyz.com" -> 3)
c: scala.collection.immutable.Map[String,Int] = Map(www.xyz.com -> 3, www.abc.com -> 8)

リスト

同様の処理はListやVectorでも使用することができます。

以下のMapを考えます。サイトごとのタグの集まりを管理するMapをイメージしています。

scala> var r = Map[String, Vector[String]]("www.abc.com" -> Vector("spring"))
var r = Map(www.abc.com -> Vector(spring))

Monoidの演算子|+|で、サイトと追加したいタグの組によるMapを足し込みます。

scala> r = r |+| Map("www.abc.com" -> Vector("summer"))
r: scala.collection.immutable.Map[String,Vector[String]] = Map(www.abc.com -> Vector(spring, summer))

別のサイトのMapを足し込むと、Mapの別のエントリとして管理されます。

scala> r = r |+| Map("www.xyz.com" -> Vector("autumn"))
r: scala.collection.immutable.Map[String,Vector[String]] = Map(www.xyz.com -> Vector(autumn), www.abc.com -> Vector(spring, summer))

まとめ

値の集まりをMapで管理する処理は、そう頻繁に使うものでもないので毎回必要となる度に実現方式やロジックを考えることになりがちでした。Scalaの場合、mutableなMapの場合はMultiMapを使う方法もありますが、immutableなMapはこの方法は取れないようです。その場合はそれなりのロジックを組む必要が出てきます。

ということでWebで実現方式を調べていた所、以下のページにScalazを使う方法が載っていたというわけです。

いわれてみればこの方法があったか、ということで最近はこのパターンを愛用しています。

Monoid Index

昔調べた記事です。

Monoidに関する2012年ごろのブログのまとめです。

OptionをMonoidで使うと便利という記事。

Scalaプログラミングの肝はMonoidにあり、というわけで結構Monoidは調べていたつもりだったのですが、大きな応用が抜けていました。他にも何か大きな応用があるかもしれません。

諸元

  • Java 1.7.0_75
  • Scala 2.11.6
  • Scalaz 7.1.0

2015年10月30日金曜日

[SDN] 値クラスでケースクラスの型を強化

Scalaでは仕様をできるだけ型化するのがプログラミングのキモとなります。

積極的に型化するメリットはコンパイラでバグの検出をしてもらえる点にあります。Curry-Howard対応によってコンパイルがプログラムが仕様通りに実装されていることの証明となる点がScalaのような関数型言語を使う重要なメリットです。

しかし、ここでいう「仕様」とは型と(型で引数と復帰値を定義した)関数でのみ記述できるので、この「型」化できなかった「仕様」はコンパイラの証明対象から外れてしまいます。(テストプログラムで明示的にテストする必要があります。)

つまり、Scalaプログラミングでは「仕様」をいかに「型」として記述するのかが勝負どころといえます。

この型化を促進する機能の一つが値クラスです。

値クラスは、Longといった基本型の値やStringへの参照値といった値を、新しく定義した型として使用できるようにしたものです。(Value Classes and Universal Traits)

値クラスを使うことで、型を表現する具体的なオブジェクトを生成することなく値に対して適切な型チェックをおこなうことができるようになります。

ケースクラスの設計の際に値クラスを使って型を強化する方法について考えてみます。

基本形

アカウント管理の以下のケースクラスを素材に考えます。

case class Account(
  accountId: String,
  username: String,
  address: String
)
問題点

このケースクラスは実用上は十分ですが、誤った情報を設定されても検知できないという問題があります。

たとえば、以下のようにString型の変数addressは、Accountのaddress属性だけでなくaccountId属性、username属性にも設定することができます。

scala> val address = "Yokohama"
scala> val a = Account(address, address, address)
val a = Account(address, address, address)
a: Account = Account(Yokohama,Yokohama,Yokohama)

これは明らかなバグですがコンパイラではチェックすることができません。

typeで定義

Scalaではtypeキーワードを使って型の別名をつけることができます。

プログラムの可読性が向上する便利な機能です。

type AccountId = String
  type UserName = String
  type Address = String

case class Account(
  accountId: AccountId,
  username: UserName,
  address: Address
)
問題点

ただ別名は型そのものの定義をおこなうわけではないので、問題点は解消されません。

scala> val address = "Yokohama"
scala> val a = Account(address, address, address)
a: Account = Account(Yokohama,Yokohama,Yokohama)

case classを使う

型を明示的に使う場合、クラスを定義します。

今回の場合、以下のようにaccountId, username, addressに対応するクラスを定義します。用途的にはケースクラスが適切なのでケースクラスとして定義しています。

case class AccountId(v: String)
case class UserName(v: String)
case class Address(v: String)

新しく定義した3つのケースクラスAccountId, UserName, Addressを使ったAccountクラスは以下になります。

case class Account(
  accountId: AccountId,
  username: UserName,
  address: Address
)
問題点の解決

まずaddressとして文字列を指定した場合ですが、ケースクラスAccountIdとは型が違うので無事コンパイルエラーとなりました。

scala> val address = "Yokohama"
scala> val a = Account(address, address, address)
<console>:34: error: type mismatch;
 found   : String
 required: AccountId
       val a = Account(address, address, address)
                       ^

ケースクラスAddressを使用した場合も、コンパイルエラーとしてバグを検出できています。

scala> val address = Address("Yokohama")
scala> val a = Account(address, address, address)
<console>:34: error: type mismatch;
 found   : Address
 required: AccountId
       val a = Account(address, address, address)
                       ^

ケースクラスAccountId, UserName, Addressを使うと、正しくAccountオブジェクトの生成を行うことができました。

scala> val accountid = AccountId("A1234")
scala> val username = UserName("Taro")
scala> val address = Address("Yokohama")
scala> val a = Account(accountid, username, address)
a: Account = Account(AccountId(A1234),UserName(Taro),Address(Yokohama))
新たな問題点

基本的にはこの解で十分なのですが、(それぞれ属性を1つしか持たない)ケースクラスAccountId, UserName, Addressを導入したので、Accountオブジェクトを生成する際に生成されるオブジェクト数が増えてしまうという新たな問題がでてきました。

ケースクラスAccountId, UserName, Addressを導入しない前に生成されるオブジェクト数はAccountオブジェクト1つですが、ケースクラスAccountId, UserName, Address導入後はAccountオブジェクトに加えてAccountId, UserName, Addressの各オブジェクトも生成されるので4個のオブジェクトが生成されることになります。実に4倍の量のオブジェクトが生成されるわけです。属性をすべてこの方法で型化すると、1つのオブジェクトを生成毎にこの副作用も含めて1+属性数個のオブジェクトが生成されるようになります。

オブジェクトの生成数が増えると、オブジェクト生成のオーバーヘッドで処理の遅延が発生するだけでなく、ガベージコレクタの負担の増大、メモリ使用量の増大という問題が起きてきます。

小さなアプリケーションの場合はこれでも問題ありません。逆に型化することのメリットの方が大きいので、多少オーバーヘッドがあってもこの手法を採用するほうが得策です。

しかし大規模アプリケーションやフレームワーク的な基盤機能では見過ごすことができない重大な問題となります。

この問題への対応策として使用できるのが値クラスです。

値クラスを使う

属性を一つだけ持つクラスがAnyValをextendsすると値クラスとして定義されたことになります。

通常のクラスでもケースクラスでも使用できます。

ここでは3つのケースクラスを値クラスとして定義しました。

case class AccountId(v: String) extends AnyVal
case class UserName(v: String) extends AnyVal
case class Address(v: String) extends AnyVal

この3つのケースクラスを使ったケースクラスAccountは以下になります。特別な考慮は不要です。

case class Account(
  accountId: AccountId,
  username: UserName,
  address: Address
)

値クラスは通常のクラスに比べるとできることの制約もありますが、今回の用途では普通のクラスと同じように使用することができます。

問題解決の確認

それでは値クラスを使った場合でも、問題点が解決しているかを確認しましょう。

まずaddressとして文字列を指定した場合ですが、ケースクラスAccountIdとは型が違うので無事コンパイルエラーとなりました。

scala> val address = "Yokohama"
scala> val a = Account(address, address, address)
<console>:34: error: type mismatch;
 found   : String
 required: AccountId
       val a = Account(address, address, address)
                       ^

ケースクラスAddressを使用した場合も、コンパイルエラーとしてバグを検出できています。

scala> val address = Address("Yokohama")
scala> val a = Account(address, address, address)
<console>:34: error: type mismatch;
 found   : Address
 required: AccountId
       val a = Account(address, address, address)
                       ^

ケースクラスAccountId, UserName, Addressを使うと、正しくAccountオブジェクトの生成を行うことができました。

scala> val accountid = AccountId("A1234")
scala> val username = UserName("Taro")
scala> val address = Address("Yokohama")
scala> val a = Account(accountid, username, address)
a: Account = Account(AccountId(A1234),UserName(Taro),Address(Yokohama))

まとめ

ケースクラスの設計時に値クラスを使って型を強化する方法について考えてみました。

ケースクラスの属性を値クラス化したケースクラスを使うだけなので若干コーディングの手間はかかるものの簡単に実現できることが分かりました。

値クラスを使用することで、型によるエラーチェックを強化を行いつつ、オブジェクトの生成回数、メモリの使用量の増大を回避できることが期待できます。

Scala Design Note

2015年9月30日水曜日

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

『圏論の歩き方』を読んで少し理解が進んだので、関数型プログラミング技術マップを更新しました。「関数型プログラミング技術マップ2014」の2015年版です。

以下の点を改良しています。

  • Curry-Howard対応をCurry-Howard-Lambek対応に拡張
  • 直観主義述語論理を追加して直観主義命題論理を包含
  • カルテジア閉圏とトポス(圏)を追加
  • 直観主義命題論理⇔カルテジアン閉圏、単純型付ラムダ計算⇔カルテジアン閉圏間の関係を追加

この図は関数型プログラミング(FP: Functional Programming)を取り巻く理論を整理することを目的としています。

誤解があるといけないので補足しておきますがFPを行うために必須の理論という意図ではありません。

業務アプリケーションをFPで開発するという目的には、圏論も論理学も抽象代数も必須知識ではなく、MonoidやMonadのプログラム上での使い方をパターンとして覚えておけば十分だと思います。代数的データ型もcase classの筋の良い使い方を覚えてしまえば大丈夫です。(もちろんFPとして筋の良いプログラミングをするためには、こういった理論を知っておいた方がよいのは言うまでもありません。)

一方、ビジネス・モデリングや要件定義といった上流のモデリングとFPとの連携を考えていく際には、こういった理論も取り込んでいく必要がありそうです。

OOAD(Object-Oriented Analysis and Design)はUML/MOF(Meta Object Facility)によるようなメタモデルの議論はあるものの、現実的には数学や情報科学とは一定の距離がある現場ベースのベストプラクティスの集大成といえます。OOADによるモデルをOOPで実装するという目的には、数学や情報科学の知識は(あった方がよいのは確かですが)必須スキルという形ではなかったと思います。

しかし、実装技術としてFPが導入されると上流モデルとFPとの連携が論点となってきます。

こういった「FP成分」を取り込んだOOADをOFAD(Object-Functional Analysis and Design)と呼ぶとすると、このOFADでは数学や情報科学をベースとした数理モデルを部分的にでも取り込んでいくことになるかと思います。

一つの切り口としては、OOADのモデルが静的構造モデル、動的モデル、協調モデルから構成されるとすると、(記述力が弱い)協調モデルを数理モデルベースのデータフローで記述し、静的構造モデル、動的モデルを数理モデルとの連続性を担保できるように強化する、といった戦略が考えられます。

このためのモデルとしてどのようなものを採用するのがよいのか分かりませんが、Curry-Howard対応あるいはCurry-Howard-Lambek対応による直観主義命題論理、単純型付ラムダ計算、カルテジアン閉圏によるトライアングルが中心になることが予想されます。

もちろん、一階述語論理/論理プログラミング(Prologなど)や直観主義高階述語論理/証明プログラミング(Coqなど)といった方向性も有力ですが、Scala&ScalazによるFPでは述語論理は(言語機能的には)スコープ外なので、仮に上流モデルで取り入れたとしてもプログラミングとは不連続になってしまいます。

また、一階述語論理/論理プログラミングや直観主義高階述語論理/証明プログラミングが最終的な解であるにしてもその前提として「Curry-Howard-Lambek対応」の理解は必要です。

そういった意味で、まずは「Curry-Howard-Lambek対応」のスコープで色々と考えていくのがよさそうと考えています。

2015年8月24日月曜日

[scalaz]Task - 並列処理

先日の「Reactive System Meetup in 西新宿」で「Scalaz-StreamによるFunctional Reactive Programming」のスライドを作るにあたってscalazのTaskについて調べなおしてみたのですが、Taskの実用性について再確認することができました。

色々と切り口がありますが、その中で並列性能が今回のテーマです。

準備

準備として以下のものを用意します。

implicit val scheduler = new java.util.concurrent.ForkJoinPool(1000)

  def go[T](msg: String)(body: => T): Unit = {
    System.gc
    val ts = System.currentTimeMillis
    val r = body
    println(s"$msg(${System.currentTimeMillis - ts}): $r")
  }

  def fa(i: Int): Task[Int] = Task {
    Thread.sleep(i)
    i
  }

スレッド実行コンテキスト(ExecutorService)にはForkJoinPoolを使用します。

ForkJoinPoolは分割統治(devide and conquer)により並行処理を再帰的(recursive)に構成する処理向けのスレッドスケジュールを行います。ざっくりいうと並列で何らかの計算を行う処理全般に向くスケジュールといえるのではないかと思います。IO処理の場合も復数のIO発行後に同期待ち合わせをするケースでは同様にForkJoinPoolが有効と思われます。

今回の性能検証では1000並列させたいのでパラメタで指定しています。

goメソッドは性能測定用のユーティリティです。

fa関数は性能測定対象の関数です。関数faは指定されたマイクロ秒間ウェイトして指定されたマイクロ秒を返す関数をTask化したものです。

問題

以下は性能検証の課題のプログラムです。fa関数の呼出しを1000回逐次型で行います。

Vector.fill(1000)(fa(1000)).map(_.run).sum

関数faは指定されたマイクロ秒間ウェイトして指定されたマイクロ秒を返す関数をTask化したものです。これを1000回繰り返したものを合計したものを計算します。

パラメタでは1000マイクロ秒=1秒を指定しているので1000回繰り返すと16.7分程かかる処理になります。

性能検証

Taskの並列処理を行う以下の関数について性能測定を行いました。

  • Task/gatherUnordered
  • Task/reduceUnordered
  • Nondeterminism/gather
  • Nondeterminism/gatherUnordered
  • Nondeterminism/reduceUnordered
  • Nondeterminism/aggregate
  • Nondeterminism/aggregateCommutative

以下ではそれぞれの関数について説明します。

gatherUnordered(Task)

TaskのgatherUnordered関数の性能測定対象プログラムは以下になります。

def apply_gather_unordered_task: Int = {
    val xs = Vector.fill(1000)(fa(1000))
    val t = Task.gatherUnordered(xs).map(_.sum)
    t.run
  }

TaskのgatherUnordered関数はNondeterminismのgatherUnordered関数とよく似ていますが、並列実行しているTaskの1つが例外になった時に処理全体を止める機能を持っている点が異なります。デフォルトではfalseになっているので、ここではこの機能は使っていません。

unorderedつまり結果順序は元の順序を維持していない(計算が終わった順の可能性が高い)ことは待ち合わせ処理を最適化できる可能性があるので実行性能的には好材料です。一方、アルゴリズム的には順序が保たれていることが必要な場合があるので、その場合はgatherUnorderedは使用することは難しくなります。

可換モノイド(commutative monoid)は演算の順序が変わっても結果が同じになることが保証されているので、並列処理結果が可換モノイドであり、並列処理結果の結合処理が可換モノイドの演算である場合は、並列処理結果が元の順序を保持している必要はありません。つまりgatherUnorderedを使っても全く問題ないわけです。

Intは「+」演算に対して可換モノイドなので、並列処理結果の総和を計算するという結合処理向けにgatherUnorderedを使うことができます。

reduceUnordered(Task)

TaskのreduceUnordered関数の性能測定対象プログラムは以下になります。

def apply_reduce_unordered_task: Int = {
    val xs = Vector.fill(1000)(fa(1000))
    val t = Task.reduceUnordered(xs)(Reducer.identityReducer)
    t.run
  }

並列処理を行った後で、復数の処理結果をまとめる場合にはreduce機能を使用すると意図が分かりやすいですし、共通処理内での最適化も期待できます。

TaskのreduceUnorderedはscalazのReducerを使って、並列処理結果のreduce処理を行う関数です。NondeterminismのreduceUnordered関数とよく似ていますが、並列実行しているTaskの1つが例外になった時に処理全体を止める機能を持っている点が異なります。デフォルトではfalseになっているので、ここではこの機能は使っていません。

並列処理の結果得られるデータはIntで、Intは可換モノイドですから、Monoidの性質を使ってreduce処理を行うことができます。そこで、ReducerとしてidentityReducer(処理結果のMonoidをそのまま使ってreduce処理を行う)を指定しています。

gather(Nondeterminism)

Nondeterminismのgather関数の性能測定対象プログラムは以下になります。

def apply_gather: Int = {
    val xs = Vector.fill(1000)(fa(1000))
    val t = Nondeterminism[Task].gather(xs).map(_.sum)
    t.run
  }

「Nondeterminism[Task]」は型クラスNondeterminismのTask向け型インスタンスの意味です。つまりTaskはNondeterminismでもあるので、Nondeterminismのgather関数を実行することができます。

gather関数はNondeterminismデータシーケンスに対してそれぞれの要素を並列処理し、シーケンスの順序を維持した結果を計算します。

上記ではその結果得られたIntシーケンスをsum関数で合算しています。

gatherUnordered(Nondeterminism)

NondeterminismのgatherUnordered関数の性能測定対象プログラムは以下になります。

def apply_gather_unordered: Int = {
    val xs = Vector.fill(1000)(fa(1000))
    val t = Nondeterminism[Task].gatherUnordered(xs).map(_.sum)
    t.run
  }

TaskのgatherUnordered関数と同様に指定された並列処理の順序を保持せず、処理結果を順不同でシーケンスとして返します。

結果としてIntシーケンスが返ってきますが、Intは可換モノイドの性質を持つため順不同で返ってきてもsum関数で合算して問題ありません。

reduceUnordered(Nondeterminism)

NondeterminismのreduceUnordered関数の性能測定対象プログラムは以下になります。

def apply_reduce_unordered: Int = {
    val xs = Vector.fill(1000)(fa(1000))
    val t = Nondeterminism[Task].reduceUnordered(xs)(Reducer.identityReducer)
    t.run
  }

型クラスNondeterminismのreduceUnordered関数を使って並列実行と実行結果のreduce処理を行います。

TaskのreduceUnorderedの場合と同じくReducerとしてidentityReducerを指定しています。

aggregate(Nondeterminism)

Nondeterminismのaggregate関数の性能測定対象プログラムは以下になります。

def apply_aggregate: Int = {
    val xs = Vector.fill(1000)(fa(1000))
    val t = Nondeterminism[Task].aggregate(xs)
    t.run
  }

aggregate関数はreduceUnordered関数と並列実行後にreduce処理を行う点では同じ系統の計算を行いますが、以下の点が異なります。

  • aggregate関数はMonoidを前提としておりMonoidの性質を利用してreduce処理を行う。それに対してreduceUnordered関数はreduce処理を行うアルゴリズムをReducerとして指定する。
  • Monoidは可換モノイドとは限らないので並列計算の順序が保存されている必要がある。このためaggregate関数は並列実行順序を保存する処理を行っている。それに対してreduceUnordered関数は並列実行順序を保存する処理を行っていない。

aggregateは(可換モノイドでないかもしれない)Monoidを処理対象にしているため、並列計算の順序を保存する処理が必要になるので、その分性能的には不利になります。

この問題に対する改良策が次のaggregateCommutative関数です。

aggregateCommutative(Nondeterminism)

NondeterminismのaggregateCommutative関数の性能測定対象プログラムは以下になります。

def apply_aggregate_commutative: Int = {
    val xs = Vector.fill(1000)(fa(1000))
    val t = Nondeterminism[Task].aggregateCommutative(xs)
    t.run
  }

aggregateCommutative関数はaggreagte関数と同様にMonoidを処理対象としていますが、指定されたMonoidが可換モノイドであるという前提で計算を行います。

可換モノイドであるということは、演算の評価順序が異なっても同じ結果になるということなので、指定された並列計算シーケンスの実行順序を保存する処理は不要です。並列計算シーケンスの実行順序を保存する処理が不要になると実行性能的に有利です。

可換モノイドとモノイドの違い(つまり可換性)は並列処理で重要ですが、現在のところScalazには可換モノイドを表現する型クラスは用意されていないので、型を使ってエラーチェックを行ったり(例:可換性前提の関数で可換性なしのデータを使用できないようにチェック)、最適化(例:可換性の有無で実行順序の保存処理の有無を切り替える)を行うようなことはできません。

aggregateCommutative関数で行っているように、使用者側が違いを意識して使う形になります。

性能

クラスメソッド性能(ms)順序保存集約機能キャンセル機能
TaskgatherUnordered2139--
TaskreduceUnordered1869-
Nondeterminismgather1082--
NondeterminismgatherUnordered1048 ---
NondeterminismreduceUnordered1718--
Nondeterminismaggregate1049-
NondeterminismaggregateCommutative1040--

評価

表では各関数を以下の性質の有無で分類しました。

  • 順序保存
  • 集約機能
  • キャンセル機能

それぞれの性質毎に性能特性を考えます。

順序保存

評価順序保存をすると順序保存なしより少し遅くなります。

可能であれば順序保存なしを選ぶのがよいでしょう。

順序保存なしを選べるかどうかは、各並列処理の計算結果がreduce処理の演算に対して可換モノイド(commutative monoid)であるかどうかで判定できます。

整数の加算や積算は典型的な可換モノイドなので、最終的な計算結果が合算処理(全要素の加算)の場合は「順序保存なし」を選べるわけです。

集約機能

並列実行関数に集約機能が包含されていると、各並列処理の結果を使って直接集約処理を行うことができるので効率的です。一度、並列実行結果をリスト上に保存して、そのリストに対して集約するより、集約対象のデータ(例:整数値)を各並列処理の完了後に直接更新して行く方がオーバーヘッドは少なくなります。

集約機能を提供しているaggregate関数とaggregateCommutative関数が、それぞれ対応するgather関数、gatherUnorderedと比較して若干速いのはそのためだと思われます。

Monoidは集約対象として優れた性質を持っているので、集約機能の対象として使用するのがFunctional Programming(以下FP)のパターンになっています。aggregate関数とaggregateCommutative関数はこのパターンに則って、集約機能の対象としてMonoidを使用します。

一方Reducerを使った集約は大きなオーバーヘッドがあるようなので、積極的に利用する価値があるという感じではないようです。

NondeterminismのgatherUnordered関数とreduceUnordered関数の比較ではreduceUnordered関数がかなり遅くなっています。この場合、Reducer経由でMonoidの集約を行っているので、Monoidを直接集約するよりオーバーヘッドがあるのが原因と思われます。

一方TaskのgatherUnordered関数とreduceUnordered関数の場合、reduceUnordered関数の方が速いので、こちらの場合はreduceUnordered関数の利用は有力な選択肢です。キャンセル機能が重たいためにReducer機能の遅さが隠れてしまうのかもしれません。

キャンセル機能

キャンセル機能はTaskのgatherUnordered関数、reduceUnordered関数が提供しています。

NondeterminismのgatherUnordered関数、reduceUnordered関数と比較すると相当遅くなっています。キャンセル機能が必要でない場合は使わない方がよいでしょう。

まとめ

性能測定の結果、並列処理結果を可換モノイドで受け取り集約処理を行うaggregateCommutative関数が一番高速であることが分かりました。

並列処理実行後の集約処理までを一体化して処理の最適化ができるのは可換モノイドの効果です。

並列処理を設計する際には、各並列処理の結果を可換モノイドで返すような形に持ち込むことができるのかというのが一つの論点になると思います。

また可換モノイドにできない場合も、モノイドにできれば汎用関数で集約まで行うことができるので、並列処理を記述する上で大きな助けになります。

Scalazではモノイドを記述する型クラスMonoidが用意されています。MonoidはScalazによるFPで中心的な役割を担う型クラスの一つですが、並列処理においても重要な役割を担うことが確認できました。

Scalazでは可換モノイドを記述する型クラスはまだ用意されていないので、Monoidで代用することになります。aggregateCommutative関数のように引数の型としてはMonoidを使い、暗黙的に可換モノイドを前提とするような使い方になると思います。

メニーコアによる並列計算が本格化するのはもう少し先になると思いますが、その際のベースとなる要素技術はすでに実用技術として利用可能になっていることが確認できました。FPが並列計算に向いているという期待の大きな部分は、モノイドや可換モノイドのような数学的な概念をプログラミング言語で直接使用できる点にあります。Scala&Scalazはこれを実用プログラミングで可能にしているのが大きな美点といえるかと思います。

諸元

  • Mac OS 10.7.5 (2.6 GHz Intel Core i7)
  • Java 1.7.0_75
  • Scala 2.11.6

2015年8月17日月曜日

クラウドアプリケーション・モデリング考

8月7日に「匠の夏まつり ~モデリングの彼方に未来を見た~」のイベントが行われましたが、この中でパネルディスカッションに参加させていただきました。パネルディスカッションでご一緒させていただいた萩本さん、平鍋さん、高崎さん、会場の皆さん、どうもありがとうございました。

パネルディスカッションがよいきっかけとなって、クラウドアプリケーション開発におけるモデリングについての方向性について腰を落として考えることができました。このところFunctional Reactive Programmingを追いかけていましたが、ちょうどモデリングとの接続を考えられる材料が揃ってきているタイミングでした。

パネルディスカッションの前後に考えたことをこれまでの活動の振り返りも含めてまとめてみました。

基本アプローチ

2008年頃からクラウドアプリケーション開発の手法について以下の3点を軸に検討を進めています。

  • クラウド・アプリケーションのアーキテクチャ
  • メタ・モデルと実装技術
  • モデル駆動開発

検討結果は以下にあげるスライドとブログ記事としてまとめていますが、基本的な考え方は現在も同じです。

ざっくりいうと:

  • クラウド・アプリケーションのバックエンドのアーキテクチャはメッセージ方式になる。
  • クラウド・アプリケーションのモデリングではOOADの構造モデル、状態機械モデルを踏襲。
  • 協調モデルの主力モデルとしてメッセージフローまたはデータフローを採用。
  • OOADの構造モデル、状態機械モデルはモデル駆動開発による自動生成。
  • メッセージフローまたはデータフローはDSLによる直接実行方式が有効の可能性が高い。

という方針&仮説です。「OOADの構造モデル、状態機械モデルはモデル駆動開発による自動生成」についてはSimpleModeler、「メッセージフローまたはデータフローはDSLによる直接実行方式」についてはg3 frameworkで試作を行っていました。

ここまでが2010年から2012年中盤にかけての状況です。

ブログ

2012年以降のアプローチ

2012年の後半にEverforthに参画してApparel Cloudを始めとするCloud Service Platformの開発に注力しています。

前述の論点の中で以下の3点についてはApparel Cloudの開発に直接取り入れています。

  • クラウド・アプリケーションのバックエンドのアーキテクチャはメッセージ方式になる。
  • クラウド・アプリケーションのモデリングではOOADの構造モデル、状態機械モデルを踏襲。
  • OOADの構造モデル、状態機械モデルはモデル駆動開発による自動生成。

「メッセージフローまたはデータフローはDSLによる直接実行方式が有効の可能性が高い」については当初はg3 frameworkという独自DSLによる実装を考えていたのですが、Object-Functional Programmingの核となる技術であるモナドがパイプライン的なセマンティクスを持ち、データフローの記述にも使用できそうという感触を得られたため、ScalazベースのMonadic Programmingを追求して技術的な接点を探るという方針に変更しました。

2012年以降ブログの話題がScalaz中心になるのはこのためです。

その後、まさにドンピシャの技術であるscalaz-streamが登場したので、scalaz-streamをApparel Cloudの構築技術として採用し、「メッセージフローまたはデータフローはDSLによる直接実行方式が有効の可能性が高い」の可能性を実システム構築に適用しながら探っている状況です。

今後のアプローチ

現在懸案として残っている項目は以下のものになります。

  • 協調モデルの主力モデルとしてメッセージフローまたはデータフローを採用。

前述したようにメッセージングのDSLとしてはscalaz-streamをベースにノウハウを積み重ねている状況なので、この部分との連続性をみながらモデリングでの取り扱いを考えていく予定です。

また、ストリーミング指向のアーキテクチャ&プログラミングモデルとしては以下のような技術が登場しています。

このような新技術の状況をみながら実装技術の選択を行っていく予定です。

参考: スライド

パネルディスカッションでのポジション宣言的なスライドとして以下のものを作成しました。

この中で6ページ目の「Cloud時代のモデリング」が今回パネルディスカッションのテーマに合わせて新規に作成したものです。

このスライドで言いたいことは、伝統的なスクラッチ開発とくらべてクラウドアプリケーションではプログラミング量が大幅に減るので、要件定義やその上流であるビジネスモデリングが重要になる、ということです。

  • アプリケーションの大きな部分はCloud Service Platformが実現
  • モデル駆動開発によってドメインモデル(静的構造)の大部分は自動生成される
  • Scalaで実現されているDSL指向のOFP(Object-Functional Programming)は記述の抽象度が高いので設計レベルのモデリングは不要
  • Scalaの開発効率は高いのでプログラミングの比重は下がる
補足:Featureモデル

後日スライドのキーワードページに入れておくべきキーワードとしてFeatureモデルがあることに気付いたので、上記のスライドには追加しておきました。

スライドの想定する世界では、クラウドアプリケーションはクラウドサービスプラットフォーム上で動作するため、クラウドサービスプラットフォームが提供している機能とクラウドアプリケーションの機能の差分をモデル化し、このモデルを元に実際に開発する所、カスタマイズで済ませる所などを具体化していく必要があります。この目的にはSoftware ProductlineのFeatureモデルが有効ではないかと考えています。