要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』で使用するスライドについて背景説明を行っています。
今回は背景説明第6弾として、「オブジェクトと関数の連携(2)」として用意した以下の図を説明します。
オブジェクトと関数の連携(1)では、オブジェクトの世界と関数の世界を完全に分けてしまうのがよい、と説明しました。今回は、その具体的な連携方法について考えます。
Scalaでは関数もオブジェクトの一種なので、そういう意味ではすべてオブジェクトの世界で動いています。ここでは、純粋関数型言語の枠組みのみを使用しているものを関数型の世界、それ以外のものをオブジェクトの世界と呼ぶことにします。
図では、オブジェクトの世界を左側に関数の世界を右側に配置しました。
更新処理の流れ
オブジェクトの世界では、オブジェクトがグラフ構造で格納されています。このグラフ構造の一部を、関数を使って更新する流れについて見ていきましょう。
純粋関数型では副作用がありません。つまり、オブジェクトの内容や構造を更新することはできません。関数ができることは、計算をすることのみです。
とはいえ、純粋関数型言語でもインタラクションゲーム やデータベースアクセスを書くことができます。
副作用はないにもかかわらず、プログラムの外の世界の副作用は記述できるわけですね。この矛盾を解決するためのちょっとしたトリックがあります。このトリックがオブジェクトと関数を連携させる場合でも鍵となります。
基本データ構造
一般的な処理で関数が扱う構造は大きく以下の4種類です。
- 値(構造は持たない)
- リスト
- 表
- 木
この中で最も複雑な構造が木構造です。木構造までは再帰によって関数で自然に記述できます。これより複雑な構造はグラフ構造になりますが、これは特別な扱いを必要とするので、一般論を考える時は除外して考えるとよいと思います。(グラフ構造を扱うときは、木構造に参照を追加したり、表(行列)の形にしたり(ノードの集合として扱う)という操作の仕方になります。)
関数で自然に扱える最も複雑な構造が木構造で、表、リスト、(構造を持たない)値は木構造を単純化した構造と考えることができます。以上の点から、以下では木構造をベースに考えていきます。木構造でできることは表、リスト、(構造を持たない)値でも同様に適用できます。
データ構造の抽出
可変オブジェクトのグラフ構造から、処理に必要な情報を木構造として抽出します。この情報を、代数的データ型と永続データ構造を用いて表現します。どちらも不変オブジェクトで、変更を行うことはできません。
図ではグラフ構造としての情報は、木構造に参照を追加する形で表現しています。
データ構造のコンバージョン
関数での計算の基本は木構造のコンバージョンです。データ構造を更新することはできないので、複製しながら複製過程でその一部を変えることで、木構造のコンバージョンを行います。
更新指示書
関数では計算ができるだけなので、直接オブジェクト側のデータ構造の更新はできません。
単純なケースでは、関数で計算したデータ構造を直接オブジェクト側のデータ構造に上書きしてしまうことで、更新処理を完結させることができます。
問題は、そのようなことはできない複雑なデータ構造の場合ですね。
その場合、関数は木構造から「更新指示書」を計算します。ここが「トリック」です。
関数はあくまでも更新指示書の計算を行うのみで、更新処理そのものにはタッチしません。副作用という下世話な処理は、関数型言語の外側の誰かが更新指示書を見ながらやってくれる、という役割分担になっています。この役割分担によって、純粋関数型の純粋性が保たれているわけです。
計算結果の反映
オブジェクト側では、関数から返ってくる更新指示書に従って、可変オブジェクトのグラフ構造を更新します。
副作用はオブジェクト側で引き受けることで、関数の世界の純粋性を守りながら、オブジェクトと関数の連携を行うことができます。
ノート
更新指示書を使った更新処理はかなりややこしいです。
オブジェクト指向から関数型に入ってくるときに戸惑うのはこの点で、この(擬似)更新処理を円滑に行うためのテクニックが数多く存在します。モナドもその一つですね。先ほど紹介したゲームは名前がMonadiusですが、これはモナドを使っているのが名前の由来とのことです。
こういった、ややこしいメカニズムで関数型の純粋性を保つと何がよいのかというと、数学の理論の範囲内で処理を完結させる事ができるということです。これは、理論的にはプログラムが証明可能ということです。
もちろん現時点では、普通の関数型言語ではプログラムを証明することはできないのですが、関数型の枠組みでプログラミングすることによって間接的にバグの混入を大幅に防ぐことができます。
たとえば、並行処理を行う小さなモナドをたくさん用意して、このモナドを合成して一つの大きなサービスを記述するとします。各モナドは代数的構造デザインパターンで取り上げた代数的構造を持つ代数データ型を操作するとします。
このようなモナドの合成では、代数的に証明されたメカニズムでモナドを合成することができ、さらに代数データ型の枠組みの中で処理の最適化を行うことも可能になるはずです。
「小さなモナド」側で十分に検証してバグを取っておけば、これらのモナドを合成して作成したサービスは、問題なく動作することが期待できます。(モナドの合成メカニズムそのものは数学的に証明されたメカニズムが使用されるはずなので。)
並行プログラミングでは、このようなプログラムの作り方が非常に有益です。並行プログラミングでは、単純なケースを除いてはデバッガによるデバッグはほぼ不可能と考えてよいので、プログラミングの時点でバグが混入しづらいメカニズムの存在が大きな鍵になります。
このあたりの考えを進めていくと証明駆動や形式手法といった方面に進んでいくことになりますが、その土台は関数型プログラミングなので、まずは関数型プログラミングに習熟することが大事です。
SQL
「更新指示書」によるメカニズムはSQLを用いたデータベースプログラミングと同じことをしていると気づくと、処理の内容が腑に落ちると思います。(SQLでは木構造ではなく表構造になります。)
「更新指示書」としてSQL文を作る関数を書けば、オブジェクトの世界を飛ばして、状態はすべてデータベースに格納管理するというアプローチも可能です。こうすることで、データベースをバックエンドに使う一般的なWebアプリケーションを純粋関数型言語で記述できますね。
トランザクション
「更新指示書」によるメカニズムはトランザクション処理とも相性がよいです。
トランザクション処理では、処理がアボートした時には、データが処理前の状態になっていなければなりません。このため、データにロックをかけて随時情報を更新していくというアプローチでは、アボートした時にデータを戻す処理を行わなければなりません。
このような手間が必要なので、「更新指示書」方式のアプローチでも処理の複雑さは変わりません。関数型では言語の特性上「更新指示書」アプローチになるので、この上でトランザクション処理を考えていけばよいわけです。また、(ロックをかけない)楽観的アプローチは、「更新指示書」アプローチと相性がよいので、そういう意味でも「更新指示書」アプローチは有効です。
つまるところ、関数型言語はトランザクション処理と相性がよいということですね。
0 件のコメント:
コメントを投稿