たなかこういちの開発ノート

システム開発に携わる筆者が、あれこれアウトプットするブログ

OOは静的構造を、関数型は動的振る舞いをモデル化するのに有用だという話

要約
 
関心対象について分析し理解しようとしたら、多かれ少なかれ「要素分解していく方法」を取るでしょう。「要素分解していく方法」とは構造を捉えることに関してのアプローチです。
 
構造を捉えるに当たって、OOは「要素分解していく方法」をよく支援します。構成要素はオブジェクト、構成要素間の関係はオブジェクトの関連、階層理解はencapsulationとして表すことができます。しかし、OOは動的振る舞いについては、implementationに“押し付けた”のみで、なんらの解法を提供していませんでした。
 
ところで、動的振る舞い=「手続き」と漫然と捉えていましたが、「手続き」とは万能チューリングマシンに由来する処理のモデル化でした。「手続き」以外に、チューリング完全である処理のモデルが存在しました。それがλ計算であり、関数型言語の「関数」です。
 
処理を「手続き」=状態の時系列的変遷と捉えている限り、モデルを(その本質が線形であっても)非線形だとして扱うしかなく、可能なことはせいぜい管理することだけです。動的解析(≒シミュレーション)が可能かもしれませんが、結局のところブラックボックス・ユニットテストを精緻に実施する以上のことは、現時点では一般には困難でしょう。(※管理やシミュレーションにおいて、OO言語は力を発揮するでしょう。)
 
処理を「関数」=変換過程と捉えることで、動的振る舞いをも「単位処理の依存関係」という構造同様の形態で表すことができます。依存関係構造を読むときは、(ある種の再帰がない限り、)「明らかに記述されていること」以上の振る舞いはありません。“脳内”スタックやキューを駆使せずとも読解することができます。関数型は、動的振る舞いを静的に構造的に扱うことに有用だと云えるでしょう。
 
関数型の具体的なプラクティスがどの様な様子になるかは、SQL SELECT、XQuery、LINQ、その他(RxJavaなどの)Streams系APIの仕立てを参照することで習得できるでしょう。
 
 
分析およびその解釈記述に関する一般的手法
 
ある関心対象について分析し、その有り様を理解しようとしたら、多かれ少なかれ「構成要素とその関係(、およびそれらの諸元詳細)」というかたちでの形式化を試みるでしょう。このアプローチを「構造から入る分析」と称しておきます。
 
対して、対象をブラックボックスな反応器と見て、入出力パターンを形式化するという「振る舞いから入る分析」もあるでしょう。振る舞いから入る分析も、対象領域があまりに大きいと、一括で扱うのは困難かもしれません。やはりまずは構成要素に分解して、各構成要素毎に振る舞いをみて、それらの合成である、として理解することを試みた方がよいように思えます。
 
加えて、対象を「構成要素とその関係」として理解するとき、各構成要素がさらに下位の小粒度の要素から構成される、もしくは、複数の構成要素がさらに上位の大粒度の要素を構成するという、階層的理解をすることがさらに可能です。
 
以上のような分析アプローチを、本記事では以降「要素分解していく方法」と称します。
 
 
ところで、「モデル」とは、私は「分析結果に基づいて、対象を如何に理解したかを表現したもの」だと捉えます。ここで「分析からモデルを構築する」という過程に関して、ポイントが二つあります。
 
一つ目は、モデルとは、分析結果を説明出来る仮説あるいは一つの解釈だということ。
二つ目は、仮説あるいは一つの解釈が描けて初めて対象が理解できたと云えるだろう、ということ。
 
 
本節の主張は下記のようにまとめられます。
 
関心対象を分析するとき、一般に「要素分解していく方法」を取るであろう。分析に基づいて構築した仮説を「モデル」と称するなら、「要素分解していく方法」によって描かれるモデルとは、構成要素とその関係、構成要素の階層構造、それらの諸元詳細、として表されるものとなる。
 
オブジェクト指向は、分析に基づくモデル(=解釈)記述の一般的手法を一般に支援する
 
関心対象を理解するために、前節に示したような「要素分解していく方法」を取り、それに基づくモデルを「構成要素とその関係、構成要素の階層構造、それらの諸元詳細」として描こうとするならば、オブジェクト指向は、そのようなアプローチに基づくモデル記述をするための道具として極めて有用だと云えます。各構成要素とその関係はオブジェクトとその関連として、階層的理解はencapsulationで表現できます。まさにその為に誂えたかのようにフィットする道具仕立てに思えます。Wikipedia日本語版をみると、プログラミング言語としてのオブジェクト指向は、構造化プログラミングの進化形としてC++において一旦の完成をみましたが、その出自はシミュレーション記述用として考案されたSimulaにあるとのことです。オブジェクト指向がもとよりシミュレーション記述用として考案された技法だというのであれば、分析結果を説明するためのモデル記述に有用だったのもある程度当然なのでしょう。
 
 
補足しておくと、ここで主張したい事は「オブジェクト指向はあらゆるモデリング技法の中で最強である」といったような事ではありません。主張したい事は「分析し、対象理解に関する仮説を描こうというとき一般に行うであろう作業において、オブジェクト指向は一般に有効に使える」という事です。重要な点は、分析結果を統合的に説明する解釈記述において有用、という点です。
 
