ラベル modeling の投稿を表示しています。 すべての投稿を表示
ラベル modeling の投稿を表示しています。 すべての投稿を表示

2020年8月31日月曜日

SimpleModel

簡単なWebサイトの開発などはDBスキーマ設計+プログラミングで十分なケースも多いと思いますが、複雑な業務や高度なユーザー経験を実現する場合は、何らかの分析設計を行い、その結果を元に実装に落とし込む必要があります。この分析設計手法の最右翼がOOAD(Object-Oriented Analysis and Design)です。

しかし、OOADは難しい技術で独学でマスターするのはなかなか大変です。

独学でマスターすることが大変な理由として、そもそもOOADがソフトウェア工学の集大成という位置付けのものなのでカバーする技術分野の範囲も広く難易度が高いということがあります。その上で独学に適したよい教科書がないということもあります。また、モデリングをアシストするためのツール類の不足も大きな問題です。

以前、大学でモデリングを教えていた時、授業やゼミで使えそうな教科書がないことが悩みのタネでした。

一つは具体的なモデリングの手順を解説した本。UMLの文法解説の本は多いのですが、最上流から実装までの流れを順を追って解説している本がなかなかありません。

また、UMLはすべての応用に適用できるように仕様が巨大でチューニング項目も沢山ありますが、ターゲットのシステム開発向けに必要最小限の大きさにカスタマイズして使用することが想定されています。この「ターゲットのシステム開発向けに必要最小限の大きさにカスタマイズ」してあるUMLのカスタマイズ仕様、いわゆるプロファイルが定義されている本はほぼないと思います。つまり、UMLの記法を学んだだけではUMLを活用することはできないわけです。

そこで中小規模の業務アプリケーションをターゲットに整備したメタモデルとしてSimpleModelを設計しました。

SimpleModelをベースにこれらの問題を解決するために授業やゼミで使える教科書の目的で以下の2つの本を書きました。

UMLを記述言語として書いたものが『上流工程UMLモデリング』、マインドマップを記述言語としたものが『マインドマップではじめるモデリング講座』です。

『上流工程UMLモデリング』が教科書、『マインドマップではじめるモデリング講座』がゼミでのワークショップ用という位置付けです。

ソフトウェア構築技術の進化

SM2008を作成したのが2008年前後ですが、すでに12年の月日が流れており、ソフトウェアの構築技術にも大幅な変化がありました。

特に以下の3つの分野での大きな進展があります。

  • クラウド
  • AI
  • DX(Digital Transformation)
クラウド

クラウドの登場によって大きく変わったのは、大規模アクセス・大規模データのアプリケーションを簡単に構築できるようになった点だと考えています。大規模アクセス・大規模データのアプリケーション構築するための部品が多数用意され、運用管理コストを含めたコストが安価に提供されるようになりました。

逆に言うと大規模アクセス・大規模データの業務アプリケーションを開発する必然性が生まれたわけで、スケールアウトや非同期処理・分散処理を達成するための技術力が求められるようになりました。

それに伴い、以下のようなアーキテクチャが登場してきました。

  • CQRS
  • Event Sourcing
  • Microservice

これらのアーキテクチャに対して、業務アプリケーションのモデリング上でどのように対峙していくのかが論点になるでしょう。

AI

業務アプリケーションの開発においてもAIは無視できない存在になってきました。

ただ、業務アプリケーションそのものがAIのエンジンを開発するということはなく、既存のAIエンジンを使用し、数理モデルを新規開発または既存モデルの改良という形で利用していくことになるケースがほとんどだと思います。

AIも大きく従来型の知識ベースや推論エンジンによるAIと、最近流行の機械学習型の2種類があります。

前者については、AIシステムをコンポーネントやサブシステムとしてモデル化し、利用方法を考えていく形になるでしょう。後者については利用方法を設計するのに加え、学習に必要な情報の収集方式についても検討する必要がでてきます。

DX

DXはもともと「ITの浸透が、人々の生活をあらゆる面でより良い方向に変化させる」という意味とのことですが、ソフトウェア開発の文脈では、企業活動のあらゆる側面をIT化して、企業の形態をIT中心に変換する、というようなニュアンスで語られることが多いと思います。

DXの文脈では、単なる業務アプリケーションの開発ではなく、企業活動全体を俯瞰した企業システムの構築の一環として業務アプリケーションを開発することになります。業務アプリケーションの設計ではなく、企業システム全体をモデル化した上で、その中に組み込まれる1コンポーネントという形での設計になってくるでしょう。

こうなってくると、プログラミング主導の開発では不十分で、全体像を俯瞰し、ターゲットの開発スコープを明確にするモデルの作成が必要なのは明らかです。

SimpleModel 2020

このように、SimpleModelを開発した2008年当時から見るとシステム開発を取り巻く要素技術は大幅に進化しています。この技術進化を取り込むためにSimpleModelの拡張を行うことにしました。その内容については本ブログでこれから検討していくことにします。

前述の書籍で使用しているSimpleModelをSimpleModel 2008年版と呼ぶことにします。そして、このブログでこれから検討していく新しい時代背景に対応したSimpleModelをSimpleModel 2020年版と呼ぶことにします。また簡易表記はSimpleModel 2008年版をSM2008、SimpleModel 2020年版をSM2020とします。

記述言語

前回説明したとおり、モデルコンパイラであるSimpleModelerの記述言語はSmartDoxのDSL基盤上に構築されています。すなわち、節による木構造、表による表構造を使用してのモデル定義と、自然言語による説明文を自然な形で混在させることができます。

文芸的プログラミング(Literature Programming)ならぬ文芸的モデリングということができます。

ツール

OOADによるモデリングを実務に適用するために重要なことは使いやすくて実効性のあるツールによるアシストです。

従来からUMLエディアなどのツールは存在しましたが、実務にモデリングを適用させたり、モデリング教育を効果的に行うといった目的には十分な機能を提供しきれていなかったともいます。

モデリングで作成するモデルは大きく以下の2つに分けることができます。

  • 実行可能モデル
  • 青写真モデル

実行可能モデルは、作成したモデルがプログラムとしてそのまま動いたり、プログラムを自動生成できる、といった形でプログラム開発に直接連携できるモデルです。

一方、青写真モデルはあくまで参考として使える情報であり、実際のプログラムには手作業で開発することになります。

従来モデリングが活用されてこなかった理由の一つは、せっかく苦労してモデルを作っても青写真モデル止まりだったことが大きいと思います。

