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

2016年10月28日金曜日

QCon Tokyo 2016

QCon Tokyo 2016で「オブジェクト‐関数型プログラミングからオブジェクト‐関数型分析設計へ~クラウド時代のモデリングを考える」と題してお話させて頂きました。


上記の個人用のSlideShareは文字化けが取りきれないので、きちんと読みたい方は会社のSlideShareの方を見ていただくとよいと思います。上記スライドもPDFをダウンロードしたものは文字化けしていません。

Reactive Streams

今回のテーマであるOFADについては別の記事で考えたいと思いますが、今回スライドを作っていて改めて以下のことを感じました。

  • Reactive Streamsは次のブレークスルーの起点になるかも

FP(Functional Programming)でI/Oを扱う技術としてIOモナドがありますが、その発展形として以下の2つの技術があります。

  • Operationalモナド(scalazではFreeモナド+α)
  • Processモナド(scalaz-streamの場合)

製品開発の中で、どちらの技術も使ってみましたがProcessモナドの方が圧倒的に楽なんですね。

Processモナドでは入出力などの作用に対する処理部は始端(source)と終端(sink)をパターンにしたがって実装すれば簡単に実現できます。source, sinkの抽象度が適切なので一度作った部品は色々な用途で再利用できます。また、(フロー制御用の)状態を管理する処理をProcessモナド内に組み込むためのメカニズムも持っていますが、これの実装もそれほど難しくありません。

一方、Operationalモナドは作用に対する処理はインタープリタとして実装して、自然変換のメカニズムでOperationalモナドの実行時に割り当てる必要があります。このメカニズムによりDI(Dependency Injection)の機能も実現できるので、その点では素晴らしいのですが、インタープリタという大きな仕掛けを作らないといけないので、単に入出力をしたいという目的には重たすぎると感じました。Scalaの場合はOOP側で自由に入出力できるので、このメカニズムを使ってまでFP化をすすめるニーズはなかなかないかも、という感触です。

このような感触が得られている中で、スライドページ「OOPとFPの協業」をまとめながら考えたのは「簡単に使えるReactive Streamsを使えば、多くの処理をFP化できる」ということです。

セッション内で説明しましたが、FPの方がOOPに対して、高品質(バグが出にくい)というメリットがあり、さらに持続的開発の中核作業であるリファクタリングで圧倒的な優位性を発揮する、というのがボクの主張点です。

このような観点からFPの範囲を大きく広げるReactive StreamsはOFP(Object-Functional Programming)にとって本質的に重要な技術なのではないかと感じました。

Reactive Streamsは大規模分散処理やストリーミング処理向けの専用機能という観点で取り上げられることが多いと思いますが、それだけではなく日常的なOFPにとって中軸となるプログラミング・モデルとなりうる点がより重要であると感じました。ここにさらに大規模、高頻度、ストリーミングがおまけでついてくるという切り口でのアプローチがよいと思います。

フィードバック

セッション後に2つほどフィードバックを頂きました。

OOPとFPの関係

スライドページ「OOPとFPの関係」ではFPでは以下のことが実現できないと説明しました。

  • 状態の更新
  • 動的束縛によるポリモーフィズム
  • 大規模開発(?)

この点について専門家の方から以下のような趣旨のフィードバックを頂きました。(文意はボクの理解によるものなので、正確な意図とはずれている可能性があります。)

  • 「動的束縛によるポリモーフィズム」と「大規模開発」は理論的に解決されており実用言語での実績もある。

「動的束縛によるポリモーフィズム」については、ボクの理解ではここがOOPとFPの違いだと思っていたので、理論的に解決されていて、さらに実用言語での実績もあるというという点は意外でした。

コンパイル時に継承関係が全て確定していれば、動的束縛部分を直和(+pattern matching)に落とし込むことはできるとは思いますが、共通ライブラリで定義したクラスの継承や、分割コンパイルといったニーズがOOP的には重要なので、この問題をどのように解決しているのか興味のあるところです。

大規模開発については、セッションではあまり深く触れていませんが、ボクのイメージでは以下のようなことがあってFP的には大変なのではと推測しています。

  • コンポーネント内に状態を持てないので、大規模システムの部品として利用するには大きな制約があるのではないか。
  • 「動的束縛によるポリモーフィズム」の問題でOOP的な継承が使えないとすると、API/SPIといったインタフェースを使ったコンポーネント部品化に制約がおきるのではないか。
  • OSGiなどを使った動的ローディングによるplugin機構は実現可能か。

機会があれば、このような観点から技術評価をしてみたいと思います。

どちらの問題も、Haslkellではできていないようですし、Scalaのロードマップにもないと思うので、Scalaで利用できるようになるのは当面なさそうとはいえそうです。

Reactive Streamsでできること

セッション後の質問時に以下のような指摘を頂きました。(文意はボクの理解によるものなので、正確な意図とはずれている可能性があります。)

  • 「HaskellでReactive Streamsを使って線形代数を行おうとしたがメモリが足りなくて動かなかった。セッションではReactive Streamsを使って大規模演算ができるとしているがいいかげんな主張ではないか?」

まず「HaskellでReactive Streams」についてはボクの経験外の話でもあり、Haskellライブラリの実装上の問題の可能性もあるのでここでは取り上げません。

線形代数に関しては、全データをメモリに展開して大規模な行列演算を行おうとすると、どのような技術を使ってもメモリ不足を起こすはずなので、この点でご質問の意図がよくわかりませんでした。

線形代数による大規模行列演算の応用にはReactive StreamsではなくSparkのMLLibのようなアプローチがよいのではないかと思います。

Reactive Streamsについては、ごくざっくりいうと「作用を始端と終端に切り離し、フロー制御ができるようになったイテレータ」なので、イテレータでできる範囲で大規模データ処理ができるということです。

具体的には、一定のウィンドウサイズの範囲でデータの一部をメモリに読込み、その範囲で処理を行ってメモリを開放する、という処理を繰り返す動きになります。

このような動きなので、原理的にはどのような大規模なサイズを扱っても大丈夫はなず、ということでセッション中は「1TBでも」とお話したのですが、この点に違和感を感じられたようです。

原理的には1TBでも大丈夫と思いますが、実証実験したわけではないので、この点は明らかにしておきます。

製品開発の中でReactive Streams(scalaz-stream)を大規模メール配信、大規模PUSH配信処理の実装で非常に便利に使っており、ほとんど問題も出ていないことから実用上は全く問題ないのでは、というのがボクの実感としてあり、その点を「1TB」として表現したしだいです。このサイズ表現問題が難しいのは10GB程度だと、現在のハードウェアではメモリに載せてしまうことも可能なので、わかりやすいインパクトのある例として使うのにはちょっと難しいことがあります。次の機会があれば、このあたりの表現を工夫したいと思います。

2016年10月11日火曜日

Object-Functional Analysis and Designふりかえり

クラウド時代のアプリケーション開発について、「クラウド・アプリケーション・モデリング」、「クラウド・アプリケーション開発のモデル体系」と考察してきました。

クラウド・アプリケーション開発では、実装時のプログラミングで「関数」が重要な構成要素となってきています。そうなると、この「関数」を上流のモデリングでどのように扱っていくのかということが重要な論点になります。

このような観点から、上流のモデリングから実装時のOFP(Object-Functional Programming)まで、オブジェクトと関数を融合させ一気通貫にまとめた開発方法論をModegramming StyleではObject-Functional Analysis and Design(OFAD)と呼んでいます。

Modegramming Styleでは2012年ぐらいからOFADについて考察を進めてきました。クラウド・アプリケーションのモデリングの検討を進めていく上で、OFADが一つの軸となると思います。このOFADについて、2012年に要求開発アライアンスでのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』向けにまとめたものがあります。

今回はこの2012年版OFADのふりかえりを行い、検討を進めていくうえでの論点整理を行いたいと思います。

Object-Functional Analysis and Design 2012

クラウド・アプリケーションの開発方法論を整備していくためには「関数」を理解した上で、関数とオブジェクトの関係を整理しないといけないという動機もあり、OFPの有力言語であるScalaを2008年から使い始めました。

ある程度「関数」とOFPについて勘所がつかめてきたところで、2012年に『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』というセッションのタイミングで一度まとめるタイミングがありました。

このセッションの内容は以下にまとまっています。

また関連して以下のような考察を行っています。

ふりかえり

今の目でOFAD 2012をチェックしてみましたが、それほど違和感はなく、以下の基本的な考え方については変更はありませんでした。

  • オブジェクトと関数の使い分け
  • オブジェクトと関数の連携
  • デザインパターン(代数的構造、圏論)
  • Domain-Driven Design (DDDD)

ただ、オブジェクトと関数の連携方法は2012年当時よりも手持ちの選択肢が増えたと思うので、その辺りは反映していきたいところです。

このタイミングで再検討したいのが以下の項目です。Reactive Streamsを始め、2012年以降、要素技術が大きく進化しているのでこれらの技術を取り込んだ上で新しい枠組みで考えてみたいと思います。

  • DSL
  • データフロー

以下のアーキテクチャ的な話題については2012年以降、特に大きな動きはなかったと思います。これらについてはOFADの再検討の中で対応を考えていきたいと思います。

  • EDA
  • DCI
  • CQRS

OFAD 2012以降の技術動向

OFAD 2012以降に起きた技術的な大きなムーブメントとしては以下の2つがあります。後者は我々の提案ですが、最近注目されているServerless Architectureに通じるところがあると思います。

  • Reactive Streams
  • Application Cloud Platform
Reactive Streams

より広い枠組みとしてはFunctional Reactive Programming(FRP)という切り口もありますが、Reactive Streamsの方が現状にあっていると思うのでReactive Streamsの用語を使います。

Modegramming Styleではscalaz-streamを中心にReactive Streamsについても考察を行ってきました。

また幾つかのセッションでお話させていただいたのでスライドとしてもまとめました。

純粋な数学的な計算はよいとして、システムの振る舞いをFunctional Programming(FP)でどのように記述するのかという点がFP実用化の重要な論点だと思いますが、モナドベースのReactive Streamsが一つの解としてブレークスルーの起点となりそうです。

そのような意味でOFADでの関数を考える上でReactive Streamsは重要な論点になります。