オブジェクト指向の実装局面における限界
 
・・・とはいえ、およそシステムをデザインするとき、全てOOでやっていく事が一般化しました。
 
個人的な話になりますが、主に時代背景的に、私がプログラマーとしてそこそこのレベルに到達することと、OOPを習得することとは、実質的に同一の意味でした。OOが分析に基づくモデル記述に一般に有用、という認識は、(※前節でOO最強と言いたいわけでは無いと断りを入れているにもかかわらず、)私のようなOOネイティブ話者に、「世界は全てOOで表現できる」と思わせるのに十分でした。実際のところ、業界的にも、OOはモデル駆動開発実現に力を発揮すると期待されました。
 
しかし、そんなOOネイティブも、その内、システムの開発は、OOだけで全てを進めることは出来ないことに気付いてきました。
 
 
繰り返しになりますが、関心対象領域における構成要素の存在はオブジェクト、要素間の関係はオブジェクトの関連で表現できます。encapsulationは、オブジェクトが、外部に対して自分がどのような仕様を持つと表明するかということ(interface)と、内部的に如何にそれを実現しているか(implementation)ということを、分離して捉えていくことを可能とします。encapsulationは、構成要素がさらに小粒度の要素から成る、あるいは複数の構成要素がさらに上位の大粒度の要素を構成する、といった階層的理解を記述するのに適用できます。各階層における各要素の諸元詳細=仕様を"interface"として記述し、その階層のその要素が、より下位の層の要素によって如何に構成されるのかについては"implementation"として捉えることができます。encapsulationを繰り返すことで、理論的には無制限に粒度を上げていく、もしくは下げていくことができます。
 
つまり、モデリング対象がどれほど複雑な階層的構成要素から成ると解釈しても、OOで完全に記述できそうに思えました。事実、ビジネスプロセスやビジネスエンティティといった大粒度から、アプリケーションフレームワークといった中粒度、個々のデータ操作ユーティリティといった小粒度まで、各階層毎の仕様記述はクラス図やシーケンス図などによってよく実践することができました。
 
 
ただ、我々のターゲットは動くシステムです。上手に何か特定の解釈を描いただけでは足りなかったのです。つまり実装する/できることが重要です。
 
encapsulationは、interfaceとimplementationの分離を実現し、階層化理解を促しました。各階層毎に、各構成要素を、正にそれが何であるのかを表現するようなinterfaceを定義することができました。encapsulationはオブジェクトの(※つまり、対象領域の構成要素の)外部インターフェイスの粒度を適切に整えつつモデル化することについて解法を提供しました。ところが、そのようなオブジェクトの内部を実装しようというとき、結局のところ構造化プログラミング以上の工夫は無く、「手続き」として記述していくしかありませんでした。OOは、各階層を結び付ける為のimplementationを如何に高度化できるかに関して、何も提供していないことに気付きました。interfaceとimplementationを分離したというのは、動くシステムのための細々した問題を、云うなればimplementationに押し付けただけだったのです。
 
OOは「要素分解していく方法」を極めてよくサポートしました。対象理解を進めるだけであれば、これで十分でした。しかし、我々のターゲットは、そのように理解された対象世界(の少なくとも一部)を代替して動くシステムを、現実的に実装する事です。コンサルティングやプランニングまでなら、OOの表現力で進められたのですが、コンストラクションとなったとき、OOには構造化技法以上の力はなかったのです。
 
 
この事態を次のように捉え直します。
 
オブジェクトとその関連で表現できるものは、つまり、システムの静的構造です。OOは、システムの静的構造が、如何に下位階層の要素から構成されるのかを記述するのに極めて有用だった、ということです。システムの静的構造は表現しさえすれば、直ちにOO的に実装することができました。
 
対して、動的振る舞いはシーケンス図などで一定程度表現しても、OO的にどのように実装すればよいか直ちに導けるものではありません。結局、システムの動的振る舞いは、オブジェクトの中身、つまり実質メソッドの中身の実装に依るのです。OOは、動的振る舞いが如何に下位の階層に基づいて現れ出ているのかをモデリングすることに対しては、ほぼ何も寄与しなかったと云えます。(※そもそも本記事冒頭の方でOOが有用だとした分析における「要素分解していく方法」も、構造を捉えることに関しての方法であり、振る舞いを捉えることに関しては棚上げした状態でした。)
 
私見としては、UMLベースのMDDが様々な苦難に直面したのも、OOは本質的に動的振る舞いを記述しきれないものなのだ、という点にあると考えています。
 
動的振る舞いを捉える
 
OOは、implementationに関して構造化技法以上の高度化の道具は提供しておらず、動的振る舞いは「手続き」として記述するしかありませんでした。
 
 
ところで、動的振る舞い=「手続き」と漫然と云っていますが、「手続き」とはなんでしょうか?
 
