読者です 読者をやめる 読者になる 読者になる

デリゲートの今後

 XOOPS Cube コアが提供する主要な機能のひとつにデリゲートがある。この機能は「統一的手続きによるコールバック」と説明される。デリゲートの存在意義は本当にシンプルで、 PHP 言語には関数へのポインタがないうえにコールバックの方法が何種類もあり正式なコールバック関数ではパラメータの参照渡しができないなどの制約もあったので、コールバックに関わるTipsを隠蔽してクラス化したものだ。C言語の用語である関数ポインタという表現を多用したくなかったので、最近の言語である C# のデリゲートをモデルにした。

 もし PHP のラムダが早いタイミングで仕様化されていれば、少なくとも XCube_Delegate は不要だったかもしれない。PHP のラムダは 5.3.0 以降なので、 XOOPS Cube コアがラムダを前提にできる時期はかなり先の話になる。それまでは XCube_Delegate にはまだまだ頑張ってもらわなければいけない。

 XCube_Delegate は比較的存在理由が分かりやすいクラスだと思う。分かりにくいといわれるのは、XCube_DelegateManager のほうだ。この管理クラスは、 PHPインタプリタ特性と XOOPS"MOD" 的な特性をフォローするために、ランタイム時にシンボルを遅延解決する役割を担っている。今考えれば、 XCube_DelegateLinkManager と名づけて、 XCube_Delegate の集約クラスではないことをはっきり明示したほうがよかったかもしれない。

 シンボルの遅延解決とはどういう意味かというと、リンカのあるC言語と異なり、インタプリタはパースしたソースコードのぶんしかシンボルを解決できない。デリゲートへは主に preload からアプローチするため、大半のソースコードはまだパースされておらず、「これから require_once されるソースコードの中に含まれるシンボル」を書くと未定義のため Fatal Error になる。しかも XOOPS の場合はユーザーが構成を決定する( MOD 特性)ため、開発者にとっては「これから require_once されるかもしれないソースコードの中に含まれるシンボル」というニュアンスになる。実際に読み込まれるかどうかを決める権利はユーザーにあり、開発者はそれをエラーの引き金にしてはいけない。

 そこで XCube_DelegateManager は、コールバックの登録要請を一時的に預かっておき、対象のデリゲートがすでに宣言されていればその場で解決を行い、宣言されていない場合は宣言されるまでパラメータを預かっておく。最終的に宣言されればそのときにパラメータを渡すし、宣言されなければ預かったパラメータは何にも使わない。そういった仕事を担っている。

 このように XCube_DelegateManager はインタプリタの特性と "MOD" ゆえの課題を同時に解決している。インスタンス内のデリゲートも普通に扱うことができる。

 話が見えてくれば、非常にシンプルで低位な存在だということが分かると思う。なお、この機能はフリーのプログラムを書いて配って他人のプログラムとくんずほぐれつ遊ばない限りは不要な機能なので、「必要性がわからん!」という人はスルーしてかまわない。 MOD 的なことをやると自然と存在意義が分かってくるはずなので、そのうえで使うかどうかを決めればいい。

 さて、現在、このデリゲートとデリゲートマネージャの意味付けを低位から比較的高位に変更しようという議論があがっている。

デリゲートコール

 ひとつはデリゲートをコールする際にマネージャを使用したいという議論だ。これはデリゲートが関数ポインタやラムダの代替であることを考えると、素直じゃない書き方だと思うし、努力規約ではあるけどデリゲート変数のスコープ内でしかコールできないというルールがソースを見たときに失われているように見える危険性もある。

 ただ、プログラムを見ている人にとっては、どこでコールバックを fire しているかが非常に読みにくくて追いにくいらしい。個人的には関数へのポインタなんてそんなもんだという一語に尽きるが、PHP には、関数ポインタがなく、ラムダも普及していないという事情が背景にあるから、慣れないという話もすごく分かる。ただ、「関数へのポインタを使うと呼び出し位置が分からない」という前提を開発者へ紐付けしてしまうと(PHPプログラマはそういうものだということにしてしまうと)、今後 PHP の正式仕様であるラムダにさえ移行することができないということになってしまう。

 実際コール元を突き止めるのに苦労するという話が出ている以上、非常に微妙なところだと思うが、僕としては、「慣れてください」ということをもう一回提案したいと思っている。よほど面倒ならブレークポイントで捕まえて、コールスタックからアプローチするという方法もあるし;;

デリゲートマネージャによるデリゲートの作成

 もうひとつの議題はデリゲートはすべてデリゲートマネージャで作成するようにしようという提案で、これは XCL 2.1.0 …つまり Cube コア 0.9 の開発中に nobunobu さんからも提案されていた。しかし、マネージャに登録するデリゲートは全て static global になってしまうという(コールバックLoverにとって)致命的な問題があるため、現在の形になっている。

 この案を採ると低位の存在であったデリゲート関連が若干高位の存在へ出世する代わりに、低位の関数へのポインタという概念が失われてしまうため、「PHPには関数ポインタがない」という問題が再浮上してしまう。やはり、現在のように使い分けるのが一番理想だが、使い分ける形でいくなら指摘されていることももっともだと思う。

 どこまでいっても結局デリゲートは型ではない。ただ、結局マネージャに作成を任せても、デリゲートマネージャーのヌルチェックが入る点やルートオブジェクトへのアプローチを逐一書かなければいけないため、本当にシンプルになるかどうかは分からない。
(確かデリゲートマネージャのヌルポチェックは XCube_Delegate::register() に仕込んである)

// 現在の場合
MyClass::MyClass()
{
  $this->mMyFuncA = new XCube_Delegate();
  $this->mMyFuncA->register("MyClass.MyFuncA");

  $this->mMyFuncB = new XCube_Delegate();
  $this->mMyFuncB->register("MyClass.MyFuncB");
}

// マネージャにした場合
MyClass::MyClass()
{
  $root =& XCube_Root::getSingleton();
  if (is_object($root->mDelegateManager)) {
    $this->mMyFuncA = $root->mDelegateManager->create("MyClass.MyFuncA");
    $this->mMyFuncB = $root->mDelegateManager->create("MyClass.MyFuncB");
  }
  else {
    $this->mMyFuncA = new XCube_Delegate();
    $this->mMyFuncB = new XCube_Delegate();
  }
}

 結局、元のものよりコードが長く複雑になっている気がするので、デリゲートマネージャがヌルだった場合(このクラスのインスタンス化が早すぎた場合)に対する対応を規約化しておいたほうがよさそうだ。ただ、デリゲートマネージャがない場合はデリゲート変数を初期化しない、というわけにはいかない気もする。

 僕は、純粋な関数へのポインタとしての低位なデリゲートを、ラムダが完全に利用できる日までは残しておくという前提に立っている。だから、この場合は、デリゲートマネージャを別名にするなりして、そのクラスがデリゲートを使うという構図に変更し、デリゲートマネージャーの概念の変更がデリゲートの使い方自体へ影響しない方法が折衷案かなと考えている。