分析モデルの段階で、Reactive Streamsに対応するモデル要素を見つけることができればモデルから実装まで一気通貫でつなげるルートを確保することができます。

Application Cloud Platform

OFAD 2012の後、クラウド時代のアプリケーション開発ではクラウド・プラットフォームが重要な構成要素になると考え、その製品化を行う活動をしてきました。その成果として「Prefer Cloud Platform」をリリースすることができました。

Prefer Cloud PlatformのようなクラウドプラットフォームをModegramming StyleではApplication Cloud Platform(ACP)と呼んでいます。Prefer Cloud Platform自体もScalaによるOFPによって実装されていますが、この開発の中でOFPに関するノウハウ、OFADに対するヒントを蓄積することができました。

またACPによってアプリケーション開発の大きな部分を省略することができることが期待できます。こうなると、モデリングの目的はビジネスとアプリケーションの連携方法の分析とシステムの拡張方法の分析設計に絞られます。このような文脈の中での開発方法論ということもクラウド時代の開発方法論であるOFADに求められる点といえます。

まとめ

クラウド・アプリケーション・モデリング」を起点に進めている考察は『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』によって始動したOFAD 2012を最新技術動向やApplication Cloud Platform(ACP)の活用を前提に、2016年版OFADとして再構築を行うという目的のものです。

この検討を進めるためのベースとしてOFAD 2012を簡単にふりかえり、論点整理を行いました。

このフィードバックを活かして、次回はOFADのモデル体系について考えてみたいと思います。

お知らせ

10月24日に開催される「QCon Tokyo 2016」で以下のテーマでお話させていただくことになりました。

  • オブジェクト‐関数型プログラミングからオブジェクト‐関数型分析設計へ~クラウド時代のモデリングを考える

現在検討しているOFADについてのチュートリアル的な内容になる予定です。

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年6月22日月曜日

[FP] パイプライン・プログラミング

関数型プログラミングとは何か?

この問は深遠すぎてとてもボクの手には負えませんが、実務的なプラクティスとしてはパイプライン・プログラミングとして考えると分かりやすいのではないかと思います。

そこでScalaでのパイプライン・プログラミングの選択肢を整理してみました。

関数呼び出し

関数型プログラミングにおける普通の関数呼び出しもパイプラインと考えることができます。

純粋関数型では副作用は発生しないので、表に見えている関数の引数と復帰値のみで関数の挙動のすべてが表現されているためです。

たとえば以下のプログラムは:

h(g(f(1)))

以下のようなパイプラインと考えることができます。

Functor

文脈を持ったパイプラインはFunctorを使って構成できます。関数呼び出しとの違いは「文脈」を持った計算になるという点です。

ここでいう「文脈」とはパイプラインの裏側で暗黙的に共有、引継ぎされる状態やデータというぐらいの意味です。関数の直接呼び出しにはそのような裏表はないので、Functorで加わった重要な機能ということになります。

以下の図はQCon Tokyo 2015のセッション「ScalaによるMonadic Programmingのススメ - Functional Reactive Programmingへのアプローチ」で使用したスライドから引用です。



Functor, Applicative Functor, Monadが構成するパイプラインを概観しています。

この中でFunctorはmapコンビネータを使ってパイプラインを構築します。

Option(1).map(f).map(g).map(h)

Applicative Functor

上記の図で示した通りApplicative Functorは復数のパイプラインを一つに統合する機能を提供します。

ScalazではApplicative Functorのために以下のような文法を提供しています。

(Option(1) |@| Option(2) |@| Option(3))(i(_, _, _))

以下の図は同スライドからFuture Applicative Functorの例です。



Monad

「Functor, Applicative Functor, Monadが構成するパイプラインを概観」する図の中のMonadは以下のプログラムになっています。このようにMonadではflatMapコンビネータを使ってパイプラインを構築します。

Functorとの違いは「文脈」をプログラム側で制御する機能が加わる点です。

Option(1).flatMap(f).flatMap(g).flatMap(h)

以下の図は同スライドからOption Monadの例をあげています。



こちらではflatMapコンビネータを使う代わりにfor式によるfor内包表記(for comprehension)を使用しています。

def bigCalcO(n: Int): Option[String] = {
    for {
      a <- Option(calcString(n))
      b <- Option(calcInt(n))
      c <- Option(calcFloat(n))
    } yield finalCalc(a, b, c)
  }
}

for内包表記はMonadを使ったパイプラインのための文法糖衣として機能します。

上記の例ではflatMapコンビネータ直接使用とfor内包表記の違いはそれほど分かりませんが以下に示すState Monadのような複雑な構造のMonadを使う場合にfor内包表記の効果が出てきます。



Reactive Stream

Applicative FunctorやMonadを使うとかなり複雑なパイプラインを構築することができますが、あくまでも制御上は関数呼び出しなのでフロー制御を行うことができません。

ここでいうフロー制御とは、一度に処理するデータ量を制限したり、データの発生またはデータの消費の契機で処理を進める制御を指します。

直感的にパイプラインというとこういう制御が入っていて欲しいところですが、Monadそのものには入っていないわけです。

そこで登場するのがReactive Streamです。単にStreamというとscala.collection.immutable.Streamと紛らわしいので、ここではReactive Streamと呼ぶことにします。

Scalaz StreamではProcess Monadを使ってReactive Streamをを実現しています。

大規模データ処理とストリーム処理それぞれでのProcessモナドの使い方は以下になります。

ストリーム処理


大規模データ処理とストリーム処理のいずれもパイプラインに抽象化されたほとんど同じプログラムになっています。

また、普通のMonadと比べても若干おまじないは増えるもののプログラミングとしては同じような形になります。

状態機械

Process MonadはMonadによるパイプラインで状態機械による制御を可能にしたものと考えることもできます。

詳しくは以下の記事を御覧ください。

まとめ

Scalaのパイプライン・プログラミングを以下のパターンに整理してみました。

  • 関数呼び出し
  • Functor
  • Applicative Functor
  • Monad
  • Reactive Stream

これらのパイプラインを適材適所で使い分ける、という切り口で関数型プログラミングを考えてみると面白いかもしれません。

2014年7月3日木曜日

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

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

純粋関数型言語の性質

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

モナドと圏論の位置付け

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

2012年7月13日金曜日

クラウド温泉3.0@小樽

今年もクラウド温泉の季節がやってきました。

今回もかなり濃い内容になりそうで楽しみです。ustreamなしのオフレコなので、かなり踏み込んだ話を聞けるのもクラウド温泉の醍醐味ですね。

今回は2つほどスライドを用意する予定です。

  • Monadicプログラミング・マニアックス
  • OFP & OFADについて(仮)

連休明けぐらいから、この2つのスライドについて:

で行ったような感じで、準備がてらブログに内容をあげていきたいと思います。この時は、以下のようなまとめを作りましたが、今回も作ることになると思います。

Monadicプログラミング・マニアックス

「Monadicプログラミング・マニアックス」というタイトルにしていますが、内容的にはScalazを使った関数型プログラミング入門という感じになると思います。現時点でScalaとScalazを使っている事自体がマニア、ということで。

内容的には、去年のJJUGで使った「楽々Scalaプログラミング」を起点にMonadicプログラミングの技やプログラミング戦略を色々書いていこうと思います。

http://www.slideshare.net/asami224/scala-9728001

キーワード: モナド、モノイド、型クラス、代数的データ型、永続データ構造、並列プログラミング、SQL。

OFP & OFADについて(仮)

こちらは、二日目午後に小樽商大会議室で行うフリーディスカッション「OFP & OFADについて」のネタ投入用のスライドです。基本的には「Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標」をそのまま使う予定ですが、OFADについて少し書き足したいこともあるので内容を追加できればと思っています。

このスライド起点の議論はOFADよりのものを想定していますが、これとは別にOOPとFP、OFPを軸にした議論もあると思うのでそちらも楽しみです。OOPもJavaのように実用志向で手続き型寄りのものが主流になっていますが、元祖オブジェクト指向でメッセージングのメタファを持つSmalltalkやJavaScriptのようなプロトタイプベースなど色々なバリエーションがあります。

個人的には業務アプリのプログラミングパラダイムはOOPからOFPにパラダイムシフトしていくと思っているのですが、このタイミングでOOPやFPの原点に立ち戻って議論することで、新しいOFP像を垣間見ることができればと期待しています。

2012年5月15日火曜日

Scalazの型クラス

Validationの定義を調べるついでにScalazの型クラスの継承関係などを調べたのでまとめておきます。

Monad

Monad関係の型クラスの継承関係は以下のようになります。


整理すると、Monad、Applicative Functor、Pointed Functor、Functorはそれぞれ以下の性質を持っていることになります。







FunctorPureApplyBind
Functor---
Pointed Functor--
Applicative Functor-
Monad

Functor&Pureは型クラスPointedとして定義されています。Pointed Functorと呼ぶこともあるようです。

Applicative Functorの型クラスApplicativeはFunctor&Pure&Applyとなります。Applicative Functorのapply演算は型クラスApplyが提供します。

Monadの型クラスMonadはFunctor&Pure&Apply&Bindとなります。Monadのunit演算、bind演算はそれぞれ型クラスPureと型クラスBindが提供します。Monadのjoin演算は型クラスとしては定義されていませんが、型クラスBindのbind演算から逆算する関数が定義されています。

機能順で示すと:

  • Monad > Applicative Functor > Pointed Functor > Functor

ということになります。型クラスPointed Functorは定義はありますが、実際に直接使用している所はほとんどないので実際はあまり意識することはありません。

代数的構造

代数的構造に関する型クラスの継承関係は以下のようになります。

型クラスSemigroupは半群(semigroup)、型クラスZeroは単位元(identity element)を記述する型クラスです。

型クラスMonoidは、型クラスSemigroupと型クラスZeroを継承した型クラスで、モノイド(monoid)を記述する型クラスです。

本来であれば、逆元(inverse element)群(group)があってもよさそうですが、これらは定義されていないようです。もちろん、環(ring)体(field)の定義もありません。

今の所、プログラミング言語の基本機能として有効な代数的構造はモノイド(半群、単位元)までということだと思います。

