2013年9月20日金曜日

補足と訂正・クエリーがどのようにしてモジュール性を阻害し得るか

前回はRDBMSの使用が引き起こすモジュール性の低下について議論したが、その根拠を具体的に示さなかった。その補足ということで今回は小さな例を1つ示したい。

また、関数プログラミングが副作用の多い状況に対して弱みを持つ、と書いたのは乱暴であったのでお詫びして訂正したい。
正しくは、副作用を含むロジックに対しては、注意深く抽象化を行う必要があり、副作用・外部入出力の種類が違えば、そのたびごとに抽象化のモデル・実装について熟慮して抽象化を行う必要があるということである。
これはうまい抽象化が不可能だということではないので、それを弱みと呼んだのは間違った一般化であった。

しかし、ありがちな抽象化の方法では、クエリーの発行がモジュール性の低下を引き起こしかねないということを小さな例を使って示したいと思う。この例は手続き的な擬似コードによるものだが、RDBMSを抽象化するfunctionalなDSLであっても同じような問題が起こる「可能性」があることには同意してもらえると思う。一方で同じ問題を起こさないDSLが存在する可能性も十分あるが、それについては後に考察する。

おそらくありそうな例

以下は、Python風の手続き的な擬似コードだ。

def procA(x):
    rows = queryA(x)
    return calcA(rows)

def procB(x):
    rows = queryB(x)
    return calcB(rows)

queryAとqueryBはSQLクエリーを発行して結果を返す手続きだ。
そしてcalcAとcalcBは純粋に汎用言語で書かれた副作用の無い手続きだ。
procAは、queryAを呼び出し、更にcalcAによる計算をして結果を返す。
procBは、queryBを呼び出し、更にcalcBによる計算をして結果を返す。

ここでprocAとprocBの手続きを続けて行うprocAandBという手続きを作りたいとする。
(関数の直列合成より現実によくありそうな形を選んだが深い意味は無い。)
その定義は以下のように書けるはずだ。

def procAandB(x):
    y = procA(x)
    z = procB(x)
    return calcC(y,z)

ここでパフォーマンスの問題が起こったとする。
queryAとqueryBを両方共呼び出すのであれば、queryAandBという形にひとつのクエリーにまとめることができ効率が良いと判明する。
そこで次のように合成されたクエリーqueryAandBを使って手続きを書き換えることになる。

def procAandB2(x):
    rows = queryAandB(x)
    y = calcA(rows)
    z = calcB(rows)
    return calcC(y,z)

プログラムは大して汚くなったわけではないが、なにか余計な面倒をしょいこんだぞ、とあなたのゴーストが囁くはずだ。

何が起こったのか

クエリーを含む手続きを複数合成するときに、パフォーマンスの観点から手続きの中で使用しているクエリーを合成し書き直す必要がでる。このようなパターンはありふれたものだと思われる。

そしてこういった書き換えが頻繁に発生すれば、モジュール性の低下、コードの冗長化を引き起こすということは明らかだろう。
そしてこれはパフォーマンスを無視出来る状況であれば問題になりえないのだが、無視できない状況が十分たくさんあるのが現状だ。

パフォーマンスのための最適化は仕方ないか?

パフォーマンスのためのチューニングでコードが汚くなるのは珍しいことではなく、仕方の無いものかもしれない。しかし、リレーショナルモデルは、本来インデックスの使い方や管理といったパフォーマンス指向の関心を、ビジネスロジックから分離するというのが売りであったはずだ。これでは本末転倒だ。ここは開発現場ではないので、もうすこし理想を追いかけてみたい。

本当のところどうあって欲しかったか

procAとprocBには必要なクエリーと必要な計算がすべて記述してあるので、procAandBを両者の合成として定義するだけで、あとは実際にどんなクエリーを発行するかはコンパイラが最適化してくれればよい。
つまりprocAandBのような記述をして、実際の実行はprocAandB2のように行って欲しいのだ。
そうすれば余計な荷物を背負い込む必要はなかったはずだ。

ところが、SQLを知らない汎用言語のコンパイラにそれを期待することは出来ないので、ライブラリとしてのDSLのレベルでSQLの合成とコンパイルを行ってくれれば良い。
ある計算をアプリケーション側とRDBMS側のどちらで実行するかも抽象化できれば理想的なので、クエリーとそれ以外のロジックをまとめてDSLで記述して適切にSQLと汎用言語にコンパイルもしくは逐次実行してくれると良いかもしれない。

こういったことは、手続き的な言語上のO/Rマッパではおそらく相当に難しいはずだ。
functionalなDSLでは、当然どんなモデルに基づくDSLなのかによる。

どんなモデルに基づくDSLであれば良いか

此処から先は多分に推測を含む。
そのようなDSLは、SQLと、汎用言語の側で計算してほしい部分のロジックの療法を理解する必要がある。
そして汎用言語側で行う処理は、リレーショナルモデルの範囲を超えたものであることが多い。
そのようなロジックとSQLの混合物を上手く記述するためには、DSLのモデルは関係モデルそのものではなく、関係モデルを包含するものか、全く別のモデルである必要があるだろうと思われる。
それは一体どんなモデルであろうか。
そういったモデルやDSLがすでに存在している可能性は高いが今のところ見つけられていない。