振り返ると「手続き」とは、コンピュータ・サイエンスにおいて、万能チューリングマシンと称されるオートマトンの一種を起源としています。オートマトンとは、(荒い理解において、)何らかの状態遷移マシンを形式化したものです。オートマトンは、その状態遷移マシンが出来ることの能力の違いで幾つかに分類されます。その中で、(非常に荒い理解において)“何でも出来る”タイプを、万能チューリングマシンと称します。手続き型の言語は、この万能チューリングマシンの応用として設計されています。 万能チューリングマシンの“何でも出来る”という特性を「チューリング完全」と称します。Java、C、PHPなど、全てチューリング完全です。
 
もう一つチューリング完全性を発揮するコンピュータ・サイエンス上の計算モデルがあります。それが「λ計算」です。関数型の言語の起源です。
 
チューリングマシンは状態遷移で処理を表現します。対するλ計算では、文字通り関数=変換過程の組み合わせで処理を表現します。更新の様子を管理すべき状態はありません。全てが関数=変換過程で表現できるということは、処理を“線形”なモデルで表現でき(得)るということを意味します。
 
y = f( x )
z = g( y )
 
fgを組み合わせ(compose)て、
 
z = g( f( x ) )
 
例えば下記のように表現されるところのチューリングマシン=状態遷移のモデルは、一般に“非線形”だと解するしかありません。
 
xt = f( xt-1, at )
 
チューリングマシンもλ計算も「チューリング完全」という観点で計算能力は等価です。また、様々なチューリング完全な計算の中には、“線形”なものも“非線形”なものもあるでしょう。当然ながら、チューリングマシンによる状態遷移のモデルで描こうが、λ計算による変換過程のモデルで描こうが、本質的に“線形”なのであれば“線形”だし、本質的に“非線形”なら“非線形”なはずです。(※なお、ここでの“線形”、“非線形”の語はあくまでイメージの取りまとめに使っているので用語としての厳密性は問わないでください。)ただし、状態遷移のモデルは基本的に“非線形”な表現なので、そのままでは「実は“線形”である」という理解をするのが困難です。一方のλ計算の変換過程のモデルは、「少なくとも、再帰が無いか、有っても末尾再帰に限られるなら、そのモデルは本質的に“線形”」というように、“線形”、“非線形”の見極めが容易となっているはずなのです。「チューリングマシンによる状態遷移のモデルか、λ計算による変換過程のモデルか」という話は、本質的に“非線形”か“線形”かということではなく、「“非線形”に表現されるので“線形”として扱えない、“線形”に表現できるので“線形”として扱える」という話です。
 
 
チューリングマシンによる状態遷移のモデル表現
λ計算による変換過程のモデル表現
モデルの本質が線形の場合
非線形表現なので、見極めができない
線形性をよく表現でき、線形を前提とした扱いができる
モデルの本質が非線形の場合
非線形表現で特に問題ない
線形でないことを見極められる
 
 
そもそも、線形なモデルは何がよいのでしょうか?非線形な状態遷移のモデルでは、「時系列的変遷」をシミュレーション(≒動的解析)で見ていくことしかできません。振る舞いは振る舞いのままに捉えるしかありません。対して、線形な変換過程のモデルでは、変換過程の各ステップを、「処理の依存関係」という、グラフもしくはツリー状の構造として捉えることができるようになります。例えば、
 
b = f( a )
c = g( a )
d = h( b, c )
 
という処理ステップがあったなら、例えば下図のような依存関係構造にあると表現できます。
 

f:id:tanakakoichi9230:20160327213106p:image

 
このような依存関係構造に描けることのメリットは、もっぱらその見通しの良さにあります。(ある種の再帰がない限り、)明らかに記述されていること以上の振る舞いはありません。その依存関係構造図を読むとき、“脳内”スタックやキューに何かを積んだりしなくても読み取ることができます。状態遷移のモデルでは、例えば状態遷移表にまとめたとしても、その振る舞いを追うには“脳内”動的解析をする必要があります。脳内スタックかキューに何か積んでいかないと読解できません。
 
 
以下に、手続き型=状態遷移のモデルと、関数型=変換過程のモデルとの対比をまとめます。
 
手続き型
関数型
チューリングマシン
λ計算
状態遷移
変換過程
時系列的変遷
依存関係(※構造同様)
非線形な表現
線形な表現が可能
動的解析しかない
静的解析が可能
 
関数型=変換過程による形式化の具体的な有り様
 
振る舞いを関数型=変換過程として捉えることが有用だとして、しかし、私にはコンピューター・サイエンスや各種数学の体系を直接参照して具体的なプログラミングパターンを構築するような力はありません。具体的なプラクティスが無いものでしょうか。幸い、既に先達たちの成果が沢山あります。(※本記事では(力尽きたので)以下列挙するにとどめます。)
 
- SQL SELECT
- XQuery
- LINQ
- RxJavaなどStreams系ライブラリーのAPI
 
 
参考記事
 
 
 
<追記(3/31)>
本記事の補足となる記事を投稿しました。動的振る舞いは、単に関数型、というより「データフロー・プログラミング」で理解するのが有用ではないか、という論です。
 
◆以上