モデリング活用の阻害要因であるこれらの問題に対応するため、2008年以降、Scalaを使って以下のプロダクトを整備してきました。

SmartDox
文書処理系
SimpleModeler
モデルコンパイラ
Kaleidox
アクション言語
Arcadia
Webフレームワーク
PreferCloudPlatform
クラウド・アプリケーション・プラットフォーム

これらのツールは現在SM2008向けになっていますが、SM2020のメタモデルをベースに拡張していく予定です。

まとめ

業務アプリケーション向けメタモデルであるSimpleModelの最新版SimpleModel 2020について紹介しました。

SM2020の具体的な内容については本ブログで順次検討していきたいと思います。

次回はSM2020を検討する上での論点を整理したいと思います。

2016年9月30日金曜日

クラウド・アプリケーション開発のモデル体系

前回「クラウド・アプリケーション・モデリング」ではACP(Application Cloud Platform)時代のアプリケーション開発におけるモデリングについて、開発プロセスの点から考えてみました。

今回はモデル体系(メタモデル/プロファイル)について考えてみることにします。

クラウド・アプリケーションの開発ではアプリケーション開発のみを考えるのではなくBusiness→Development→Operation→Evaluationによる一種のPDCAループをビジネススコープで回していくことが重要になります。

Business→Development→Operation→Evaluationループを回すためには持続化可能な形で各アクション間で情報を持ちまわる必要があります。この情報はDevelopmentで使用するオブジェクト・モデルと連続性のあるものが要求されるので、Businessでもオブジェクト・モデルを使用するのが自然です。

このため、ITシステム開発のアクションであるDevelopment(以下単にDevelopment)はもちろんですが、Businessアクション(以下単にBusiness)でのオブジェクト・モデルが非常に重要になってきます。

今回はこのような枠組みで開発方法論を考える上でベースとなるオブジェクト・モデルのリファレンス・モデルについて考えます。

全体像 

Business ProcessによるBusinessのモデルとDevelopmentでのITシステムのモデルの全体像を整理しました。おおまかな関係を示すのが目的なので、詳細は省いて強調したい部分のみを載せる形にしています。

大きく3つの領域があります。

  • Business Process Model :: ビジネス・プロセスを記述。
  • IT System Model :: ITシステム向けのオブジェクト・モデルを記述。
  • Domain Model :: ビジネスとITで共有するドメイン・モデルを記述。

Business Process

Businessのモデリングの方法としては色々なアプローチが考えられるところですが、ITシステム開発とのシームレスな連携を念頭に置いた場合、Business Processを用いるのがよいのではないかと考えています。

ここでは『Business Modeling with UML: Business Patterns at Work』をベースにカスタマイズしたものを用います。

Business Process Modelはざっくり以下のモデル要素で構成されています。

  • Vision / Value / Goal :: ビジョン・価値・ゴールなど「何」を目標にしたいのかを記述。
  • Business UseCase/UX :: ビジネス要求を物語で記述。
  • Business Flow :: ビジネスの流れ/仕組みを記述。

「Vision / Value / Goal」はビジネスで「何」をするのかを記述するのに必要そうなモデル要素を代表させた項目です。ここに何を持ってくるのかはビジネス・モデリングの方法論で変わってきます。「Vision / Value / Goal」はその1例と考えてください。

IT System Modelとの連携で必要なのはBusiness UseCase/UXとBusiness Flowです。

また、Domain ModelをIT System Modelと共用する点が本質的に重要です。

要求仕様の記述にはBusiness UseCaseを使用します。UX(User Experience)とUseCaseの棲み分けは難しい課題ですが、いずれ考えてみたいと思います。ここではUseCaseを中心に、必要に応じてUXも併用、というような形でとらえて下さい。

IT System Model

IT System Modelはざっくり以下のモデル要素で構成されています。

  • UseCase/UX :: 要求仕様を物語形式で記述
  • Collaboration :: 物語を実現するためのシステム内外の協調動作
  • Responsibility :: システムが果たすべき責務
  • System/SubSystem :: システム/サブシステム。
  • Module/Component :: システム/サビスシステムを構成するソフトウェア部品。
  • Class/Object :: クラスとオブジェクト。オブジェクト・モデルの基本分類子。

Bisuness Processと同様にUseCaseとUXの関係は微妙ですが、ここではざっくりUseCase/UXとひとかたまりにして扱います。

IT System ModelではUseCase/UXからDomain Modelを睨みつつCollaborationを経てResponsibilityを抽出します。このResponsibityをIT System ModelおよびDomain ModelのSystem/Subsystem, Module/Component, Class/Objectに配置していきます。

基本的にオーソドックスな手順ですが、オブジェクト・モデリングは最近はロストテクノロジーになってしまっているきらいもあるので、ACPの要素も加味しつつ一度まとめる予定です。

Domain Model

Domain Modelは、Business Process ModelとIT System Modelから共用します。

  • Context Map :: Bounded Contextの対応関係のマップ。
  • Bounded Context :: ドメインの領域。Entityの集まり。
  • Entity :: エンティティ(ドメイン・オブジェクト)。
  • Relashionship :: 各種分類子間の関係。
  • StateMachine :: 分類子の振る舞いを記述する状態機械。
  • Business Rule :: ビジネスルール。

オブジェクト指向技術は、上流のビジネス・モデリングからオブジェクト指向プログラミングまでシームレスに連携できる点が最大の美点であるといえるでしょう。この美点の中軸となるのがDomain Modelです。

Domain ModelはBusiness ProcessとIT System Modelから共通のモデルとして共有されます。

基本的には通常のオブジェクト・モデルですが、以下の拡張を行っています。

  • Context MapとBounded ContextはDDD(Domain-Driven Design)の用語を採用。
  • Business Ruleを採用。

モデルの対応関係

図に示すモデル間の対応関係について説明します。

まず大事な点は、Domain ModelをBusiness Process ModelとIT System Modelから共用することです。Domain ModelはBusiness Process Modelの一部でもあり、IT System Modelの一部でもあるという関係になります。

Business Process ModelのBusiness UseCase/UXとBusiness FlowからIT System ModelのUseCase/UXを作成します。IT System ModelのUseCase/UXとDomain Modelが作成できれば、ここからは通常のオブジェクト指向開発の手順で開発を進めることができます。

まとめ

ACP向けのモデリングを考えるベースとなるオブジェクト・モデルについて整理しました。