Scalazを使っていると、モノイドという数学(代数的構造)上の抽象概念が、プログラミングにおいて非常に便利なことを実感します。具体的には、モノイドは畳込み系(fold系)の演算で頻出します。

関数型プログラミングの「コメ」

モナドは、Scala本体でもflatMapメソッドのハードコーディングという荒業で実現していますが、Applicative Functorやモノイドについては今の所Scalazを使わないと利用することができません。また、モナドやファンクタも型クラスMonadや型クラスFunctorを使うことで、静的型付けの枠組みを活かしながら安全で発展的な運用を行うことができます。

Functor, Applicative Functor, Monad, Monoidが関数型プログラミングの「コメ」とするなら、現時点のScalaプログラミングではScalazを使うのが唯一の解ということになります。

並列プログラミング

代数的構造は、普通の関数型プログラミングでも有効な概念ですが、並列プログラミングではさらにその重要性が増すと考えています。ボクがScalazを使って代数的構造ベースのプログラミングの経験値を上げておきたいと考えているのも並列プログラミングが大きな動機となっています。

代数的構造に登場する結合律(associative law)、可換律(commutative law)、分配律(distributive law)は専門用語としてみると難しく感じますが、小学生の算数にも出てくる概念で、概念そのものは誰でも知っていることです。

ただし、これらの概念を任意のデータ構造とそれに対する演算に適用しようとすると、とたんに難しくなります。少なくても今までは普通のオブジェクト指向言語では、スコープ外の概念でした。

これが、HaskellやScala+Scalazといった現代風の関数型プログラミングで、始めて普通のプログラマの道具として使用できるようになったと認識しています。

この結合律、可換律、分配律が並列プログラミングでは非常に重要な概念になってくると推測しています。結合律、可換律、分配律を満たすオブジェクト群に対する演算は、処理の実行順番に対する制約が低くなるので、処理の最適化を自動化できる可能性が高まります。

メニーコア時代に入ると、一般のアプリケーションもネイティブな並列アプリケーションになってくると予測されるので、そうなれば一般のアプリケーション・プログラマにとっても基本的な素養となるということです。

結合律

結合律は現段階でもモノイドを用いて利用可能です。

たとえば、fold系の畳込みの対象がモノイドである場合には、畳込みの演算の評価順番が左からや右からといった制約がなくなるので、並列実行して処理の終わったところから畳み込んでいくといった最適化が可能になります。

具体的には「A + B + C」を「(A + B) + C」と計算しても、「A + (B + C)」と計算してもよいということですね。

分散カウンタ的なアルゴリズムをより汎用的な形で実現することにも使えそうです。分散カウンタの場合は、整数値の加算がモノイドであることを利用しているわけですが、これを任意のデータ構造に応用できるようになります。

型クラスMonoidを用いることで、こういったメカニズムを静的型付けで利用できるようになります。

可換律

結合律は型クラスMonoid(Semigroup)の導入ですでに実用化されていますが、次の課題は可換律です。

可換律を満たした演算は、結合律よりもさらに大きな最適化が可能になります。

前述のfold系の畳込みの例で言うと、畳込み対象が可換モノイドの場合は、畳込みの演算の評価順番だけでなく、演算の実行順番も変えることができます。

具体的には「A + B + C」を「(A + B) + C」と計算しても、「(A + C) + B」と計算してもよいということですね。並列処理では、どの処理がどの順番で完了するか事前に決定できないので、式の形を変形して実行順番を変えてよいというのは、非常に大きなアドバンテージになります。

前述の分散カウンタの例でも、整数の加算は可換モノイドでもあるので、この性質も利用可能です。任意のデータ構造に適用する場合も、モノイドより可換モノイドの方がより最適化された処理を実行することが可能になります。

現時点でのScalazでは定義されていませんが、いずれCommutativeSemigroupやCommutativeMonoidといった型クラスが登場して、これらの型クラスを使用した並列処理の関数などが登場するようになるのではないかと思います。Scalazでは、parMapメソッドやparBindメソッドでPromiseモナドを使った並列処理のファンクタ演算やモナド演算が使えますが、ここにCommutativeMonoidを対象としたparFoldReduceメソッドが提供されるというようなイメージです。

分配律

分配律は、二項演算が2つ登場してきて、難易度がぐっと上がってきます。代数的構造では環や体で分配律を扱いますが、プログラミングへの応用は当分先の話になりそうです。

Comonad

Monadの逆の動きをする型クラスComonad関連の型クラスです。

中心となるのが型クラスCojoinです。Scalazの型クラスには直接現れてきませんが、bind演算はmap演算+join演算です。このjoin演算がモナド演算の核となります。join演算は、入れ子になったモナドを一つにまとめる操作を行いますが、この操作時にそれぞれのモナド特有の結合処理を行うのが、モナド演算のミソとなっています。型クラスCojoinはこのjoin演算の逆演算をする型クラスです。つまり、単層のモナドから、モナドの入れ子を再構築します。

型クラスComonadは、このCojoinを利用して単層のモナドからモナドの入れ子を再構築します。

同様に型クラスCopureは型クラスPureの逆演算をする型クラスです。

Comonadは理屈は朧気(おぼろげ)ながらわかるのですが、実際のプログラミングでどのようなユースケースがあるのかは今の所判然としません。

ここでは存在を記録しておくにとどめておきます。

Category

ScalaやScalazのモナドは圏論の理論をプログラミングに応用した物です。その大元となる圏論の圏も型クラスCategoryとして型クラス化されています。

圏はオブジェクトと射(arrow、morphism)から構成されますが、この射を記述する型クラスがArrowです。

Category関連の型クラス群は、型クラスArrowの型クラスインスタンスが分からないと、プログラミングとの接点が見えてこないので、図に入れています。

  • Function1Arrow
  • PartialFunctionArrow
  • KleisliArrow
  • CokleisliArrow

現時点でのユースケースは、これらの型クラスArrowの型クラスインスタンスを用いて引数1の関数や、モナドを合成することに使用されます。

引数1の関数の合成は、Scala本体でもcomposeメソッドやandThenメソッドが用意されているので、それほどニーズがあるわけではありませんが、「関数型とデータフロー(2)」で取り上げた複雑な構成のパイプラインを構築するときには有効です。

モナドの結合はflatMapメソッドなどを使えばよいのですが、モナドを合成してより大きなモナドを作るということになるとKleisli圏が登場してきます。Kleisli圏ではモナドは射として扱われるので、射の合成としてモナドを合成することができます。

kleisli圏でのモナドの合成は並列処理を行うPromiseモナドで有力な利用方法です。flatMapなどによるモナドの結合ではflatMapのポイントで並列処理がブロックされてしまうので、結合したモナド全体で並列実行したい場合には、モナドの合成を行う必要があるためです。このケースが、型クラスArrowとその関連型クラスであるCategoryの最も有効な応用になると思います。

以上のように、現時点では型クラスCategoryとArrowはほぼPromiseモナドの専用品ですが、Lens(scalaz.Lens、Lenses: A Functional Imperative)といった応用が増えてくれば、より活用の範囲が広がるのではないかと思います。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月3日火曜日

関数型とデーターフロー(5)

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。

今回は「KliesliでOption/Listを合成」として用意した以下のスライドを説明します。

関数型でデータフローを記述する方式を以下の事前準備の記事で考えました。

関数型とデータフロー(4)」では、右の図を作成しました。Kleisliを用いてOptionモナドを合成しています。

この図をスライドに収めるために簡略化したものが右の図です。

スライド作成時にListモナドの合成も入れておいた方がよいと考え、右の図も作成し、スライドには左右に併置する形で入れました。

この新しく追加した図「KleisliによるListモナドの合成」について「関数型とデータフロー(4)」にならってについて説明します。

Listモナドの合成

ScalazはKleisliのための一連の機能を提供しています。☆はモナドのbind演算に用いる関数をKleisliに入れる演算を行う関数です。

val plus5l = (a: List[Int]) => List(a.map(_ + 5))
val mul10l = (a: List[Int]) => List(a.map(_ * 10))
val plus5lk = ☆(plus5l)
val mul10lk = ☆(mul10l)

☆はkleisli関数の別名なので、以下のように書くこともできます。

val plus5lk = kleisli(plus5l)
val mul10lk = kleisli(mul10l)

☆(plus5l)によってKleisli化されたplus5lが、☆(mul10l)によってKleisli化されたmul10lが定義されます。それぞれにplus5lk、mul10lkという名前をつけています。

関数plus5lkとmul10lkはそれぞれ以下のように動作します。Int型を引数にとってOption[Int]型を返します。

scala> List(1, 2, 3) |> plus5l
res16: List[List[Int]] = List(List(6, 7, 8))

scala> List(1, 2, 3) |> mul10l
res25: List[List[Int]] = List(List(10, 20, 30))

plus5lkとmul10lkは、いずれもKleisli化されている(Kleisliの容器に入っている)ので、以下のように演算子>=>で合成することができます。合成して作成した新しいモナド演算を行う関数にp5m10okという名前をつけます。

scala> val p5m10lk = plus5lk >=> mul10lk
p5m10lk: scalaz.Kleisli[List,List[Int],List[Int]] = scalaz.Kleislis$$anon$1@2d688eb5

以下のように期待通りに動作しました。

scala> List(1, 2, 3) |> p5m10lk
res26: List[List[Int]] = List(List(60, 70, 80))

ノート

データフローの実現方式にListモナドを入れたのは、Listモナドを使った並列プログラミングへ言及したかったためで、セッションでは口頭でその点に触れることができました。

ただ、今見返してみるとこの図の範囲だと、Optionを使って以下のようにした方が自然そうです。

val plus5l = (a: List[Int]) => a.map(_ + 5).some
val mul10l = (a: List[Int]) => a.map(_ * 10).some
val plus5lk = ☆(plus5l)
val mul10lk = ☆(mul10l)
val p5m10lk = plus5lk >=> mul10lk

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

scala> List(1, 2, 3) |> p5m10lk
res36: Option[List[Int]] = Some(List(60, 70, 80))

List処理を並列化する場合は、以下のようにパラレルコレクションを使いますが、この場合Kleisliの合成に使うモナドはListモナドではなくOptionモナドでもよいわけです。

