Think Stitch
PRINCIPIA  最近の更新


関数呼び出しによる相互作用について

相互作用と計算の分離のところで、関数呼び出しを相互作用の手段として使うと、ちょっと困ることがあるということを示唆しました。 それについて説明したいと思います。 困っているだけです。 解決編がないことはあらかじめ謝っておきます。ごめんなさい。 (円の描画のときみたいに、あとですぐにわかるってことはないですし。) もちろん CSP にしてしまえばいいんですけど、それはそれ、これはこれとして、C++ などを使わざるを得ないこともありますから。 もしかしたら同じことを思っている人がどこかにいないかなと思って。 実は過去に何度か身近な人に話したことがあるんですけど、あまり共感は得られなかったんです。 いえ、もちろん共感が欲しいわけではなくて、なんというか、すっきりする方針があればいいなと。

ここでは C++ や Java など、関数呼び出し(あるいは手続き呼び出し)をオブジェクト間相互作用の手段としているオブジェクト指向言語を想定しています。 コルーチンのことは考えていません。 コルーチンを相互作用の基礎とするオブジェクト指向言語があるのかどうかも知りません(あるんでしょうか?)。 あと、デザインパターンなどで出てくる、ベースクラス(スーパークラス)と導出クラス(サブクラス)にあるコード間での協調についても除外します。 あくまでもオブジェクト間の相互作用の話です。

クラス不変条件と状態の安定・不安定

一般に、オブジェクトはメンバ変数(インスタンス変数)を持っていて、メンバ関数(メソッド)の中で値が更新されます。 メンバ変数が複数あるとき、それらの値の間にある関係が成り立っている場合があります。 例えば、整数 c と整数の配列 v をメンバ変数として持っていて、c は v の中に含まれている 0 の数を表しているとします。 このような場合、v の要素を書き換えたとき、値が 0 から 0 以外へ、または 0 以外から 0 へ書き換えられたときに c も合わせて更新しなければなりません。 このようにメンバ変数の間に成り立つ条件をクラス不変条件といいますよね。

メンバ関数はメンバ変数の更新を逐次的に行いますから、一時的に不変条件が満たされなくなることがあります。 これは問題ではありません。 不変条件はメンバ関数の入り口と出口で満たされていればいいはずだからです。

不変条件が成り立っているオブジェクトの状態を安定状態、成り立っていない状態を不安定状態と呼ぶことにします (CSP の安定状態・不安定状態とは関係ありません)。 形容詞として、状態、あるいはオブジェクトが安定であるとか、安定でないあるいは不安定であるとかいったりもします。 この用語を使うと「メンバ関数はメンバ変数の値を更新するために一時的に不安定な状態を作り出すが、出口では必ず安定にする」ということができます。

状況の説明

2つのクラス A, B があり、それぞれのインスタンス a, b があるとします。 オブジェクト a に対してメンバ関数 foo が呼び出されると、位置 @A でオブジェクト b に対してメンバ関数 bar の呼び出しを行います。 関数 bar は途中で a に対し、メンバ関数 baz を呼び出すとします。

問題点その 1:関数呼び出し時の安定性

オブジェクト a に対して baz が呼ばれた時点 @C で、オブジェクトは安定でなければなりません。 メンバ関数の入り口だからです。 そのためにはメンバ関数の実行途中である点 @A において安定でなければなりません。 しかし @A はメンバ関数の中ですから、状態は不安定かもしれません。 結局、メンバ関数の中から別のオブジェクトに対してメンバ関数を呼び出すとき、さらにその制御の延長線上で自分自身に対してメンバ関数の呼び出しがある場合には、状態を安定させてから呼び出さなければならないということになります。

別のオブジェクトに対してメンバ関数の呼び出しを行った際に、その制御の延長線上で自分自身が呼ばれることを「呼び返し」ということにします。 上の事例からわかるように、呼び返しの可能性があるすべてのメンバ関数の呼び出しの直前で、状態を安定にしなければならないことになります。 そんなことは実際に意識して行われているのでしょうか。

「すべてについて行う必要はない。メンバ関数ごとに、呼び返しがあるかどうかを明確にしておいて、それを呼び出すときだけ安定であることを保証すればよい」という考え方もあるかもしれません。 しかしそのような管理はできるでしょうか。 複数のクラスが関与している場合はどうでしょうか。 クラス図や相互作用図によって俯瞰すれば、チェックするポイントは列挙できるでしょう。 しかしこのことはオブジェクトの独立性を大きく損なうと思うのです。 あるクラスが変更されるたびに、呼び出し関係を見直さなければならないのだとしたら、クラス間の依存関係が強すぎるということではないでしょうか。