オブジェクト・モデルをBusiness Process, IT System Model, Domain Modelの3つの領域に分類する枠組みとそれぞれの対応関係を考えてみました。

大きな枠組としては、2000年代前半に完成されたオブジェクト指向技術がベースで、セマンティクス的に大きな拡張は行っていません。業界全体でもここ10年程大きな動きはなかったと思います。

このベースに対して、ACPの登場、また関数型言語、リアクティブといった要素技術の実用化がどのようなインパクトを与えていくのかという点が1つの論点になります。

いずれにしても、新しい時代向けに開発方法論が進化するタイミングになっていると思います。本ブログでもこの観点から引き続き考察を続けていきたいと思います。

2016年8月31日水曜日

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

製品開発を通じて色々と経験を積むことができたので、一度腰を落としてクラウドアプリケーション開発向けの開発手法について考えてみることにしました。何回かに分けて考察していきたいと思います。

考察する開発手法は以下の2つの機能で構成されています。

  • 開発プロセス
  • モデル体系(メタモデル)

今回は開発プロセスの全体像についてまとめます。

クラウドアプリケーションは、スクラッチで開発するケースもあると思いますが、主流はクラウドアプリケーション向けのプラットフォーム上で必要な機能のみを開発することになるのではないかと推測しています。

この場合、クラウドアプリケーションの開発手法はクラウドアプリケーションのプラットフォーム上での開発を前提としたものが重要になってきます。

本稿ではこの前提の上で、クラウドアプリケーションのプラットフォーム上でのクラウドアプリケーション開発向けの開発手法をテーマにします。

アプリケーション開発の基盤となるクラウドプラットフォームをApplication Cloud Platform(以下ACP)と呼ぶことにします。

背景

クラウドアプリケーションは以下のような既存のアプリケーションの複合体と考えることができます。

  • a) 消費者向けUX指向のBtoCアプリケーション
  • b) 基幹システムや外部システムとの連携を行うInB, BtoB的な業務アプリケーション
  • c) 利用者同士を結びつけてコミュニティを構築するPtoPアプリケーション
  • d) 統合運用・管理システム
  • e) 蓄積されたデータに対する分析・評価基盤

このような複雑なシステムをスクラッチで開発することはコストもかかりますし、長期の開発期間を必要とするためビジネス的にタイムリーなタイミングでリリースすることが難しくなります。

ACPは上記のような機能をビルトインしたプラットフォームであり、UXを実現するWeb/モバイルアプリと必要最小限のサーバー機能を開発するだけで、アプリケーションを開発することができます。

このようにACP上で動作するアプリケーション開発を行う場合、スクラッチでアプリケーション全体を構築する場合とは、異なった開発手法になってきます。

また、クラウドアプリケーションではアプリケーション開発の力点が、「アプリケーションを開発」することから、「ビジネスを駆動」することに移っていることも見逃すことができません。 

単にアプリケーションを構築するだけでは不十分で、ビジネスのライフサイクルの中でアプリケーションの開発、運用、評価から次のビジネス設計に繋がるサイクルをまわしていく仕掛けを構築し、ビジネスに組込んでいくことが求められます。

以上のような点を念頭に置いたアプリケーションの開発手法が必要になってきます。これが本稿のテーマです。

開発プロセスの全体像

開発プロセスは4つのアクションで構成されます。

  • Business Modeling :: ビジネス設計を行います。
  • Development :: ACPアプリケーションの開発を行います。
  • Operation :: アプリケーションを配備して運用します。
  • Evaluation :: アプリケーションの動作結果を分析しビジネスや開発にフィードバックします。

これらの4つのアクションで一種のPDCAサイクルを構成します。



Business Modelingは、ビジネスレイヤーでの目標、価値などの分析を行いビジネスプロセスなどを用いてビジネス設計を行います。このビジネス設計の中で必要とされるアプリケーション・プログラムの責務が明らかになるので、これをDevelopmentアクションで開発を行います。

Developmentアクションでは、Business Modelingの成果物をベースにアプリケーションの開発を行います。Developmentアクションは必要に応じて繰り返し開発を行います。

クラウドアプリケーションにおいて重要な点として、アプリケーションの運用によって得られた各種データを分析してビジネスに対するフィードバックするというビジネス改善サイクルをどのようにして構築してビジネスに組込んでいくのかという点があります。

伝統的なアプリケーション開発ではこの活動はあまりフォーカスされていなかったので開発プロセスとしては中心的な話題にはなってこなかったと思います。(そもそもプログラムを作るのが大変だったので、そちらへの対応で力を使いきってしまっていたといえるかもしれません。)

一方、クラウドアプリケーションの場合ビジネス改善サイクルの構築とビジネスへの組み込みをいかに短く効率よく行うのかという点が主要な論点の一つとなっていると思います。1つにはACPによって複雑なバックエンドシステムを持つプログラムを開発することが簡単になったこと。また、ビッグデータ的なデータ活用技術の実用化によってビジネスにフィードバックするデータの分析が安価かつ容易にできるようになりました。

Operationで分析に必要なデータを蓄積すること、Evaluationで蓄積したデータを効率よく解析・分析してビジネスやアプリケーションの改善に必要なフィードバックを作成する作業をいかに行うのかという点が重要になります。

これらの2つのアクションではACPがプラットフォームとして提供するデータ分析基盤が非常に重要になります。

ACPのデータ分析基盤では、Operationアクションで各種データを標準形式で採取し、分析に適した形の分析データに再構成して管理します。Evaluationアクションではこれらの分析データを元に、各種分析を行いビジネスやアプリケーションへのフィードバックを作成します。

また、Business Modelingでは単にアプリケーションの要求仕様を抽出するだけでなく、全体ビジネスの中でのアプリケーションの位置付け、ビジネスの目標、価値といったものを踏まえてビジネス改善サイクルを組み込んだビジネス設計も同時に行うことになるでしょう。

開発の流れ

Developmentアクションでの開発の流れは以下になります。

  • Requirement
  • Design
  • Implementation
  • Test
  • Integration Test

開発の各アクティビティは標準的なものです。

ただし、ACPを使う場合ターゲットのプラットフォームは決まっているためPSM(Platform Indepent Model)を作成するAnalysisは効果が薄いので省略しています。必要に応じてDesignの中で行うとよいでしょう

これらのアクティビティは流れ作業的に順番にこなしていくのではなく、適切なプロジェクト運営のもと効率のよい順序で作業していくことになります。

