メタプログラミング:インターセプターを作ってみる

はじめに
---------
メタなプログラミングがホイホイ気軽にできてしまうのがSmalltalk。アプリを作っているのか、言語を作っているのか、しばしばわからなくなります。必要なのはオブジェクトにメッセージを送るという原則だけ、シンプルですね。

ということで軽い気持ちでインターセプターを作ってみることにしましょう。

インターセプターとは、ここでは、「オブジェクトに送られたメッセージを横取りするもの」とします。悪い奴ではないので、横取りした後で、オリジナルのオブジェクトにメッセージを送り直します。

メッセージを横取りして何をするかですが、今回は単純に、送られたメッセージを逐一トランスクリプトに表示させることにします。

これだけでもデバッグなどの目的で、なかなか役に立つでしょう。

なお、処理系は[Squeak4.4-ja](http://sourceforge.jp/projects/squeak-ja/releases/59208)を使います。
言語の基本的な部分しか使っていないので[Pharo](http://pharo.org/)など他の処理系でも動作すると思います。

クラスの定義
-----------

`ProtoObject`を継承します。オリジナルのオブジェクトを参照するためのインスタンス変数として`originalObject`を持ちます。

```smalltalk:AdvMessageInterceptor
ProtoObject subclass: #AdvMessageInterceptor
	instanceVariableNames: 'originalObject'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'AdvMessageInterceptor'
```

`ProtoObject`はSmalltalkのクラス階層のトップに位置するクラスで、ほとんど何のメソッドも実装されていないという特徴を持ちます。表だってのトップである`Object`も、実は`ProtoObject`を継承しています。

以下をワークスペースで"print it"(Alt or Cmd+p)してみると確認できます。

```smalltalk:doIt
Object superclass. "=> ProtoObject"
ProtoObject superclass. "=> nil"
```

ほぼメソッドが実装されていないクラスを継承する理由ですが、`doesNotUnderstand:aMessage`というコールバックを使いたいからです。
これは、あるオブジェクトがメッセージを受け取ったけれども、理解できないという時に、VM側から送られる特殊メッセージになります。
(早い話がRubyの`method_missing`です。動的のOO言語であれば、最近はよく見られる機構でしょう。)

つまり、`AdvMessageInterceptor`は`ProtoObject`を継承することで、ほとんどのメッセージ送信を理解できず、結果としてVMから`doesNotUnderstand:aMessage`が送られるということになります。引数は処理できなかったメッセージオブジェクトです。

メソッドの定義
-----------

`doesNotUnderstand:`は以下のように実装できるでしょう。

```smalltalk:AdvMessageInterceptor>>doesNotUnderstand:
doesNotUnderstand: aMessage 
	aMessage transcriptOn: originalObject.
	^aMessage sendTo: originalObject
```

引数のメッセージオブジェクトを、`transcriptOn:`で、トランスクリプトに出力しています。その後の`sendTo:`で、元のオブジェクトにメッセージを送り直しています。

基本はこれだけなのですが、もう少し詳しく見ましょう。`transcriptOn:`はいわゆるエクステンション(拡張メソッド)として、`Message`クラスのほうに追加しました。


```smalltalk:Message>>transcriptOn:
transcriptOn: receiver
	Transcript cr; 
		show: ('a {1}({2}) >> {3}'
			 format: {receiver class name. receiver identityHash. self selector})

```

Smalltalkはコア部分のクラスライブラリであろうが、分け隔て無くどこでも拡張するという文化を持ちます。クラスは常にオープンです。
`Transcript`に`show:`を送ってメッセージの情報を文字列として書き出しています。フォーマット文字列の生成もSmalltalkではメッセージ送信です。'a{1}({2}) >> {3}'といった文字列に`format: <オブジェクトの配列>`を送ると各オブジェクトが文字列化して埋め込まれます。

さて、後はサポート部分です。`AdvMessageInterceptor`に、元のオブジェクトを設定するためのメソッド`xxxOriginalObject:`を定義します。


```smalltalk:AdvMessageInterceptor>>xxxOriginalObject:
xxxOriginalObject: anObject
    originalObject := anObject
```

これは単なるセッターです。xxxで始まる変な名前にしているのは、「理解できる」メソッドがこれ以上増えないようにするための工夫です。よく使われるような、普通の名前のメソッド(例えば`name`)を`AdvMessageInterceptor`に定義してしまうと、`name`がインターセプターに送られたときに、普通に`AdvMessageInterceptor`の`name`メソッドが起動されてしまうので、`doesNotUnderstand:`のフックでインターセプトするという戦略が役に立たなくなってしまうのです。そのため`AdvMessageInterceptor`のメソッド定義は、必要最小限にしておき、内部的に使うようなものは変な名前にしておくほうがよいのです。

インスタンス生成用のメソッドとして`on:`を作りましょう。これはクラスメソッドです(他の言語で言うところのstaticメソッド)

```smalltalk:AdvMessageInterceptor_class>>on:
on: anOriginalObject
	^self new xxxOriginalObject: anOriginalObject

```

説明の必要はないでしょう。インスタンスを作ってオリジナルのインターセプトされるオブジェクトをセットしています。結果、オリジナルのオブジェクトを保持したインターセプターが返されます。

もう動きます。ワークスペースで"print it"(Alt or Cmd+p)して試してみましょう。トランスクリプトに出力されるので、あらかじめトランスクリプトを開いておくのをお忘れなく。

```smalltalk:doIt
(AdvMessageInterceptor on: 'Hello'), 'World'. "=> 'HelloWorld'"
```

'HelloWorld'という文字列が返ってきます。同時に`,`メッセージが送られた旨がトランスクリプトにも表示されます。

![HelloWorldIntercepted-001.png](https://qiita-image-store.s3.amazonaws.com/0/61787/b4be1668-f600-bf9a-5a2f-efcc63fb029e.png)

ただ、毎回`AdvMessageInterceptor on: 'Hello'`などとするのはいかにも野暮ったいですね。ここは一つ、`Object`に`intercepted`というショートカット用のメソッドを定義することにしましょう。

```smalltalk:Object>>intercepted
intercepted
	^AdMessageInterceptor on: self
```

これで`'Hello' intercepted, 'World'`と簡潔に書けるようになります。また、インターセプターがインターセプターをインターセプトするとややこしいことになるので、`intercepted`を`AdvMessageInterceptor`にも定義しておくことにしましょう。

```smalltalk:AdvMessageInterceptor>>intercepted
intercepted
	^self
```

これは必須ではないですが、予防のためです。誤って既にオブジェクトをインターセプト中のインターセプターに、`intercepted`を送っても何も起こりません。

動作確認
-----------

では、定番の3+4でやってみることにします。

```smalltalk:doIt
3 intercepted + 4 intercepted. "=>7"
```

![3+4Intercepted-002.png](https://qiita-image-store.s3.amazonaws.com/0/61787/5702aea4-fe06-4db5-629a-94d73cbf488a.png)


3+4もSmalltalkではメッセージ送信で処理されます。3に`+`メッセージが引数4で送られると、引数の4側にいろいろと内部でメッセージが送られているということが確認できます。

「こんなことをしているから遅いのだ」という感じもあるかもしれませんが、Smalltalkは実際のところ動的言語の中では[結構速い](http://d.hatena.ne.jp/sumim/20140826/p1)ので問題ありません。

そして混沌へ
-----------

仕上げとして、もっと複雑なオブジェクトをインターセプトしてみます。「クラスもオブジェクト」なのがSmalltalkですから、いままで定義してきた`AdvMessageInterceptor` _クラス_ をインターセプトすることにしましょう。

こんな感じでしょうか。ワークスペースで"do it"します(Alt or Cmd+d)。

```smalltalk:doIt
Smalltalk at: #AdvMessageInterceptor put: AdvMessageInterceptor intercepted
```
ここでの`Smalltalk`は、システム辞書と呼ばれるグローバルな名前空間を提供するオブジェクトです。Smalltalk内のクラスオブジェクトは、グローバルなクラス名をキーにして、この辞書に登録されています。
上記を実行すると、#AdvMessageInterceptorというキーで登録されていた、`AdvMessageInterceptor`クラスがインターセプターに包み込まれ、登録し直されることになります。(若干危険なことをするので、"do it"前にイメージを保存しておいたほうが良いかもしれません。)

さて、この状態で`AdvMessageInterceptor`クラスをブラウズしてみましょう。

![commentIntercepted-003.png](https://qiita-image-store.s3.amazonaws.com/0/61787/ea05962a-0a94-0736-4ec1-87f302f8f47e.png)


ブラウザ上で`AdvMessageInterceptor`クラスのメソッドを選択する、コメントを表示させるなど、いろいろと操作してみてください。そのたびにトランスクリプトにメッセージが次々と出力されるはずです。上は?ボタンを押してコメントを表示させてみたところです。ブラウザが`hasComment`メッセージを送って、`AdvMessageInterceptor`クラスにコメントがあるかを調べているのがわかりますね。(コメント書いていないので、すみませんという感じですが)

このように、クラスブラウザも、クラスオブジェクトに様々なメッセージを送ることでツールとしての機能を実現しているということがわかります。どこでもメッセージ送信ですね。クラスオブジェクト自身に対するインターセプターというのは、デバッグ用途でブラウザの動きを知りたいといった時には、非常に役立つのではないでしょうか。

とはいえ、そろそろ怖くなってきたので元に戻したいという気持ちも出てきました。戻し方は、インターセプターの`originalObject`に入っている`AdvMessageInterceptor`クラスオブジェクトを、#AdvMessageInterceptorというキーで、`Smalltalk`システム辞書に登録しなおせば良いはずです。

```smalltalk:doIt
Smalltalk at: #AdvMessageInterceptor put: (Smalltalk at: #AdvMessageInterceptor) xxxOriginalObject
```

ところが`xxxOriginalObject`アクセッサを定義していなかったのでした! これは失敗ですね。最小限のメソッド定義にとらわれすぎ、`xxxOriginalObject:`というセッターのみに節約してしまいました。

さて、困りものですが、なんとか回復することにします。

`Object>>becomeForward:`を`AdvMessageInterceptor`に移植してきましょう。

```smalltalk:AdvMessageInterceptor>>becomeForward:
becomeForward: otherObject 
	"Primitive. All variables in the entire system that used to point
	to the receiver now point to the argument.
	Fails if either argument is a SmallInteger."
	(Array with: self)
		elementsForwardIdentityTo:
			(Array with: otherObject)
```

`becomeForward:`は、引数のオブジェクトに、レシーバ自身が成るというものです。`self`であったオブジェクトはどこかに葬り去られます。Smalltalkの重要な黒魔術の一つであり、通常は、あるオブジェクトへの参照を保ちつつそのオブジェクトをこっそりと変身・進化させる場合に用いることが多いです。

残念ながら`ProtoObject`でなく`Object`に定義されていたので、メソッドのコピペとなりました。Smalltalker的にはメソッドのコピペは、かなりの屈辱なのですが、緊急事態なのでまあ良しとしましょう。

`becomeForward:`を使う`quitIntercepting`を定義します。

```smalltalk:AdvMessageInterceptor>>quitIntercepting
quitIntercepting
	self becomeForward: originalObject
```

これで回復できますね。ワークスペースで以下を"do it"します。

```smalltalk:doIt
(Smalltalk at: #AdvMessageInterceptor) quitIntercepting
```

もちろん、以下のようにしても良いです。

```smalltalk:doIt
AdvMessageInterceptor quitIntercepting
```
一見「`AdvMessageInterceptor`クラス」にクラスメッセージを送っているようですが、実際は _「`AdvMessageInterceptor`クラスをインターセプトしている`AdvMessageInterceptor`インスタンス」_ にメッセージを送っているというところに注意してください。


これが送られると、「AdvMessageInterceptorクラスをインターセプトしているAdvMessageInterceptorインスタンス」はただの「AdvMessageInterceptorクラス」になり、平和が戻ります。もうクラスブラウザをいろいろ操作してもトランスクリプトには何もでません。

システムの深い部分をいじりすぎてピンチに陥り脱出する、というのもSmalltalkならではのスリリングで楽しい体験ですので、皆様、ぜひお試し下さい。

ソース一式は[Gist](https://gist.github.com/mumez/59a3da9e4cc31a1165d1)に載せておきました。
パッケージは<a href="http://smalltalkhub.com/#!/~MasashiUmezawa/AdvMessageInterceptor">SmalltalkHub</a>にあります。

実は全く別のアプローチ「メッセージをとにかく理解する」というインターセプターの実装も用意しているのですが、長くなりましたのでこれにて終了です。またの機会ということでお楽しみに。