scala> val plus5l = (a: List[Int]) => a.par.map(_ + 5).some
plus5l: List[Int] => Option[scala.collection.parallel.immutable.ParSeq[Int]] = <function1>
Promise

ここで、ListモナドやOptionモナドではなく、並列処理を専門に扱うモナドを導入するとさらに高度な並行プログラミングを行うことができます。そのような目的でScalazがサポートしているのがPromiseです。

Promiseについては以下のスライドが参考になります。

たとえば、以下のようにOptionモナドをKleisli化していたところを:

val plus5lk = ☆((a: List[Int]) => a.map(_ + 5).some)
plus5lk: scalaz.Kleisli[Option,List[Int],List[Int]] = scalaz.Kleislis$$anon$1@695a1b07

以下のようにすると:

scala> val plus5lk = ((a: List[Int]) => a.map(_ + 5)).promise
plus5lk: scalaz.Kleisli[scalaz.concurrent.Promise,List[Int],List[Int]] = scalaz.Kleislis$$anon$1@6f17b190

PromiseモナドをKleisli化されたものが返ってきます。(上側の型「scalaz.Kleisli[Option,List[Int],List[Int]]」と下側の型「scalaz.Kleisli[scalaz.concurrent.Promise,List[Int],List[Int]]」を比較するとイメージが湧いてくると思います。)このような形に持ってくると、Promiseモナドを使った高度な並行プログラミングが可能になるわけです。

関数やモナドの合成で、安全に高度な並行プログラミングを行うことが、今後のScalaプログラミングの方向性のひとつになってくるはずです。また、今回のセッションの中心的な話題であったデータフローモデルの実現手法という意味でも重要な技術です。

このあたりはKleisliやPromise以外にも色々な技がありそうなので、本ブログでも整理をしていきたいと思っています。

諸元

当日使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)

このスライド作成の様子は以下の記事になります。

まとめは以下の記事になります。

回顧は以下の記事になります。

2012年3月28日水曜日

EDAとオブジェクトと関数

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。

今回はセッションで説明したモデルの改良案として、EDA(Event-Driven Architecture)の導入を行ってみます。

元のスライド

オブジェクトと関数型の繋ぎのところを具体的に考えるために2つスライドを作成しました。

「イベント→データフローの流れ」は、OOPレベルの細かい処理の流れを示しています。

「ユースケースと関数」は、OOADの観点からユースケース・モデルからOOPを経て、FPに至る流れを示しています。

OOADを軸としたアプローチが検討対象にしているので、いずれも(協調→)状態機械→アクションというオブジェクト・モデルとして素直な連携を軸に考えています。

実際に図にして考えてみると、(協調→)状態機械→アクションという流れのモデリングはあまり便利そうに見えません。協調→状態機械のモデリングは難しいですし、仮にモデルをきちんと作ったとしても、モデルと実装の乖離が大きいため、実装が進むにつれモデルが朽ちていきそうです。

理論的には正しくても、便利でないものは使われないので、もう少し実装に近いモデルで再検討してみます。

EDA

クラウド・アプリケーションのアプリケーション・アーキテクチャでは、EDA(Event Driven Arachitecture)やEIP(Enterprise Integration Patterns)といったSOA、エンタープライズ系のアーキテクチャの技術が有効ではないかと考えています。

そこで、OOAD的な(協調→)状態機械→アクションではなくて、EDA上での実現を考えてみました。

イベント→データフローの流れ

スライド「イベント→データフローの流れ」の改良案です。

OOAD的には、イベント発生が状態機械に状態遷移を発生させ、状態機械のアクションによってプログラムの処理がキックされるというモデルになりますが、「状態機械→アクション」の部分をEDAに取り替えてみたのが以下の図です。


図に登場するEDAの構成要素は以下の通りです。

イベント
システム内で発生する事象
イベントプロデューサ
イベントを生成するエージェント
イベントチャネル
イベントプロデューサとイベントコンシュマーを結び付けるチャネル
イベント処理エージェント
イベントのルーティングを行うエージェント
イベントコンシュマー
イベントを受信するエージェント

イベントプロデューサがイベントを発生させると、イベントチャネルからイベント処理エージェントを経由してイベントコンシュマーにイベントが通知されます。

イベントコンシュマーは、元のOOADの図におけるアクションに対応します。ここで、イベントに対するアプリケーション・ロジックを記述します。

イベントコンシュマーの実装では、データフローと関数型でアプリケーション・ロジックの中核部分を記述し(データフローの実装技術)、イベントコンシュマー本体でエンティティの更新を行うという役割分担(オブジェクトと関数の連携(2))を採ることになります。

EDAの構成を採ることで、イベント発生とアプリケーション・ロジックを疎結合にし、アプリケーション・ロジックの追加や拡張を容易にします。複数のユースケース・コンテキストがドメイン・モデル上に重なってくるようなアプリケーションでは、ユースケース・コンテキスト(あるいはユースケース)毎にイベント・コンシュマーを用意することで、システムのモジュール化を促進します。

また、イベントチャネルを介在させることで、MOMやESBを使用した非同期処理での実装も可能になります。

実装

論理モデルとして、この構成でモデリングしておいても、設計/実装の段階ではイベントプロデューサからイベントコンシュマーを直接同期型で呼んでしまうという実現方法を選択することも可能です。

実装時のイメージとしては、右のようなものを想定しています。基本的な処理はイベントチャネルを介さず、同期型でイベントコンシュマーにイベントを通知し、処理結果を持ち帰ります。一方、それ以外の処理はイベントチャネル経由で非同期にイベントコンシュマーにイベントを通知して処理を行います。

ユースケースと関数

スライド「ユースケースと関数」の改良案です。

OOAD的な視点では、ユースケースから関数までの流れが重要になります。

ユースケースは自然言語で記述した物語ですが、システム開発につなげるためには、もう少しシステムよりのモデルを用意してユースケースから使用するような形に持ってくる必要があります。ここでは、タスクとサービスというモデル要素を用意しました。

タスクは、ユースケース・フローにおけるシステムの使用方法を定義したものです。システムが提供するサービスの使い方(パラメタなど)を定めます。

サービスはイベントプロデューサを呼出して(サービス自身がイベントプロデューサも可)、EDAのメカニズムの上で処理を進めます。ここから先は前節で説明した流れで処理が進んでいきます。(イベント処理エージェントは宣言的な定義で記述する事が多いと思われるので図ではOOPから外していますが、OOPやFPあるいはDSLによる実現が可能です。)

DSLによるモデル記述と実装への展開

EDA上でオブジェクト・モデルと関数型を連携させていく方法についてやや実装よりに細かく考えてきました。

このあたりは、g3フレームワークSimpleModelerで試行錯誤しながら色々と検討してきた点なのですが、ここでいっているような形では状態機械にはこだわらず、まずEDAベースでモデリングとフレームワークを構築するのがよさそうというのが最近の考えです。そのようなこともあり、今回ちょっと腰を据えて考えてみました。

今回考えたモデルをベースに、SimpleModelerとg3フレームワークでどのようにDSL化(モデルDSLとフレームワークDSL)していくのかというのが次の課題です。

諸元

当日使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)

このスライド作成の様子は以下の記事になります。

回顧は以下の記事になります。

2012年3月15日木曜日

関数型とデータフロー(4)

要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』で使用するスライドについて背景説明を行っています。

今回は背景説明第12弾として、「関数型とデータフロー(4)」として用意した以下の図を説明します。

前回はモナドを結合してデータフローを実現する方法を説明しました。

これが一般的なモナドの使い方ですが、モナドを部品として使いまわそうとすると、若干問題が出てきます。

モナドの合成

前回使用したOptionモナドを使ったデータフローの配線です。再利用のためにp5m10という名前をつけました。

val p5m10 = (_: Int).some >>= plus5o >>= mul10o

以下のように動作します。

scala> 3 |> p5m10
res64: Option[Int] = Some(80)

ここで問題なのが、p5m10を合成して新しいデータフローを作ることが難しいことです。試してみると以下のようになりますが、エラーメッセージから分かるとおりp5m10の入力型と出力型が合っていないのが原因です。

scala> p5m10 >>> p5m10
<console>:17: error: type mismatch;
 found   : Int => Option[Int]
 required: Option[Int] => ?
              p5m10 >>> p5m10
                        ^

scala> p5m10 >>= p5m10
<console>:17: error: type mismatch;
 found   : Int => Option[Int]
 required: Option[Int] => Int => ?
              p5m10 >>= p5m10
                        ^

関数型とデータフロー(1)関数型とデータフロー(2)で説明したとおり、関数の場合は以下のように「>>>」などを用いて関数の合成を行うことで新しい関数を生成することができます。このことによって、関数をデータフローの部品として使用することが容易になります。

scala> val p5m10 = plus5 >>> mul10
p5m10: Int => Int = <function1>
scala> 3 |> p5m10
res0: Int = 80

モナドをデータフローの部品として使用することを考えると関数の合成と同様の使い勝手で利用できるモナドの合成が必要になってきます。

kleisli

モナドの合成で登場するのがクライスリ圏(kleisli category)です。クライスリ圏については以下のページが参考になりますが、理論的にはかなり難解でボクもきちんとは把握できていません。今の所、モナドを合成するにはクライスリ圏に持ち上げる、というように理解しています。

ただ、プログラミングのイディオムとしては慣れてしまえばそれほど難しくはありません。簡単に言うと、モナドを合成するためにKleisliという入れ物を用いる、ということです。プログラミングイディオムとしてのKleisliは以下のページが参考になります。

そこで、図に登場する以下の式です。

ScalazはKleisliのための一連の機能を提供しています。☆はモナドのbind演算に用いる関数をKleisliに入れる演算を行う関数です。

val plus5o = (a: Int) => (a != 5).option(a + 5)
val mul10o = (a: Int) => (a % 10 != 0).option(a * 10)
val plus5ok = ☆(plus5o)
val mul10ok = ☆(mul10o)

☆はkleisli関数の別名なので、以下のように書くこともできます。

val plus5ok = kleisli(plus5o)
val mul10ok = kleisli(mul10o)

☆(plus5o)によってKleisli化されたplus5oが、☆(mul10o)によってKleisli化されたmul10oが定義されます。それぞれにplus5ok、mul10okという名前をつけています。