目安としては、図で示した通りDevelopmentアクション全体のイテレーション、Design, Implementation, Testのイテレーションを想定しています。

サブシステム毎の開発手順カスタマイズ

図はDevelopmentアクションのRealization部を詳細化したものです。



ここでは複数のサブシステムを個別に開発し、最終的に結合する開発を想定しています。

図では特に重要なものとしてPresentation, Service Extension, Configurationを示しています。

Presentation

PresentationはWebやiOS/AndroidといったMobileアプリの開発です。このRealizationは以下の3つのactivityで構成されることを想定しています。

  • UX Design :: 画面設計を行います。
  • Implementation :: アプリケーションを実装します。
  • Test :: アプリケーションのテストを行います。

UX Designは画面設計や画面とAPCの提供するAPIなどの接続関係を設計します。

UX Designで行う画面設計はRequirementとの境界線が曖昧になりますが、開発中に顧客の意見を取り入れながら画面の調整を行う作業はUX Designの一環とします。(逆にRequirementでは、ペルソナやユースケースで利用者の体験する物語とドメインモデルの定義といった抽象度の高い成果物が中心となります。)

プロジェクト運営はアジャイル的なものを想定しています。

UX Design, Implementation, Testはこの順番に行うのではなく、作業の都合に合わせて任意の順番で行っていきます。

Service Extension

Service Extensionは、ACPが提供する拡張メカニズムに則ったプラグイン的な機能です。

ACP内に配備することで、ACPに対してアプリケーションが必要とする機能拡張を行います。

プロジェクト運営は計画駆動型を想定しています。

Service Extensionでは、基幹システムとの接続など、仕様がrequirement activityで明確に定義されるケースが多いと考えられるので計画駆動型が適していると考えています。もちろん、小規模開発など、場合によってはアジャイル型のプロジェクト運営が適しているケースも多いでしょう。

Configuration

アプリケーション向けのACPの振る舞いをカスタマイズパラメータとして設定します。

ここでは意図を明確にするために、独立した開発として示していますが、小規模開発の場合はPresentationなどの作業内で行われることになると思います。

Web/Mobileアプリ開発プロセス

「概要」の図に示す開発プロセスは汎用的な大きめの開発手順になっています。

さらに「サブシステム毎の開発手順カスタマイズ」の図ではPresentation, Service Extension, Configurationといった開発ターゲットごとの開発手順としてカスタマイズしています。



クラウドアプリケーションで多い形態は、フロントエンドのWeb/MobileアプリケーションのみUX重視で開発し、バックエンドサービスはACPのものをそのまま利用するものです。

このような開発では、多少ACPに対するconfigurationが発生しますが、基本的には従来技術であるWeb/Mobileアプリケーションの開発手法をそのまま用いてクラウドアプリケーションを開発できます。

Web/Mobileアプリ・プロファイルではこのような開発を想定したカスタマイズを行っています。

OperationやEvaluationはACPが自動的に提供してくれるため、開発エンジニアが特別に何かを開発する必要はありません。ただ、ACPが提供するAPIを使用する手順などで、何らかの意識は必要なのでWeb/Mobile開発者向けのユーザーガイドなどで、この点は明確化しておくことになるでしょう。

まとめ

今回はACPをベースにした、クラウドアプリケーションの開発プロセスの全体像についてざっくり考えてみました。

面白いテーマなので、引き続き考えていることをブログ化していきたいと思います。

2015年3月30日月曜日

Scala的状態機械/FP編

Scalaにおける状態機械の実装戦略について検討しています。

Scala的状態機械/OOP編」で状態+状態遷移を表現するトレイトであるParseStateを作成しました。ParseStateの具象クラスはcase classまたはcase objectとして実現しています。これらのトレイト、case class、case objectがワンセットでFPで使用できる代数的データ構造となっています。

「OOP編」ではこのParseStateを使用して、オブジェクト版の状態機械とアクター版の状態機械を作成しました。代数的データ構造でもあるParseStateがOOP的に問題なく使用できることが確認できました。

「FP編」では、ParseStateの代数的データ構造の性質を活かしてMonadic Programming(以下MP)版の状態機械を考えてみます。

Stateモナド版状態機械

MPで状態を扱いたい場合には、状態を扱うモナドであるStateモナドが有力な選択肢です。

代数的データ型であるParseStateはそのまま利用し、これをStateモナドでくるむことで状態遷移を実現したものが以下のParserStateMonadです。

package sample

import scalaz._, Scalaz._

object ParserStateMonad {
  def action(event: Char) = State((s: ParseState) => {
    (s.event(event), event)
  })

  def parse(events: Seq[Char]): Seq[String] = {
    val s = events.toVector.traverseS(action)
    val r = s.run(InitState)
    r._1.endEvent.row
  }
}
action関数

モナドを使った共通機能を作る場合には、共通機能としていわゆるモナディック関数を提供するのが一つの形になっています。

モナディック関数とは「A→M[B]」(Mはモナド)の形をしている関数です。モナドMのflatMapコンビネータの引数になります。

Stateモナドを使用する場合には、A→State[B]の形の関数を用意することになります。

def action(event: Char) = State((s: ParseState) => {
    (s.event(event), event)
  })

今回作成したStateモナド用のモナディック関数であるaction関数は「Char→State[ParseState→(ParseState, Char)]」の形をしています。

A→M[B]の形とは以下の対応になります。

  • A : Char
  • M : State
  • B : ParseState→(ParseState, Char)

Stateモナドに設定している関数は「ParseState→(ParseState, Char)」の形をしていますが、action関数全体ではaction関数の引数もパラメタとして利用しているので、結果として「Char→ParseState→(Parse, Char)」の動きをする関数になっています。

action関数が返すStateモナドはParseStateによって表現された状態を、受信したイベントとの組合せ状態遷移する関数が設定されています。

状態+状態遷移を表現するオブジェクト兼代数的データ型であるParseStateがきちんと定義されていれば、Stateモナド用のモナディック関数を定義するのは容易なことが分かります。

parse関数

CharのシーケンスからCSVをパースするparse関数は以下になります。

def parse(events: Seq[Char]): Seq[String] = {
    val s = events.toVector.traverseS(action)
    val r = s.run(InitState)
    r._1.endEvent.row
  }

parse関数は、Stateモナド用モナディック関数actionと型クラスTraverseのtraverseSコンビネータの組合せで実現しています。

traverseSコンビネータはStateモナド用のtraverseコンビネータです。Scalaの型推論が若干弱いためStateモナド専用のコンビネータを用意していますが、動きは通常のtraverseコンビネータと同じです。