しかしそのDSLライブラリが既存であろうがなかろうが重要なのはそのモデルがどんなものかということだ。
そのモデルは少なくともリレーショナルモデルを包含するか、もしくはその良いところを同じように持ち合わせている必要がある。
さらにリレーショナルモデルの欠点(もしあるならば)を克服していればさらに良いだろう。

次回

そのモデルなり具体的なDSLを見つけることができれば、それを調べて紹介したいと思う。
見つからなければ、リレーショナルモデルについての考察に移りたいと思う。

2013年9月18日水曜日

関数プログラミングのボトルネックとしてのRDBMS

プログラム開発は、多くの人々が目的達成のため、もがき苦闘するタールの沼である

– Frederic P. Brroks, Jr., 人月の神話

モジュール性はプログラミング成功の鍵である

– John Hughes, 関数プログラミングはなぜ重要か


タールの沼の底から

 タールの沼と聞いて連想するのは大規模なSIである。業務アプリケーションやWebアプリケーションは規模が大きくなればなるほど、複雑さが増し収拾がつかなくっていく。そしてそのようなアプリケーションを大きな単位で上手くモジュール化し、さらには再利用することは不可能に近い。
その技術的な原因の一端、そして問題を解く鍵は、そのようなアプリケーションが常に携えているRDBMSの周辺にあり、さらに言えばおそらくRDBMSとアプリケーションロジックの組み合わせにあると考えている。
ここでは、アプリケーションロジックの実装に関数プログラミングを適用してもこの問題が容易には解決しないことを示し、この問題の根の深さと重要性を主張したい。

関数プログラミングの強み

関数プログラミングは、非常に強力である。その所以は関数が提供する高いモジュール性にある。
関数の純粋性を損ない、モジュール性を阻害する副作用はモナドを使って記述し、安全に分離しておくことができる。
これが関数プログラミングの強みである。

関数プログラミングの弱み

では関数プログラミングの弱点はなにか。
それは副作用を含む処理が大半を占めるようなシステムのプログラミングに対しては、その強みが発揮しづらいことである。そのような副作用が支配的なプログラミングとして、UI、ネットワーク、データベースプログラミングなどがある。どれも重要だが今回のターゲットはデータベースプログラミングである。

UIプログラミング

UIは、そもそもインタラクティブな入出力を扱うものである。ウィジェットやフォームといった部品がそれぞれ状態を持ち、ユーザからの入力という副作用を元にそれらの状態・表示を更新していかなければいけない。このため多くのロジックは必然的に副作用を含むものとなる。
このUIプログラミングはリアクティブプログラミングの主な応用領域のひとつである。UIとリアクティブプログラミングの組み合わせ、UIとRDBMSの組み合わせに関しての詳しい議論は別の機会としたい。ここではリアクティブプログラミングにより一応の解決策が与えられているとしよう。ネットワークプログラミングについても、リアクティブプログラミングの応用例があり、UIプログラミングと同様にここでは議論しない。

データベースプログラミング

最後がデータベースプログラミングでありこれが今回のターゲットである。
RDBMSとのやりとりもまた副作用であり、UIがシステムの末端に位置するのに対しRDBMSは中心に位置することが多い。
このRDBMSに対する副作用が、いかにモジュール性を阻害するかについて考えてみよう。

ボトルネックとしてのRDBMS


RDBMSを中心とするシステム、特にWebアプリケーションや業務アプリケーションなど、
外部からの入力をもとにRDBMSを操作するシステムにおいては、
関数プログラミングの利点は大きく阻害されることになる。
その理由について、リッチクライアントに対してAPIを提供するWebアプリケーションサーバの実装を例に考えてみたい。

API

APIはクライアントからのリクエストを入力に取り、なんらかのレスポンスを出力する。
ここでレスポンスが、リクエストの内容だけから一意に決まるのであれば、レスポンスを生成するロジックは純粋な関数として記述でき全く問題ない。
しかし、そのようなAPIは明らかに非常に稀であり、多くのAPIがリクエストの内容に基づきRDBMSに対するクエリーを発行し、その結果をもってしてレスポンスを生成することとなる。
このようなAPIは副作用を含む手続きであり、純粋な関数としてみなすことはできない。
APIの内部のドメインロジックを分解していっても、重要なロジックほど内部にデータベースとのやりとりを含む。

データベースのモジュール化の難しさ

そこで状態の側、データベース自体を分割しモジュール化すればよいように思える。各テーブルやそれをまとめるスキーマといったものを利用できないだろうか。テーブルをグループ化することはもちろん出来る。しかしアプリケーションロジックをそのグループに合わせてモジュール化しようとしても、なかなか上手く行かない。なぜなら、個々ののAPIやビジネスロジックが、苦労して作ったテーブルのグループ分けをまたぐようにして状態にアクセスすることが頻繁に発生するからだ。
個々のロジックが複数のテーブルを結びつけていき、綺麗に分けることのできない塊にしてしまうのだ。