関数plus5okとmul10okはそれぞれ以下のように動作します。Int型を引数にとってOption[Int]型を返します。

scala> 3 |> plus5ok
res3: Option[Int] = Some(8)

scala> 3 |> mul10ok
res4: Option[Int] = Some(30)

plus5okとmul10okは、いずれもKleisli化されている(Kleisliの容器に入っている)ので、以下のように演算子>=>で合成することができます。合成して作成した新しいモナド演算を行う関数にp5m10okという名前をつけます。

scala> val p5m10ok = plus5ok >=> mul10ok
p5m10ok: scalaz.Kleisli[Option,Int,Int] = scalaz.Kleislis$$anon$1@6a2a9a0e

以下のように期待通りに動作しました。

scala> 3 |> p5m10ok
res5: Option[Int] = Some(80)

Kleisli化されたOptionモナドの動作

Kleisli化されたOptionモナドを使ったデータフローの配線は以下のものになります。

plus5ok >=> mul10ok

Int型の値を受け取り、これをplus5ok関数、mul10ok関数で合成された関数に流していきます。概念的には、図にあるようにKleisliの箱の中にあるOptionの箱の中を左から右へデータが流れていきます。

以下ではそれぞれのパイプラインの動作についてみていきましょう。パイプライン内の動作は基本的に前回のものと同様です。

正常動作

データフローにSome(3)を流すと、データフローの計算は成功し、計算結果としてSome(80)が出力されます。

plus5oでエラー

データフローにSome(5)を流すと、データフローの計算は失敗し、計算結果としてNoneが出力されます。

5はplus5oでエラー判定されるため、plus5oから失敗側のパイプラインが動作します。

mul10oでエラー

データフローにSome(15)を流すと、データフローの計算は失敗し、計算結果としてNoneが出力されます。

15はplus5oでは正常な値なので、plus5oの結果は20となりますが、この20がmul10oでは10で割り切れるためエラー判定され、mul10oから失敗側のパイプラインが動作します。

ノート

「関数型とデータフロー」というタイトルで、関数とモナドを使ったデータフロー構築手法についてみてきました。

関数、モナドという軸と呼出し、合成という軸があります。それぞれの軸の組合せによる、関数/モナドの呼出しと合成の見取り図を表にしてみました。








関数/モナドの呼出しと合成
呼出し合成
関数g(f(x))f >>> g
モナドx.some >>= f >>= g☆(f) >=> ☆(g) |

簡単な応用であれば左側の「呼出し」で十分なのですが、データフロー的な利用方法を目指して部品化、再利用を考えると右側の「合成」が必要になってきます。

関数の合成は、ScalaでもcomposeメソッドやandThenメソッドとして実現されていますが、一連の記事ではScalazのArrowの方を利用してみました。(ScalazではArrowというメカニズムの上で関数合成を可能にしています。Arrowは圏論における射(arrow, morphism)の実現なので、適切に圏を設定すれば関数以外の射の合成にも適用できます。)

モナドの合成についてはScalaの標準機能にはないのでScalazのKleisliを利用する必要があります。

2012年3月14日水曜日

関数型とデータフロー(3)

要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』で使用するスライドについて背景説明を行っています。

今回は背景説明第11弾として、「関数型とデータフロー(3)」として用意した以下の図を説明します。



モナド

今回はモナドを使って、データフローのパイプラインの振舞いをよりリッチなものにしてみます。

今回使用するのは、成功パイプラインと失敗パイプラインの2つのパイプラインを持つモナドであるOptionモナドです。正常系の演算は成功パイプライン上で行われます。Optionモナドの異常系は、エラー発生時点で即失敗パイプラインに移行し、そのままデータフローを終了させます。エラー情報の通知もありません。

以下ではまずOptionモナドで使用する関数オブジェクトを説明した後、Optionモナドの動作について説明します。

モナドで使用する関数オブジェクト

図ではモナドで使用する関数plus5oとmul10oを定義しています。どちらも、Int型を引数に取って、Option[Int]を返す関数です。Scalazを用いて簡潔に記述しています。

plus5o

関数plus5oの定義は以下の通りです。

val plus5o = (a: Int) => (a != 5).option(a + 5)

前回、前々回に使用した関数plus5のOptionモナド版です。関数plus5では、引数にInt値を取り、返却値もInt値でしたが、モナド版のplus5oでは、引数にInt値を取るのは同じですが、返却値はOption[Int]となり、本来の返却値をOptionモナドに包んで返します。

Optionモナドを返すので、関数が成功したのか失敗したのかをOptionのSomeまたはNoneとして通知できるようになりました。引数が5の場合は失敗としてNone、それ以外は成功で引数に5を加えた値をSomeに包んで返します。

このようにOptionモナドを返す関数(正確には型Tを取りOption[T]を返す関数)は、Optionモナドのbind演算(ScalaではflatMapメソッド、Scalazでは>>=メソッド)に適用することができます。

さらに汎用的にいうと、型Tを取りモナドM[T]を返す関数はモナドM[T]のbind演算に適用することができます。今回の例はOptionモナドですが、他にもListモナド、EitherモナドなどScalaでは多数のモナドが用意されています。

普通にScala的に書くと以下のようになります。

val plus5o = (a: Int) => if (a != 5) Some(a + 5) else None
mul10o

関数mul10oの定義は以下の通りです。

val mul10o = (a: Int) => (a % 10 != 0).option(a * 10)

引数が10で割り切れる場合は失敗としてNone、それ以外は成功で引数を10倍した値をSomeに包んで返します。

普通にScala的に書くと以下のようになります。

val mul10o = (a: Int) => if (a % 10 != 0) Some(a * 10) else None
参考

Scala文法上の参考情報です。

関数オブジェクトplus5oを関数リテラルを用いて記述しましたが、これを普通の書き方で関数として定義すると以下のようになります。

def plus5od(a: Int): Option[Int] = {
  if (a != 5) Some(a + 5)
  else None
}

この関数を関数オブジェクトにするには、もう一段以下の処理を行います。

val plus5o = plus5od _

Optionモナドの動作

Optionモナドを使ったデータフローの配線は以下のものになります。

(_: Int).some >>= plus5o >>= mul10o

Int型の値を受け取りSomeに包んで、plus5o関数、mul10o関数に流していきます。概念的には、図にあるようにOptionの箱の中を左から右へデータが流れていきます。

前回、前々回のデータフローと違うのは正常系のパイプラインと異常系のパイプラインの2系統のパイプラインが用意されていることです。

以下ではそれぞれのパイプラインの動作についてみていきましょう。

正常動作

データフローにSome(3)を流すと、データフローの計算は成功し、計算結果としてSome(80)が出力されます。

plus5oでエラー

データフローにSome(5)を流すと、データフローの計算は失敗し、計算結果としてNoneが出力されます。

5はplus5oでエラー判定されるため、plus5oから失敗側のパイプラインが動作します。

mul10oでエラー

データフローにSome(15)を流すと、データフローの計算は失敗し、計算結果としてNoneが出力されます。

15はplus5oでは正常な値なので、plus5oの結果は20となりますが、この20がmul10oでは10で割り切れるためエラー判定され、mul10oから失敗側のパイプラインが動作します。

ノート

以上で説明したようにモナドを使うことで、データフローを構成するパイプラインに特殊な振舞いを付加することができます。

ここでは最も単純な(しかし強力なのでScalaプログラミングでは必須の)Optionモナドを使用しましたが、この他にも様々な種類のモナドが提供されています。

たとえば、条件によってパイプラインを切り替えながら動作させる場合にはEitherモナドが有効です。

また、複数の値を同時に流す場合はListモナドやStreamモナドを使います。この場合は、パイプラインを並行実行させ、並行処理を行うことも可能です。各パイプラインの動作の待合せも自動的に行ってくれます。

モナドやMonadicプログラミングというとかなり敷居が高い感じがしますが、ここで説明したようにデーターフローをパイプラインで実行するようにイメージするとずいぶん印象が変わってくると思います。

2012年3月13日火曜日

関数型とデータフロー(2)

要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』で使用するスライドについて背景説明を行っています。

今回は背景説明第10弾として、「関数型とデータフロー(2)」として用意した以下の図を説明します。


動作

図の動作は、入力されたInt値からデーターフロー的な処理を4段行い、最後に並行して計算した結果を足しこんで最終的な結果の値を計算しています。

これを、関数型の以下の記述方式で実現しています。

3 |> (plus5 &&& mul10) >>> (plus5 *** mul10) >>> plus5.first >>> plus5.second >>> join2

これは、Scalazの型クラスArrowで実現しています。Arrowのメカニズムは、以下のページが詳しいです。

構成要素

データフローの格段の処理を細かく見ていきましょう。


Fork
3 |> (plus5 &&& mul10)

「分岐」とすると条件分岐と紛らわしいのでForkとしておきました。値を2つのパイプラインにForkして、それぞれに関数plus5とmul10を適用します。結果は、タプルで返ってきます。

並列計算
(8, 30) |> (plus5 *** mul10)

タプルに格納された2つの値に対して並列演算します。結果は、タプルで返ってきます。

First
(13, 300) |> plus5.first

並列計算のパイプラインの1番目のパイプラインに関数を適用します。結果は、タプルで返ってきます。

Second
(18, 300) |> (plus5 *** mul10)

並列計算のパイプラインの2番目のパイプラインに関数を適用します。結果は、タプルで返ってきます。

結合

結合は、以下の関数で行っています。この関数では、タプルに格納された値を足しあわせています。

val join2 = { fs: (Int, Int) => fs.fold(_ + _) }

ノート

Scalazが提供するArrowまわりの部品を使って、それなりに複雑なデータフローを記述することができることが分かりました。もちろん、本格的なデータフローをここで紹介した部品だけで構築するのはちょっと難しそうですが、関数型の他の機能と合わせ技で色々なことが可能になりそうです。

2012年3月12日月曜日

関数型とデータフロー(1)

要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』で使用するスライドについて背景説明を行っています。

今回は背景説明第9弾として、「関数型とデータフロー(1)」として用意した以下の図を説明します。

オブジェクト・モデリングと関数型の連携ではデータフロー・モデルが重要な位置付けにあることを説明してきました。そして、前回「データフローの実装技術」では具体的な実装技術について取り上げました。