状態遷移のロジックそのものはParseStateオブジェクトにカプセル化したものをaction関数から返されるStateモナド経由で使用します。

traverseSコンビネータとStateモナドを組み合わせると、Traverseで走査対象となるコレクションをイベント列と見立てて、各イベントの発生に対応した状態機械をStateモナドで実現することができます。この最終状態を取得することで、イベント列を消化した最終結果を得ることができます。

OOPオブジェクトであり代数的データ構造でもあるParseStateは、このようにしてStateモナドに包むことで、FP的な状態機械としてもそのまま使用することができるわけです。

使い方

プログラムを実行するためのSpecは以下になります。

package sample

import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks

@RunWith(classOf[JUnitRunner])
class ParserStateMonadSpec extends WordSpec with Matchers with GivenWhenThen with GeneratorDrivenPropertyChecks {
  "ParserStateMonad" should {
    "parse" in {
      val events = "abc,def,xyz".toVector
      val r = ParserStateMonad.parse(events)
      println(s"ParserStateMonadSpec: $r")
      r should be (Vector("abc", "def", "xyz"))
    }
  }
}
実行

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

$ sbt test-only sample.ParserStateMonadSpec
ParserStateMonadSpec: List(abc, def, xyz)
[info] ParserStateMonadSpec:
[info] ParserStateMonad
[info] - should parse
[info] ScalaTest
[info] 36mRun completed in 400 milliseconds.0m
[info] 36mTotal number of tests run: 10m
[info] 36mSuites: completed 1, aborted 00m
[info] 36mTests: succeeded 1, failed 0, canceled 0, ignored 0, pending 00m
[info] 32mAll tests passed.0m
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 1 s, completed 2015/03/21 17:49:39

scalaz stream版状態機械

Stateモナド版状態機械ではStateモナドと型クラスTraverseのtraverseSコンビネータを使用して、状態機械をMPで実現しました。

この実現方法は、メモリ上に展開したデータに対して一括処理をするのには適していますが、大規模データ処理やストリーム処理への適用は不向きです。

そこで、大規模データ処理やストリーム処理をFunctional Reactive Programming(以下FRP)の枠組みで行うために、scalaz streamを使用して状態機械の実装してみました。

package sample

import scalaz._, Scalaz._
import scalaz.stream._
import scalaz.stream.Process.Process0Syntax

object ParserProcessMonad {
  def fsm(state: ParseState): Process1[Char, ParseState] = {
    Process.receive1 { c: Char =>
      val s = state.event(c)
      Process.emit(s) fby fsm(s)
    }
  }

  def parse(events: Seq[Char]): Seq[String] = {
    val source: Process0[Char] = Process.emitAll(events)
    val pipeline: Process0[ParseState] = source.pipe(fsm(InitState))
    val result = pipeline.toList.last
    result.endEvent.row
  }

  import scalaz.concurrent.Task

  def parseTask(events: Seq[Char]): Task[Seq[String]] = {
    val source: Process0[Char] = Process.emitAll(events)
    val pipeline: Process[Task, ParseState] = source.pipe(fsm(InitState)).toSource
    for {
      lastoption <- pipeline.runLast
      last = lastoption.get
    } yield last.endEvent.row
  }
}
fsm関数

Processモナドは一種の状態機械なので、この性質を利用してPraseStateによる状態遷移をProcessモナド化します。この処理を行うのがfsm関数です。

fsm関数は状態であるParseStateを引数に、Charを受け取るとParseStateとCharの組合せで計算された新しいParseStateによる状態に遷移しつつ処理結果としてParseStateを返すProcessモナドを返します。

def fsm(state: ParseState): Process1[Char, ParseState] = {
    Process.receive1 { c: Char =>
      val s = state.event(c)
      Process.emit(s) fby fsm(s)
    }
  }
parse関数

parse関数はscalaz streamをメモリ上の小規模データに適用する際の典型的な使い方です。

def parse(events: Seq[Char]): Seq[String] = {
    val source: Process0[Char] = Process.emitAll(events)
    val pipeline: Process0[ParseState] = source.pipe(fsm(InitState))
    val result = pipeline.toList.last
    result.endEvent.row
  }

parse関数のおおまかな流れは以下になります。

  1. パイプライン(Processモナド)を生成
  2. パイプラインの処理を記述
  3. パイプラインで加工後のデータを取得
パイプラインの生成

まず引数のCharシーケンスからソースとなるProcessモナドを生成します。

val source: Process0[Char] = Process.emitAll(events)

ProcessのemitAll関数は引数に指定されたシーケンスによるストリームを表現するProcessモナドを生成します。

ただし、内部的に非同期実行する本格的なシーケンスではなく、通常のシーケンスに対してストリームのインタフェースでアクセスできるという意味合いのProcessモナドになります。(内部的には実行制御に後述のTaskモナドではなく、Idモナドを使用しています。)

パイプラインの処理を記述

pipeコンビネータでfsm関数にInitStateを適用して得られるProcessモナドをパイプライン本体のProcessモナドに設定しています。

val pipeline: Process0[ParseState] = source.pipe(fsm(InitState))

これが処理の本体です。ここではpipeコンビネータを始めとする各種コンビネータを使ってパイプラインを構築します。

パイプラインで加工後のデータを取得

最後にtoListメソッドで、パイプラインの処理結果をListとして取り出し、ここからパース結果を取り出す処理を行っています。

val result = pipeline.toList.last
    result.endEvent.row
parseTask関数

parse関数はscalaz streamをメモリ上の小規模データに適用する際の使用例ですが、より本格的な応用である大規模データ処理やストリーム処理では少し使い方が変わってきます。

そこで、参考のために実行制御にTaskモナドを使ったバージョンのparseTask関数を作りました。

def parseTask(events: Seq[Char]): Task[Seq[String]] = {
    val source: Process[Task, Char] = Process.emitAll(events).toSource
    val pipeline: Process[Task, ParseState] = source.pipe(fsm(InitState))
    for {
      lastoption <- pipeline.runLast
      last = lastoption.get
    } yield last.endEvent.row
  }

parse関数と同様にparseTask関数のおおまかな流れは以下になります。

  1. パイプライン(Processモナド)を生成
  2. パイプラインの処理を記述
  3. パイプラインで加工後のデータを取得
パイプラインの生成