これが問題の1つ目です。 これを解決することができるのかどうかはわかりません。 仮にすべての呼び出しの直前で状態を安定化させることを原則にしたとします。 しかし状態を安定にするために、まさに必要な計算を行う関数が別クラスにあるという場合だってあります。 (これは相互作用のための関数と計算の下請け関数の区別が曖昧だから起こるのかもしれません。) そのような場合は一時的な計算結果をすべて保存しておいて、最後に一度に更新するというようなことになるのでしょうか。 それはそれで大変だし、いつもオブジェクトが2重化するような感じになってしまいます。 さらに、実は不安定な中間状態に依存した呼び返しだってあるかもしれません。 これはあまりいいことがなさそうですが、この問題に無自覚であれば、そうなってしまっているかもしれないということです。

問題点その 2:オブジェクトの自己同一性

よい言葉が見つからなかったので「自己同一性」という言葉を使いました。 ふつうオブジェクトの自己同一性というと、等号で比較したときに同じかどうかという意味ですが、ここではちょっと違う意味に使います(ごめんなさい)。 クラスのコードを書いている人が、オブジェクトの気持ちになって一人称的に考えているときの、その自己の同一性のことなんです。

再び図の状況を考えます。 関数 foo を書いているときには @A の状態で b.bar を呼び出して @B 来ると考えるでしょう。 しかし実際にはその間に状態 @C を通るわけです。 @C にいるとき「同時に別のメンバ関数のどこにいる可能性があるだろうか」といつも考えるのでしょうか。 この場合、baz を実行中で、同時に foo も実行中というような、2重人格のような状態に思えるのです。

このような状況で、メンバ関数 baz の中でメンバ変数を書き換えたらどうなるのでしょうか。 @A の時点で状態が安定化されているとして、baz の呼び出しから返った後の @B では、@A とは別の安定状態になっています。 foo を書く人は、いつでもその可能性を考慮するのでしょうか。 考慮して何をするのでしょうか。 もし、「メンバ変数に変更があった場合には・・・」と考えるのだとすると、@B 以降のコードがさらに多重人格に分裂していくことになると思うのです。

さらに baz からの呼び出し先でオブジェクト a が削除されたらどうなるのでしょうか。 baz は削除されることを知っていたとして、メンバ変数に触らない等の対策が打てるでしょう。 しかし foo ではどうなるのでしょう。

まとめると、呼び返しがあるために「いまオブジェクトがやっていること」を1つに絞って集中してコードを書くことが難しいように見えるのです。

問題点その 3:相互作用の拒否

関数呼び出しというのは呼び出し側の都合でいつでも呼び出すことができるので、呼び出される側が拒否するということができません。 状態から判断して都合の悪いときに呼び出されたらエラーで返すとか assert で落とすとか例外を発生させるなどの対策はできるでしょう。 呼び返しも検出して、判断の材料とすることは可能です。 ただ、これを全面的に行うのは大変な作業なので、抜けが発生しやすくなります。 結果として、メンバ関数を書いたときにはまったく想定していなかった状況で呼び出しが行われ、そこで問題が顕在化したり、内部状態を破壊したりしているかもしれません。

クラスが持つ各メンバ関数を呼び出す手順のようなものは、クラスによっていろいろありうるでしょう。 一方の極では実行順序が細かく規定されている場合があるでしょう。通信だけでなく、メンバ関数の呼び出し規則のこともプロトコルと呼ぶことがありますが、まさに手順があって、いつでもどれでも呼べるわけではないということです。 その一方で、いつでもどれでも呼べるというクラスもあるでしょう。 関数呼び出しが相互作用の手段である場合、後者のようなケースではよいのですが(そしてそういう場合の方が多いと思いますが)、前者のような場合は、テストのための仕掛けがたいへんだと思うのです。

まとめはないけど補足

  • どれも過去に痛い目に会いました。主に GUI のツールキットのあたりで。
  • コールバックは同じような問題を抱えているんじゃないかなあ、と想像しています。
  • 「オブジェクト指向」関係の話をすると、(人のことはいえないのですが)各自自分の脳内にあるイメージ及び言葉の定義(らしきもの)を前提に話をされちゃうことが多いし、相手のいわんとするところを努力して汲み取ろう、チューニングしよう、共通理解を組み立てて話をしようという人になかなか会えないので困るんですけど、この問題については詳しい人の丁寧な説明を聞いてみたいものです。あまり怖い人はいやですけど。もちろん問題が共有できればですが。あるいは話しているうちに問題そのものが変質してもいいんですけど。
  • 3番目の問題は、同期型の相互作用をイメージしてもらえないと「拒否ってなんだよ、何が問題なんだよ」って思われちゃうかもしれません。
2013/09/03
© 2013,2014,2015 PRINCIPIA Limited