関数型言語でデータフロー・モデルを扱うための要素技術として、関数型言語上でデータフローをどのように記述するのかという記述方式が論点の一つとなります。

関数型言語上でデータフローモデルを記述する絶対確実な方法としてデータフローを構成する各ノードとノード間のリンクを集合的に記述する方法がありますが、可読性はあまりよくないので、あくまでも最終手段として考えておきたいところです。

そういう意味で、前回紹介したSparkでの以下の記述は、関数型言語として自然であり、とても魅力的です。

val file = spark.textFile("hdfs://...")
 
file.flatMap(line => line.split(" "))
    .map(word => (word, 1))
    .reduceByKey(_ + _)

この記述は、以下の処理をデータフロー的に繋ぐ表現を行っています。

  • テキストデータを各行毎に空白を区切り記号にして単語に分割
  • 書く単語毎に単語とカウンタの対のデータ構造に変換
  • 単語毎に値を加算で畳込み

このデータフローの実行の結果、テキストファイル内に存在する単語の単語数の一覧を得ることができます。

関数型言語を使うと、このように関数型言語的にもデーターフロー的にも自然な記述が可能になりそう、ということが分かりました。そこで、今回から数回に分けてScalaを例に関数型言語でデータフローを記述する方式について考えていきます。(セッションでは、ここで考えた内容を1,2枚のスライドに圧縮して説明することになると思います。)

基本

今回の図は、関数型言語でデータフローを記述する方法を考える基本形です。

まず、関数オブジェクトplus5とmul10を定義します。plus5は引数に5を加えたInt型の値を返す関数、mul10は引数に10を掛けたInt型の値を返す関数です。

val plus5 = (_: Int) + 5
val mul10 = (_: Int) * 10

この2つの関数を使ってInt型に5加え、さらに10を掛けるデーターフローについて考えます。このデータフローにInt値3を渡すと80が返ってきます。

普通の記述

この演算を普通に記述すると以下のようになります。これは、関数型言語でも手続き型言語でも共通のごく普通の記述方式です。ここでは関数呼び出し方式と呼ぶことにします。

mul10(plus5(3))

このように考えてみると、この基本的な記述方式も一種のデータフローと考えることができます。ただし、図で示したデータフローと記述方式のマッピングが直感的には分からないという問題があります。

関数合成

関数型言語では、関数合成によって複数の関数を合成して新しい関数を定義することができます。

以下では、関数オブジェクトのcomposeメソッドとandThenメソッドで関数合成しています。

(mul10 compose plus5)(3)
(plus5 andThen mul10)(3)

関数合成は、関数オブジェクトをプログラム的に操作して柔軟な結合ができるようになるのが魅力ですが、データフローの記述に用いるには、記述が冗長なのと、データフロー的な意味での可読性はあまり高くありません。

Scalaz

Scala用のクラスライブラリScalazを用いると、以下のような記述が可能になります。

3 |> plus5 >>> mul10

データフローの左側からInt値3が右側の方向に流れていくことが直感的に分かる記述方式になっています。

これは一種のDSLといえますが、Scalaの関数呼び出しに型クラスのメカニズムを用いて文法糖衣的な皮をかぶせているだけで、内部的にフレームワーク的な新しい演算メカニズムを導入しているわけではありません。つまり、関数型言語の持つデータフロー的なセマンティクスを活かす表現方式となっているわけです。

2012年3月7日水曜日

オブジェクトと関数の連携(2)

要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』で使用するスライドについて背景説明を行っています。

今回は背景説明第6弾として、「オブジェクトと関数の連携(2)」として用意した以下の図を説明します。

オブジェクトと関数の連携(1)では、オブジェクトの世界と関数の世界を完全に分けてしまうのがよい、と説明しました。今回は、その具体的な連携方法について考えます。

Scalaでは関数もオブジェクトの一種なので、そういう意味ではすべてオブジェクトの世界で動いています。ここでは、純粋関数型言語の枠組みのみを使用しているものを関数型の世界、それ以外のものをオブジェクトの世界と呼ぶことにします。

図では、オブジェクトの世界を左側に関数の世界を右側に配置しました。

更新処理の流れ

オブジェクトの世界では、オブジェクトがグラフ構造で格納されています。このグラフ構造の一部を、関数を使って更新する流れについて見ていきましょう。

純粋関数型では副作用がありません。つまり、オブジェクトの内容や構造を更新することはできません。関数ができることは、計算をすることのみです。

とはいえ、純粋関数型言語でもインタラクションゲームデータベースアクセスを書くことができます。

副作用はないにもかかわらず、プログラムの外の世界の副作用は記述できるわけですね。この矛盾を解決するためのちょっとしたトリックがあります。このトリックがオブジェクトと関数を連携させる場合でも鍵となります。

基本データ構造

一般的な処理で関数が扱う構造は大きく以下の4種類です。

  • 値(構造は持たない)
  • リスト

この中で最も複雑な構造が木構造です。木構造までは再帰によって関数で自然に記述できます。これより複雑な構造はグラフ構造になりますが、これは特別な扱いを必要とするので、一般論を考える時は除外して考えるとよいと思います。(グラフ構造を扱うときは、木構造に参照を追加したり、表(行列)の形にしたり(ノードの集合として扱う)という操作の仕方になります。)

関数で自然に扱える最も複雑な構造が木構造で、表、リスト、(構造を持たない)値は木構造を単純化した構造と考えることができます。以上の点から、以下では木構造をベースに考えていきます。木構造でできることは表、リスト、(構造を持たない)値でも同様に適用できます。

データ構造の抽出

可変オブジェクトのグラフ構造から、処理に必要な情報を木構造として抽出します。この情報を、代数的データ型と永続データ構造を用いて表現します。どちらも不変オブジェクトで、変更を行うことはできません。

図ではグラフ構造としての情報は、木構造に参照を追加する形で表現しています。

データ構造のコンバージョン

関数での計算の基本は木構造のコンバージョンです。データ構造を更新することはできないので、複製しながら複製過程でその一部を変えることで、木構造のコンバージョンを行います。

更新指示書

関数では計算ができるだけなので、直接オブジェクト側のデータ構造の更新はできません。

単純なケースでは、関数で計算したデータ構造を直接オブジェクト側のデータ構造に上書きしてしまうことで、更新処理を完結させることができます。

問題は、そのようなことはできない複雑なデータ構造の場合ですね。

その場合、関数は木構造から「更新指示書」を計算します。ここが「トリック」です。

関数はあくまでも更新指示書の計算を行うのみで、更新処理そのものにはタッチしません。副作用という下世話な処理は、関数型言語の外側の誰かが更新指示書を見ながらやってくれる、という役割分担になっています。この役割分担によって、純粋関数型の純粋性が保たれているわけです。

計算結果の反映

オブジェクト側では、関数から返ってくる更新指示書に従って、可変オブジェクトのグラフ構造を更新します。

副作用はオブジェクト側で引き受けることで、関数の世界の純粋性を守りながら、オブジェクトと関数の連携を行うことができます。

ノート

更新指示書を使った更新処理はかなりややこしいです。

オブジェクト指向から関数型に入ってくるときに戸惑うのはこの点で、この(擬似)更新処理を円滑に行うためのテクニックが数多く存在します。モナドもその一つですね。先ほど紹介したゲームは名前がMonadiusですが、これはモナドを使っているのが名前の由来とのことです。

こういった、ややこしいメカニズムで関数型の純粋性を保つと何がよいのかというと、数学の理論の範囲内で処理を完結させる事ができるということです。これは、理論的にはプログラムが証明可能ということです。

もちろん現時点では、普通の関数型言語ではプログラムを証明することはできないのですが、関数型の枠組みでプログラミングすることによって間接的にバグの混入を大幅に防ぐことができます。

たとえば、並行処理を行う小さなモナドをたくさん用意して、このモナドを合成して一つの大きなサービスを記述するとします。各モナドは代数的構造デザインパターンで取り上げた代数的構造を持つ代数データ型を操作するとします。

このようなモナドの合成では、代数的に証明されたメカニズムでモナドを合成することができ、さらに代数データ型の枠組みの中で処理の最適化を行うことも可能になるはずです。

「小さなモナド」側で十分に検証してバグを取っておけば、これらのモナドを合成して作成したサービスは、問題なく動作することが期待できます。(モナドの合成メカニズムそのものは数学的に証明されたメカニズムが使用されるはずなので。)

並行プログラミングでは、このようなプログラムの作り方が非常に有益です。並行プログラミングでは、単純なケースを除いてはデバッガによるデバッグはほぼ不可能と考えてよいので、プログラミングの時点でバグが混入しづらいメカニズムの存在が大きな鍵になります。

このあたりの考えを進めていくと証明駆動や形式手法といった方面に進んでいくことになりますが、その土台は関数型プログラミングなので、まずは関数型プログラミングに習熟することが大事です。

SQL

「更新指示書」によるメカニズムはSQLを用いたデータベースプログラミングと同じことをしていると気づくと、処理の内容が腑に落ちると思います。(SQLでは木構造ではなく表構造になります。)

「更新指示書」としてSQL文を作る関数を書けば、オブジェクトの世界を飛ばして、状態はすべてデータベースに格納管理するというアプローチも可能です。こうすることで、データベースをバックエンドに使う一般的なWebアプリケーションを純粋関数型言語で記述できますね。

トランザクション

「更新指示書」によるメカニズムはトランザクション処理とも相性がよいです。

トランザクション処理では、処理がアボートした時には、データが処理前の状態になっていなければなりません。このため、データにロックをかけて随時情報を更新していくというアプローチでは、アボートした時にデータを戻す処理を行わなければなりません。

このような手間が必要なので、「更新指示書」方式のアプローチでも処理の複雑さは変わりません。関数型では言語の特性上「更新指示書」アプローチになるので、この上でトランザクション処理を考えていけばよいわけです。また、(ロックをかけない)楽観的アプローチは、「更新指示書」アプローチと相性がよいので、そういう意味でも「更新指示書」アプローチは有効です。

つまるところ、関数型言語はトランザクション処理と相性がよいということですね。

2012年2月29日水曜日

関数型言語の技術マップ

要求開発アライアンス定例会で『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』というタイトルでセッションを行うことになりました。