ProcessモナドのtoSourceメソッドで、Taskモナドを実行制御に使用するProcessモナドに変換されます。

val source: Process[Task, Char] = Process.emitAll(events).toSource
パイプラインの処理を記述

fsm関数から得られたProcessモナドをpipeコンビネータでパイプラインに設定します。

val pipeline: Process[Task, ParseState] = source.pipe(fsm(InitState))

この処理はTask版でないparse関数と全く同じです。

パイプラインで加工後のデータを取得

Taskモナドを実行制御に使用する場合には、for式などを使ってモナディックに実行結果を取得する形になります。

for {
      lastoption <- pipeline.runLast
      last = lastoption.get
    } yield last.endEvent.row
使い方

プログラムを実行するためのSpecは以下になります。

package sample

import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks

@RunWith(classOf[JUnitRunner])
class ParserProcessMonadSpec extends WordSpec with Matchers with GivenWhenThen with GeneratorDrivenPropertyChecks {
  "ParserProcessMonad" should {
    "parse" in {
      val events = "abc,def,xyz".toVector
      val r = ParserProcessMonad.parse(events)
      println(s"ParserProcessMonadSpec: $r")
      r should be (Vector("abc", "def", "xyz"))
    }
  }
}
実行

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

$ sbt test-only sample.ParserProcessMonadSpec
ParserProcessMonadSpec: List(abc, def, xyz)
[info] ParserProcessMonadSpec:
[info] ParserProcessMonad
[info] - should parse
[info] ScalaTest
[info] 36mRun completed in 301 milliseconds.0m
[info] 36mTotal number of tests run: 10m
[info] 36mSuites: completed 1, aborted 00m
[info] 36mTests: succeeded 1, failed 0, canceled 0, ignored 0, pending 00m
[info] 32mAll tests passed.0m
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 0 s, completed 2015/03/21 17:49:12

scalaz stream + Stateモナド版状態機械

「scalaz stream」ではParseStateオブジェクトを直接使用して状態機械を作成しました。通常はこれで十分ですが、Stateモナド用モナディック関数が用意されている場合は、こちらを使用する方法もあります。

この方法では、Stateモナドの使い方だけ理解していればよいので、プログラミングはより簡単かつ汎用的になります。

package sample

import scalaz._, Scalaz._
import scalaz.stream._

object ParserProcessMonadStateMonad {
  def fsm(state: ParseState): Process1[Char, ParseState] = {
    Process.receive1 { c: Char =>
      val s = ParserStateMonad.action(c).exec(state)
      Process.emit(s) fby fsm(s)
    }
  }

  def parse(events: Seq[Char]): Seq[String] = {
    val source: Process0[Char] = Process.emitAll(events)
    val pipeline: Process0[ParseState] = source.pipe(fsm(InitState))
    val result = pipeline.toList.last
    result.endEvent.row
  }

  import scalaz.concurrent.Task

  def parseTask(events: Seq[Char]): Task[Seq[String]] = {
    val source: Process0[Char] = Process.emitAll(events)
    val pipeline: Process[Task, ParseState] = source.pipe(fsm(InitState)).toSource
    for {
      lastoption <- pipeline.runLast
      last = lastoption.get
    } yield last.endEvent.row
  }
}
fsm関数

ParseStateによる状態機械の動作を行うProcessモナドを生成するfsm関数のStateモナド版は以下になります。

def fsm(state: ParseState): Process1[Char, ParseState] = {
    Process.receive1 { c: Char =>
      val s = ParserStateMonad.action(c).exec(state)
      Process.emit(s) fby fsm(s)
    }
  }

ParseStateのeventメソッドを直接使用する代わりに、ParseStateを包んだStateモナドを返すモナディック関数actionを使用します。

この版のfsm関数ではaction関数から取得したStateモナドのexecメソッドを使用して、現状態とイベント(Char)から新状態を計算し、この状態を保持した、新たなProcessを生成しています。

この方法のメリットはParseStateの使い方(この場合はeventメソッド)は知る必要がなく、汎用的なStateモナドの使い方だけ知っていればよい点です。

つまりaction関数だけ作っておけば、Traverseを使った状態機械、Processモナドを使った状態機械のどちらも簡単に作ることができるわけです。

使い方

プログラムを実行するためのSpecは以下になります。

package sample

import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks

@RunWith(classOf[JUnitRunner])
class ParserProcessMonadStateMonadSpec extends WordSpec with Matchers with GivenWhenThen with GeneratorDrivenPropertyChecks {
  "ParserProcessMonad" should {
    "parse" in {
      val events = "abc,def,xyz".toVector
      val r = ParserProcessMonadStateMonad.parse(events)
      println(s"ParserProcessMonadStateMonadSpec: $r")
      r should be (Vector("abc", "def", "xyz"))
    }
  }
}
実行

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

$ sbt test-only sample.ParserProcessMonadStateMonadSpec
ParserProcessMonadStateMonadSpec: List(abc, def, xyz)
[info] ParserProcessMonadStateMonadSpec:
[info] ParserProcessMonad
[info] - should parse
[info] ScalaTest
[info] 36mRun completed in 286 milliseconds.0m
[info] 36mTotal number of tests run: 10m
[info] 36mSuites: completed 1, aborted 00m
[info] 36mTests: succeeded 1, failed 0, canceled 0, ignored 0, pending 00m
[info] 32mAll tests passed.0m
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 0 s, completed 2015/03/21 17:49:26

状態機械実装戦略

オブジェクト・モデルで状態機械が出てきた場合の実装戦略としては、まずベースとして:

  • sealed trait + case classで状態+状態遷移のオブジェクト&代数的データ型(以下、状態遷移case class)

を作成します。

前回に見たようにこの状態遷移case classを使って、OOP版の状態機械を簡単に作成することができます。

次に、FP用に以下の部品を整備しておくのがよいでしょう。

  • Stateモナド用モナディック関数
  • Processモナド用状態機械関数

どちらの関数も状態遷移case classが用意されていれば、ほとんど定型的な記述のみで作成することができます。この部品を使うことでFP版の状態機械を簡単に作成することができます。

以上、状態機械実装戦略について整理しました。

この戦略で重要なのは、状態+状態遷移を表現するために作成した状態遷移case classは1つだけ作ればよく、これをOOP流、FP流にマルチに展開していける点です。

この点が確定すれば、オブジェクト・モデルに状態機械が出てきたら安心して1つの状態遷移case classの実装に集中することができます。

まとめ

