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を見つけることができれば、それを調べて紹介したいと思う。
見つからなければ、リレーショナルモデルについての考察に移りたいと思う。

3 件のコメント:

  1. もしかして: Datomic?

    返信削除
  2. 先行評価と遅延評価(LazyEvaluation)の話じゃないのかな?と思って読みました。
    関数プログラミングのパラダイムでは先行評価による障害にぶち当たるときが必ず来ますね。
    IOを扱わない範囲、例えば数学の問題でも、メモリ、計算速度のボトルネックもあり、
    宣言的に書くには、遅延評価でなければいけないという例として、
    自然数などの無限数列の表現は遅延評価モデルでないと無理ですね。
    あと「たらい回し関数」やフィボナッチ数列なども遅延評価モデルでは簡潔なアルゴリズムかつ
    パフォーマンスが高い実装が可能です。
    Haskellが遅延評価の純粋関数プログラミング言語として昨今著名ですが、
    JavaScriptでもかなりの範囲で実現できるlazy.js
    http://danieltao.com/lazy.js/
    という秀逸な遅延評価関数ライブラリがあります。
    上記サイトの#Introductionにおそらく今回問題として提示されたことに対する解法としての遅延評価ライブラリであると記述されていると思います。
    http://adamnengland.wordpress.com/2013/10/10/benchmarks-underscore-js-vs-lodash-js-vs-lazy-js/
    このブログの最後のベンチマークはExcellのDBをlazy.js含む複数ライブラリで回したベンチマーク比較ですが、作者自身から甘すぎると物言いがコメント欄でついているものの、圧倒的なパフォーマンスを示しています。

    返信削除