セッション時間が50分なので、かなり俯瞰した形での全体像の説明になりそうですが、関連する要素技術の数が多いのと、内容が込み入っているので、ブログで補足説明をすることにしました。

今回はその第一弾です。

「関数型言語の関連技術」として用意した以下の図を説明します。関数型プログラミング言語レベルの説明はScalaを対象にします。

Disclaimer

2008年にScalaをはじめて足掛け4年、関数型プログラミングとは、どうも数学を使ってプログラミングしていくことらしい、ということが分かってきました。

ScalaをBetter Javaとして使うのであれば、そこまで頑張らなくてもよいのですが、関数型言語のパワーを引き出すにはやはり関数型プログラミング、さらにいうと関数合成をベースとしたMonadicプログラミングをしていく必要があります。

ボク自身はにわか関数型プログラマですし、数学や計算機科学は門外漢なので関数型言語の背景技術を調べるのはなかなか辛いのですが、関数型プログラミングをする以上は避けて通れないので、牛歩のようなスピードですが、少しづつ調べています。

現時点で分かったことをまとめたのが上の図です。

計算機科学、数学の観点からは緩い点もあると思いますが、逆にオブジェクト指向プログラマの目から見た関数型言語という観点で見て頂けると、これから関数型言語にアプローチする人にはよいスタートポイントになるかもしれません。

Curry-Howard対応

プログラミング言語の中での関数型言語の位置付けを考える上で重要なのがCurry-Howard対応(Curry-Howard correspondence)です。

Curry-Howard対応の詳細は上記Wikiページやk.inabaさんのCurry-Howard Isomorphism も参考になります。

ざっくりいうと、「単純型付ラムダ計算」と「直感主義命題論理&自然演繹」がIsomorphism(同型)ですから文字通り相互変換が可能ということです。この枠組みの中では「型=命題」、「計算=証明」となり、コンパイルが成功すれば証明完了となります。まさにプログラミングが数学の証明と同じということです。凄いですね。

残念ながら「直感主義命題論理&自然演繹」で記述できる範囲は狭いので、汎用的に一般の問題を記述できるわけではありません。

しかし、同型という形で数学と直結している点が重要です。「直感主義命題論理&自然演繹」を軸に、数理論理学の膨大な理論体系をプログラミングに取り込む可能性がみえてきます。

純粋関数型言語

関数型言語は大きく純粋関数型言語と(純粋でない普通の)関数型言語に分類することができます。

純粋関数型言語は、副作用がないといった性質が有名?ですが、重要なのは「単純型付ラムダ計算」をプログラミング言語として実現した物ということであろうと思います。

「純粋関数型言語」=「単純型付ラムダ計算」であればさらに、「純粋関数型言語」=「直感主義命題論理&自然演繹」であるわけで、プログラミング言語と数理論理学が直結することになります。

ハードウェアの壁

純粋関数型言語は、「マッカーシーの5つの基本関数」 の時代から関数型言語の理想の姿でした。関数型言語における関数の評価は、ラムダ計算を基盤にしていて、数学的に記述して証明できることがそもそもの存在理由だったわけです。

しかし、ハードウェア性能の壁を乗り越えることは難しく、現在に至っています。関数型言語は実用性能を得るために、手続き型言語機能やオブジェクト指向言語機能を取り込んで、事実上関数型言語的な手続き型言語、関数型言語的なオブジェクト指向言語として使用されてきました。

いうまでもありませんが、ハードウェア性能の向上はこういった制約を取り払いつつあります。

Webアプリケーションなどのアプリケーションプログラムでは、RubyやPythonといったインタープリタ型のスクリプト言語が実用言語として十分機能することが明らかになってきました。コンパイル型の純粋関数型言語ではこの壁はすでに超えていることは容易に推測できます。

Scalaの立場

ハードウェア性能が純粋関数型言語を動作させるのに十分なレベルに向上してきたのではないかということを説明しました。しかし、それでは一足飛びに純粋関数型言語に切り替えてしまえばよいのかというと、そう簡単でもありません。

ハードウェア性能が向上したといっても、やはりぎりぎりの局面では副作用のあるプログラムにしたいケースは残りそうです。仮にそういう事は事実上は滅多にないにしても、いざという時のために保険の意味で機能は残しておきたいのが人情です。

もう一つは、純粋関数型言語で状態を扱う技術であるモナドの難易度が高いことです。習得コストがかなり高いので、すでにオブジェクト指向言語で普通にプログラミングできるエンジニアが、コストをかけて学ぶメリットがあるのかという問題が出てきます。モナドを習得してはじめてスタートラインに立てるというだけなので、習得のインセンティブはかなり小さくなります。

これらのニーズを満たす解として考えらられるのがScalaが採用しているハイブリッド方式です。

Scalaでは、キーワードval、mutable/immutableの2種類がパラレルに用意されているコレクションライブラリ、代数的データ型を実装するのに適したcase classとキーワードsealed、といった形で副作用のない純粋関数型的なプログラミングを行うための仕掛けが多数用意されています。

この範囲でプログラミングしておけば、純粋関数型言語に近しい効果を得ることができます。

Scalaでは不変オブジェクトであるListは何も設定しなくても使えるのに対して、オブジェクト指向プログラミングに必須のArrayBufferは、importしなければ使えないようになっています。これは、純粋関数型的な利用方法を推奨するのがScalaのスタンスということの現れだと思われます。もちろん「純粋」では対処できない問題に対して(Javaと同等以上の)通常のオブジェクト指向プログラミングも可能になっています。

また、アルゴリズムのコアの部分は純粋関数型的に記述するとしても、それを取り囲む周辺機能は普通のオブジェクト指向プログラミングで記述するのも可能です。このため、関数型プログラマとオブジェクト指向プログラマが同一言語で協業するような運用も可能になります。

もちろん、不慮のバグで副作用が発生する可能性もあるので、純粋関数型言語のように安全というわけではありませんが、プログラマが気をつければ、純粋関数型プログラミングのメリットを享受できるようになっているわけです。

新しい要素技術

純粋関数型言語と相性のよい色々な要素技術が追加されています。

  • 代数的データ型
  • 永続データ構造
  • 型クラス

代数的データ構造は、代数的な計算に適したデータ型です。Scalaでの実装は不変オブジェクトであることに加えてキーワードsealedを用いることで、開いた形での継承によるポリモーフィズムを使わないようにします。

永続データ構造は、副作用を持たないデータ構造です。純粋関数型データ構造(pure functional data structure)という用語もあり、同じ意味を持つと思われます。一度作ったデータ構造は、二度と変更されることがなくメモリ上に残り続けるので「永続」と呼ばれています。(不揮発性のストレージに格納するという意味の「永続」ではありません。)不変オブジェクトのみで木構造やグラフ構造といったものを実現します。関数型プログラミングでは永続データ構造を使うスキルが重要になってきます。

型クラスは、「アドホック・ポリモーフィズムを提供する型システム」と定義されています。

オブジェクト指向的にいうと、アルゴリズムを実現したフレームワークと処理対象のデータオブジェクトを、それぞれ相互に依存しない形で実装したものを、どちらも変更することなく後付で接続するためのメカニズムです。

Scalaでは暗黙パラメタを使ったConceptパターンという手法で実現します。暗黙パラメタに対する文脈として型クラス・インスタンスをバインドすることで、フレームワークとデータオブジェクトをプログラム上は疎結合のままコンパイル時に接続できるのがミソです。

群論と圏論

型クラス導入前から、Scalaでもモナドを使うことはできましたが、コンベンションを使用した決め打ちの実装で、拡張性に乏しいものでした。

モナド以外にも、関数型プログラミングに有用な抽象代数学の概念はたくさんあるので、これらを必要に応じて取り入れていきたいニーズがあります。この目的に適したメカニズムが型クラスです。

型クラスは純粋関数型言語であるHaskellが導入した言語機能で、Haskellのクラスライブラリで代数的なメカニズムを構築するのに使用されています。

Scalaでは、型クラスを実現するクラスライブラリScalazを用いると、群論(モノイドなど)や圏論(モナドなど)が定義しているさまざまな代数的な処理をプログラミングテクニックとして利用することが可能になります。

代数的な計算メカニズムのサポートは、当然ながら型付ラムダ計算とも相性がよく、さらに他の数学分野との連携でも有効に機能すると思われます。

発展の方向

数理論理学では、命題論理は基本中の基本ですが、述語論理や様相論理といった形で、よる複雑で応用範囲の広い理論が存在しています。

現段階では、ボクの調べた範囲では、述語論理や様相論理を関数型プログラミングの基本テクニックとして活用できるようにはなっていないようです。

とはいえ、最近話題の証明駆動という形になるのか、論理型言語に昇華していくのか、着地点は分かりませんが長い目で見ればいずれそういう方向に進むのではないかと思います。

圏論の上に論理学の圏を載せたものとしてトポスという理論体系があるようです。また、論理学と代数の裏側には常に集合論が見え隠れしています。このあたりの相互に関連を持つ数学の理論群がいろいろな形で関数型プログラミングにも取り入れられていくことが期待できます。

プログラミング言語が数学と直結することで、数学の理論体系がある意味、プログラムから利用できる具体的な機能となるわけです。実際にモナドやモノイドといった数学上の概念が型クラスという形で利用可能になりました。このポテンシャルはかなり大きく、今後プログラミング技術が大発展するホットスポットになるのではないかと思います。

2012年2月27日月曜日

Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標

3月19日に要求開発アライアンスの3月定例会で『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』というタイトルでお話させていただくことになりました。

今回の定例会のテーマは『要求開発×クラウド』で丸山先生の『エンタープライズ・クラウドの現在』との二本立てとなっています。

内容は以下のようなものになる予定です。個人的にはクラウド、FPの文脈を取り込んだOFADの大枠みたいなものが朧気ながらみえてきた気がするので、各種の論点についてOOADの専門家の皆さんのご意見をお聞きできればという感じで考えています。

  • OOADの問題点
  • 最新FP with Scala
    • trait, monad, type class
  • クラウドプラットフォーム
  • モデリング&アーキテクチャ
    • DDD, DCI, CQRS, EDA
  • その他の技術動向
    • DSL, コード中心, Agile/Lean, SA/SD
  • OFP = OOP+FP
  • OFAD
  • OFAD with Scala