OFPの状態機械の実装についてOOP的実装、FP的実装について整理してみました。

いずれの場合もオブジェクト&代数的データ型である「状態遷移case class」が基本で、ここからOOP的状態機械、FP的状態機械へマルチに展開できることが確認できました。

OFPではアプリケーション・ロジックは可能な限りFP側に寄せておくのがよいので、FPでも状態機械を簡単に実現できることが確認できたのは収穫でした。

また、FP的なアプローチが使えないケースが出てきても、簡単にOOP的アプローチに切り替え可能なのも安心材料です。

諸元

  • Scala 2.11.4
  • Scalaz 7.1.0
  • Scalaz-stream 0.6a
  • Scalatest 2.2.4

2015年3月23日月曜日

Scala的状態機械/OOP編

オブジェクト・モデリングにおける動的モデルは状態機械で記述するのが基本です。つまり状態機械はオブジェクト・モデリングの重要な構成要素の一つということです。

ScalaでObject-Functional Programming(OFP)を行う場合でも、要求・分析・設計の各アクティビティを経て作成されたオブジェクト・モデル内の状態機械をどのように実装していくのかという実装方式が論点になります。

普通のOOP流の実装はすでに議論しつくされていると思いますが、OFPにおけるFPでの実装方式については、これから整備されていくことになると思います。

注意が必要なのはクラウド・アプリケーション開発をターゲットにする場合、伝統的なFPというよりMonadic Programming(以下MP)を経てFunctional Reactive Programming(以下FRP)がゴールになるということです。

このためFRPとして利用可能な状態機械実装を探っておきたいところです。

課題

状態機械の使い所としては、エンティティの状態遷移を記述することで業務ワークフローの構築に用いたり、プロトコルドライバのフロー制御といったものが考えられます。前者はDBに状態を格納することになりエンタープライズ・アプリケーション的なライフサイクルの長い応用ですし、後者は制御系の応用でメモリ内の制御で完結する形のものです。

色々な応用があるわけですが、どの方面でも使える実現方法を念頭におきつつ、ここではCSVのパーサー処理を状態機械で実装してみることにします。FPとの接続部分に興味を集中させるためできるだけ簡単なものを選んでみました。

具体的には以下の課題になります。

  • CSVの1行を「,」区切りのレコードとしてパースし、文字列の列を取得する処理に使用する状態機械

この状態機械は文字列の列の各文字をイベントとして扱い、一連のイベント終了後にパース処理の結果となる「文字列の列」を状態の情報として保持する形になります。

case classで状態機械

状態機械は「状態×イベント→状態」のメカニズムが基本の構成要素です。このメカニズムをcase classで実装します。

いうまでもなくcase classはScalaプログラミングの超重要な構成要素で、OOP的側面とFP的側面を兼ね備えたObject-Functional Programmingの肝となる機構です。

OOP的にはDDDでいうところのvalue objectの実装に適した文法になっています。

FP的には代数的データ型(algebraic data type)として利用することが想定されています。

つまりvalue object兼代数的データ型を実現するための機構がcase classです。

ただしcase classの文法上はvalue objectや代数的データ型に適さない普通のオブジェクトを実装することも可能です。このため、value object兼代数的データ型を実現するための紳士協定を組み込んでおかなければなりません。

この紳士協定は以下の2つです。

  • 不変オブジェクト(immutable object)にする
  • 基底のトレイトや抽象クラスはsealedにする

Value objectと代数的データ型は共に不変オブジェクトである必要があります。

また代数的データ型の要件としてコンパイル時に全インヘリタンス関係が確定している必要があるので、sealedにすることでこれを担保します。

case classはオブジェクト指向の普通のオブジェクトでもあるので、不変オブジェクトの性質さえ守ればオブジェクトのフルスペックを使用することができます。

package sample

sealed trait ParseState {
  def event(c: Char): ParseState
  def endEvent(): EndState
}

case object InitState extends ParseState {
  def event(c: Char) = c match {
    case ',' => InputState(Vector(""), "")
    case '\n' => EndState(Nil)
    case _ => InputState(Nil, c.toString)
  }
  def endEvent() = EndState(Nil)
}

case class InputState(
  fields: Seq[String],
  candidate: String
) extends ParseState {
  def event(c: Char) = c match {
    case ',' => InputState(fields :+ candidate, "")
    case '\n' => EndState(fields :+ candidate)
    case _ => InputState(fields, candidate :+ c)
  }
  def endEvent() = EndState(fields :+ candidate)
}

case class EndState(
  row: Seq[String]
) extends ParseState {
  def event(c: Char) = this
  def endEvent() = this
}

case class FailureState(
  row: Seq[String],
  message: String
) extends ParseState {
  def event(c: Char) = this
  def endEvent() = sys.error("failure")
}

状態機械は、「状態×イベント→状態」の情報を記述したマトリックスとして表現できます。このマトリクスを実現するデータ構造と評価戦略は色々考えられますが、ここではOOP的なアプローチで実現しました。

状態

まず状態機械全体を表すトレイトとしてParseStateを定義します。sealedにすることで代数的データ構造の要件の1つを満たします。

ParseStateのサブクラスとして具体的な状態を定義します。情報を持つInitState, InputState, EndState, FailureStateはcase classとして、情報を持たないInitStateはcase objectとして定義しました。

イベント

発生するイベントはメソッドで表現しました。

event
1文字入力イベント発生
endEvent
パース終了イベント発生
状態×イベント→状態

状態とイベントの組から新しい状態を決定するアルゴリズムは「発生するイベント」で定義したメソッドの実装になります。

具体的には各case class, case objectのeventメソッド、endEventメソッドの実装を参照して下さい。

オブジェクト版状態機械

case classによる状態機械の表現はimmutableなので、そのままでは状態機械としては動きません。あくまでも「状態×イベント→状態」のメカニズムを提供するまでになります。

状態機械の状態遷移は状態を遷移させるので本質的にはmutableな処理です。

OOP的には、オブジェクトのインスタンス変数でmutableな状態を保持する実現方法になります。

OOP版の状態機械をParserObjectとして実装しました。

package sample

class ParserObject {
  var state: ParseState = InitState

  def charEvent(c: Char) {
    state = state.event(c)
  }

  def parseEnd(): Seq[String] = {
    val end = state.endEvent()
    state = end
    end.row
  }
}

PaserObjectクラスでは、変更可能なインスタンス変数としてstateを定義しています。