巨大な単一オブジェクト

その結果として、この種のプログラミングは、
RDBMSという複雑な状態を内部に抱え、メソッドとして多くのAPIを持つ、
巨大な単一オブジェクトの実装という様相を呈することになる。
俯瞰してみた場合のプログラムの大まかな構造は関数プログラミングの技法を使おうが使うまいが、変わらないということになってしまう。

オブジェクト指向とMVC

この問題は、オブジェクト指向やMVCといった技法に基づきデータベースを抽象化しても全くかわらない。
またオブジェクト指向に基づきオブジェクトに分解することはもちろん可能である。しかしカプセル化や疎結合を実現しようと努力しても状態を通じた暗黙的なオブジェクト同士の依存関係を断ち切ることはできない難しい。可変状態を抱えるオブジェクトを苦労して分解したところで、それをつなぐ手続きの記述が煩雑になるばかりで全体の複雑さはなかなか下がらない。それはSOAの苦戦が示すところである。
オブジェクト指向の効能はもちろんある。しかしここで重要なのは、俯瞰的に見れば 手続き+RDBMS という構図は変わらないということだ。そしてそもそも手続きではなぜ駄目なのか、という点についてきちんと議論するのは本当に難しい。それを示すためには、手続き以外のもの、関数プログラミングやリレーショナルモデルが何を提供しているかについて詳しく見ていく必要があるだろう。オブジェクト指向に関しては今回の主要なターゲットではないので、別の機会に詳しく議論したい。

ではリレーショナル・データベースが「悪い」のか

そもそもRDBMSではなく、ほかのデータモデルに基づくDBMSを使ったらどうだろうか。しかし、ドキュメント指向データベースやグラフデータベース、オブジェクトデータベースなどを用いても問題が解決することはないだろう。
もしくは、そもそもDBMSを使わないという選択肢はないだろうか。
DBMSを使うその理由は永続化やトランザクションのためだけだろうか。
もしメモリ空間が無限大で、プロセスが永遠に落ちないのであれば、
データベースなど必要ないのだろうか。

おそらくRDBMSを使わないという選択は、問題を悪化させる。それは、RDBMSの提供するトランザクションとリレーショナルモデルがすでにかなりの程度問題の度合いを軽減してくれているからだ。
この点については他の機会に詳しく議論したい。

気がつけばそこはタールの沼の底だった


各々のAPIは、DBMSを通じて暗黙的に相互に依存し合い、それにより個々のテーブルも結び付けられていき、さらに各「画面」もまた複数のAPIにまたがることになる。斯くしてAPIや画面といった単位より大きなモジュール性は完全に阻害されることになる。

ではどうすればよいのか

結局のところ問題の核心はどこなのだろうか。
RDBMSだけ、もしくはアプリケーションロジックだけであればそもそも問題は起こらなかったはずだ。その2つを組み合わせたときに初めて問題が起こるのである。
そしてこれは古の問題、パラダイムやモデルの異種接続が引き起こすインピーダンスミスマッチングよりも、おそらく根が深い。
RDBMS内の状態とアプリケーションロジックの結びつきがモジュール性を阻害することが問題の原因であるならば、それらの境界だけを見るのではなく、システム全体に視野を広げ、データベースモデルとプログラミングパラダイムの両面から抽象化のやり方について考え直す必要があるのではないだろうか。
さらにDBMSと汎用プログラミング言語という分業体制を懐疑的に眺めつつ、もし全体の抽象化についてやり直すとしたらどうすればよいか、ということもおそらく考えてみるべきだろう。

次回の予定

リアクティブプログラミングをもってしてもこの問題を解決するのは容易ではない。
それは第一にUIプログラミングと違い、状態がメモリ内部ではなく、DBMSという外部にあるからであり、第二に、データベースを宣言的な形で隠蔽・抽象化するということは、別のデータベースモデルを作ることに限りなく近いからだ。UIとリレーショナルモデルとリアクティブプログラミングについて考える前の準備として、次回はリレーショナルモデルの恩恵と問題点について考えてみたい。

ご意見募集

実務家、理論家の双方の方々からのご意見ツッコミを切に望みます。
コメント欄もしくはtwitterの@pokarimへどうぞ。

ログ

 2013/9/19:「オブジェクト指向とMVC」に修正・追記した。
 2013/9/19:「ではどうすればよいか」に追記した。

引っ越しました

ブログを引っ越し、統合しました。
今まで、具体的な言語やライブラリ関係の小ネタを本ブログ"Clojureで行こう"で、
プログラミングパラダイムとかデータベースモデルに関するも比較的抽象的な話を
"Conceptual Contexture"というブログの方でやっていました。

どちらもほとんど更新していませんでしたが、
今回2つのブログをひとつにまとめて再開することにしました。
Clojureを最近は触っていないこともあり、タイトルはConceptual~の方にしました。

ソフトウェア開発やプログラミングに関する小ネタと、
プログラミングやデータベースに関するモデルやパラダイムについての両方について
書いていきたいと思っています。