セッション時間は50分ですが、OFADの要素技術、関連技術が多岐に渡るため個別の技術についてはOFADの文脈における枠組みを示すにとどまることになります。

今までは、こういった場合、スライドを作りすぎて駆け足になってしまったり、作ったスライドを捨てたりすることが多く、なかなか情報を的確に伝えきれないというのが反省点になっていました。駆け足で説明をはしょったり、削った項目についてはその場ではブログでまとめておくことを考えたりするのですが、セッションが終わってしまうと集中力が切れるためか、結局そのまま放置ということになってしまっています。

そこで、今回は個別の技術については、準備ができたものについては事前にブログ上に説明を上げておくことにしました。セッション(スライドはセッション後に公開予定です)と合わせてより包括的に情報を伝達することができればと思います。

2012年1月11日水曜日

SmartDoxの実装技術

昨日はSmartDoxの紹介をしましたが、 今日はSmartDoxの実装技術について説明します。
既存のプログラムの改良では、今まで積み上げてきたアーキテクチャや 使っているフレームワークの関係があって、 思い切ったコーディング方針の変更はなかなかできません。
SmartDoxは新規開発なので、練習も兼ねて ボクが認識している今風のScalaプログラミングを取り入れてみました。
  • Scala
  • sbt
  • conscript
  • GitHub
  • 関数型プログラミング
  • 代数データ構造
  • Scalaz
  • パーサーコンビネータ

Scala, sbt

実装言語Scala、ビルドツールsbtは現在の既定路線です。
sbtは、後発だけあって何かと使いやすいのと、Scalaプログラミング特有の 色々な事情をハンドリングしてくれるので、Scalaプログラミングでは必須です。
たとえば、Scalaは、コンパイルしたScalaバージョンの異なるライブラリのバイナリ互換が鬼門で、 JavaだとJARファイルが libname-1.0.jarとなるところを、Scalaだとlibname2.9.1-0.1.jarといった具合に 使用するScalaライブラリのバージョン(2.9.1)をファイル名に埋め込むのが、基本的な運用に なっていますが、sbtはこのあたりをうまくハンドリングしてくれます。
編集、デバッグにはEclipseを使っていますが、sbtのEclipseプラグインで「.classpath」を生成して、 ライブラリの依存性を取り込む運用にしています。
ただし、sbtだけでは力不足のところもあります。 以下の処理はsbtでやり方を見つけられなかったのでmavenとantを併用しています。
  • JavaのみのプロジェクトのJar作成(2.9.1を付けないJar名)
  • FTPによるmavenリポジトリへのアップロード(sftpならできるのですが、昔ながらのftpはダメみたい)

conscript

SmartDoxのインストールには conscript を使いました。
conscriptはGitHubに格納したアプリケーションの構成情報を使用して、 アプリケーションの依存関係を解決した上でローカル環境にインストールしてくれる インストーラです。
プログラムの提供側、利用者側のどちらもかなり便利なので、これから広く使われるように なるのではないかと思います。

GitHub

ソースコードの管理はGitHubを使っています。 GitHubはUIが使いやすいので、 このところ新規プログラムはGitHub上で開発していますが、 SmartDoxの場合は、conscriptを使うというためという理由もあります。
GitHubは、ソースコードのバージョン管理という枠組みを超えて、 conscriptのような形でソフトウェアリポジトリ的な使われ方もしてきています。 こういう新しい応用が出てくるので、使っていて刺激的ですね。

関数型プログラミング

関数型プログラミングは、ボクがLispをかじっていた頃(25年ぐらい前)はラムダ計算をベースに、 List、クロージャといった部品を使ってプログラミングしていくものでしたが (さらにいうと綺麗につくると性能がでないので、手続き型的なプログラミングにしたり、 nreverseといった破壊的な関数を使うのがバッドノウハウ)、現在は状況が一変しています。
まず ハードウェア性能の向上で、関数型言語の宿命だった動作性能は事実上あまり問題にならなくなっています。 Javaを使って大丈夫な用途であればScalaでも基本的には大丈夫と考えてよいでしょう。 RubyやPythonで大丈夫な用途であれば、Scalaだと逆に高速に動作しそうです。
技術的には、 モナド、型クラスという新しい技術が登場し、これが新しい関数型プログラミングの基盤になって います。
ボクも2008年にScalaを始める前は、 昔の関数型プログラミングのイメージで関数型言語を捉えていたのですが (List処理が便利で開発効率がアップとか)、 現在では全く別物と考えています。 モナド、型クラスはそれだけのインパクトのある技術ということが分かりました。 いつの間にか、こんなことになっていたのか、という感じです。
さらに、Scalaでは関数型プログラミング(FP)とオブジェクト指向プログラミング(OOP)の融合した Object-Functional Programming(OFP)という概念が提唱しています。
歴史的な蓄積やOOADからの連続性を考えると、 OOPは今後も軸の一つで在り続けることになると予想されます。 ここにFPの記述力をどのように融合させていくのかということが、 実務の世界では非常に重要になるのは明らかで、 OFPはこれは今後大発展する分野と考えています。
このあたりを意識しつつ SmartDoxの開発では、今風のFP的な技術をできるだけ使うことにしました。

代数的データ型

まずデータ構造として、代数的データ型(Algebraic data type)を使用します。 Scalaでは、代数的データ型は直接サポートされていませんが、 case classがこの目的での利用を想定して提供されています。 たとえば A Scala Tutorial for Java programmers にも以下の記述があります。 このあたりの背景は、 Object-Oriented Pattern Matching に詳しいようです。
In Java, such a tree would be represented using an abstract super-class for the trees,
and one concrete sub-class per node or leaf. In a functional programming language,
one would use an algebraic data-type for the same purpose. Scala provides the concept of case classes which is somewhat in between the two
今までは、内部データ構造はオブジェクト指向的な作り方にしていたわけですが、 SmartDoxではcase classを使ってimmutableな構造をにしてみました。
オブジェクト指向的な作りの場合、可変オブジェクトを複数のコンポーネントで共有して 少しづつ変更を加えていくプログラミングモデルになりますが、 代数的データ型(case class&immutable)にすると、コンポーネントで全複写しながら変換する プログラミングモデルになります。 immutableにすると、なにか不測の事態があったときの回避方法が限られるためOOP派的には 非常に怖い選択ですが、 FPでは避けて通れない道です。
SmartDoxでは、このプログラミングモデルにチャレンジしてみたわけです。 今の所、特に問題もなく、このプログラミングモデルでもやっていけそうな感触を得ています。
また、代数的データ構造を取っても、いざという時には型クラスの技法を使って 色々細工ができそうという読みもありました。 なにか問題が出てきたら試してみようと思っています。

Scalaz

Scalaz は、型クラスと純粋関数型データ構造を提供するScalaライブラリです。 Scalaの暗黙型変換、暗黙パラメータの機能を利用してHaskell的な型クラスを提供しています。
Scalaプログラミングを足掛け4年程やってきて分かってきたのは、 FPは関数の合成でプログラミングしていくのがコツということです。 Scalaでも提供されているMonadという仕組みは、この関数合成のメカニズムの一種で 普通の関数合成(composeやandThen, orElseなど)よりもより強力な関数合成を可能に します。 このMonadを活用したプログラミングスタイルをMonadicプログラミングと呼ぶようです。
Scalaの基本機能にもMonadはあるので、ScalaだけでもMonadicプログラミングは できるのですが、Scalazの強力な型クラス群を使用すると、さらに強力な Monadicプログラミングを行うことができるようになります。
Scalazが用意している型クラスはたとえば、Monoid, Functor, Applicative Functor, Monad といったもので、MonadをKleisli圏で合成するようなこともできるようになっています。 このあたりの型クラスを手足のように使えるようになるのが現在目標としていることで、 そのためにSmartDoxではできるだけScalazを使ってプログラミングする方針にしています。
Scalazを使うもう一つの効用は、Scalaで型クラスを使うためのよいお手本になるということ。 Scalaでは、型クラスの機能は直接サポートされておらず、暗黙パラメタ、暗黙変換の機能を駆使 して実装する必要があります。この実装技術をScalazで学ぶことができます。
型クラスは、フレームワークと実装クラスを疎結合に保ちつつ(静的な)多態性を可能にする 非常に強力な言語機能と理解しています。 Scalazの技法を使えば、Scalaでも型クラスを使った プログラミングが可能になるわけで、いずれ自分のプログラミング技法に 取り入れたいと考えています。

パーサーコンビネータ

SmartDoxでは、 SmartDox文書のパースにパーサーコンビネータを使ってみましたが、 驚くほど簡単にパーサーを書くことができることが分かりました。
パーサーの記述はBNF的なDSLになっていますが、基本的な動きとしては MonadicでかつApplicativeという感じです。 Parser[T]を返す関数がMonadic的な動き、その中で「^^」メソッドに渡すクロージャが Applicative的な動きと思います。
簡単なパーサーであれば、BNF的DSLという理解の範囲で使えますが、 細かいことをしようとするとMonadicかつApplicativeな動きを理解して いないと辛そうです。 パーサーコンビネータをある程度使いこなせるようになったのも、 Scalazを使ってMonadicプログラミングに慣れてきた効用かなと 思います。
パーサーコンビネータを使っていて分かったのは、 パーサーコンビネータは文字列以外にも使えるということ。 DOMなどのXML木やマイ代数的データ型からパターンマッチングで構造を抽出して ドメインモデルを構築するという用途には簡単に適用できそうです。 翻って考えてみればこのあたりがMonadicプログラミングの威力ですね。


以上、SmartDoxを素材にScala+Scalazを中心としたFP技術を概観してきました。 15年前はC→Javaによって、OOPがメインストリームのプログラミングパラダイムになり プログラミングの生産性が大きく向上しました。 それと同等の大きなムーブメントがFPの本格導入により起こりそうです。 もちろん前述したように、OOPは今後も軸の一つとなり続けるでしょうから、 OOPとFPを融合した新しいプログラミングパラダイムという形になるでしょう。
最後にどのプログラミング言語が残るのかは分かりませんが、現時点では Scala+Scalazで技を磨いておくのが有力な選択肢だと思います。