イベントの発生をメソッドで受取り、状態とイベントの組合せで新たな状態を計算し、これをstateに設定することで状態遷移を実現しています。

新しい状態の計算ロジックは、ParseStateオブジェクトにカプセル化しています。

代数的データ構造で表現した状態機械のimmutableなオブジェクトを、インスタンス変数で管理することで、OOP的な状態機械が実現できました。

使い方

プログラムを実行するためのSpecは以下になります。

package sample

import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks

@RunWith(classOf[JUnitRunner])
class ParserObjectSpec extends WordSpec with Matchers with GivenWhenThen with GeneratorDrivenPropertyChecks {
  "ParserObject" should {
    "parse" in {
      val parser = new ParserObject()
      val text = "abc,def,xyz"
      for (c <- text) {
        parser.charEvent(c) // 一文字づつパーサーに送信
      }
      val r = parser.parseEnd() // 解析終了のイベントを送信
      println(s"ParserObject: $r")
      r should be (Vector("abc", "def", "xyz"))
    }
  }
}
実行

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

$ sbt test-only sample.ParserObjectSpec
ParserObject: List(abc, def, xyz)
[info] ParserObjectSpec:
[info] ParserObject
[info] - should parse
[info] ScalaTest
[info] 36mRun completed in 180 milliseconds.0m
[info] 36mTotal number of tests run: 10m
[info] 36mSuites: completed 1, aborted 00m
[info] 36mTests: succeeded 1, failed 0, canceled 0, ignored 0, pending 00m
[info] 32mAll tests passed.0m
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 0 s, completed 2015/03/21 17:47:46

アクター版状態機械

FRP的な応用を考える場合、並行・並列処理で枠組みの中で状態機械を実現していく必要があります。

この実現方式として有力なのがアクターです。

ScalaではAkkaというアクター基盤を使用することができます。

このアクターは(理論的には色々あるでしょうが)状態を持ったactive objectを実現する方式になっています。

active objectであるアクター間はメッセージボックスでキューイングされたメッセージ通信で協調動作するので、アプリケーションレベルで特別な排他制御を行わなくても並行・並列処理を簡潔に記述することができます。

アクター版のCSVパーサーであるParserActorは以下になります。

package sample

import akka.actor._

case class ParseCharEvent(c: Char)
case object ParseEnd
case class ParseResult(result: Seq[String])

class ParserActor extends Actor {
  var state: ParseState = InitState

  def receive = {
    case ParseCharEvent(c) =>
      state = state.event(c)
    case ParseEnd =>
      val end = state.endEvent()
      state = end
      sender ! ParseResult(end.row)
  }
}

基本的に行っているのは(passive objectである)ParserObjectと同じです。オブジェクトのインスタンス変数でmutableな状態を保持する実現方法になります。

ParserObjectとの違いは、メソッド呼び出しではなくアクター間でのメッセージ通信によって処理が実行されるため、以下のようなメッセージ通信のためのコードを用意しないといけない点です。

  • アクター間で送受信されるメッセージの定義
  • メッセージを受け取り処理を行うイベントハンドラ

ParserObjectではメソッドとして簡単に定義できる処理ですが、これをメッセージ処理ように仕立て直さないといけないわけです。

アクターはとても簡単に利用できるのですが、少しボイラープレイトのコードを書く必要があります。

使い方
package sample

import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import akka.actor._
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._

@RunWith(classOf[JUnitRunner])
class ParserActorSpec extends WordSpec with Matchers with GivenWhenThen with GeneratorDrivenPropertyChecks {
  "ParseActor" should {
    "parse" in {
      implicit val timeout = Timeout(5.seconds)
      val system = ActorSystem("state")
      implicit val context = system.dispatcher
      val actor = system.actorOf(Props[ParserActor])

      val text = "abc,def,xyz"
      for (c <- text) {
        actor ! ParseCharEvent(c)
      }
      for (r <- actor ? ParseEnd) yield {
        r match {
          case ParseResult(r) =>
            println(s"ParserActorSpec: $r")
            r should be (Vector("abc", "def", "xyz"))
        }
      }
      system.shutdown()
    }
  }
}

アプリケーションロジックは以下の部分です。

val text = "abc,def,xyz"
      for (c <- text) {
        actor ! ParseCharEvent(c)
      }
      for (r <- actor ? ParseEnd) yield {
        r match {
          case ParseResult(r) =>
            println(s"ParserActorSpec: $r")
            r should be (Vector("abc", "def", "xyz"))
        }
      }

アクターを使うために以下のような準備が必要になります。

// タイムアウト値の暗黙知を定義
      implicit val timeout = Timeout(5.seconds)
      // アクターの実行基盤を作成
      val system = ActorSystem("state")
      // スレッドの実行文脈を定義
      implicit val context = system.dispatcher
      // アクターの生成
      val actor = system.actorOf(Props[ParserActor])
      ....
      // アクターの実行基盤をシャットダウン
      system.shutdown()

アクターはJava流のスレッドによる並行・並列プログラミングと比較するとはるかに楽でバグも出にくいアプローチなのですが、それでも少し込み入ったボイラープレイトが必要になります。

実行

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

$ sbt test-only sample.ParserActorSpec
[info] Compiling 1 Scala source to /Users/asami/src/workspace2015/0317.blog.statemachine/target/scala-2.11/test-classes...
ParserActorSpec: List(abc, def, xyz)
[info] ParserActorSpec:
[info] ParseActor
[info] - should parse
[info] ScalaTest
[info] 36mRun completed in 408 milliseconds.0m
[info] 36mTotal number of tests run: 10m
[info] 36mSuites: completed 1, aborted 00m
[info] 36mTests: succeeded 1, failed 0, canceled 0, ignored 0, pending 00m
[info] 32mAll tests passed.0m
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 2 s, completed 2015/03/21 17:48:09

まとめ

今回は状態機械のベースとなるcase class群(ParseState)を作成した後、通常のOOPのアプローチとして普通のオブジェクト版(passive object)のパーサーとアクター版(active object)のパーサーを作成しました。

ParseStateはOOPのvalue objectであると同時にFPの代数的データ型でもあります。このParseStateを普通のオブジェクト、アクターの両方で普通に利用して状態機械を作成できることが確認できました。

次回は今回のParseStateをベースにMonadic Programming, Functional Reactive Programmingにおける状態機械の実現方法について考えます。

諸元

  • Scala 2.11.4
  • Scalaz 7.1.0
  • Scalaz-stream 0.6a
  • Scalatest 2.2.4