EmacsでAOPに挑戦?
Emacsで"M-x grep-find"を良く使います。.emacs.elで次のような設定をしています。Windowsの場合、はじくファイルが多いので(.exe、.dll、.pdbその他色々)、もっと長くなっています。
(setq grep-find-command "find . -type f ! -name '*,v' ! -name '*~' ! -name '*.o' ! -name '*.a' ! -name '*.so' ! -name '*.class' ! -name '*.jar' ! -name 'semantic.cache' ! -path '*.deps*' ! -path '*/obsolete/*' ! -path '*/.svn/*' ! -path '*/CVS/*' -print0 | xargs -0 -e grep -n -e ")
"M-x grep-find"を使う場面のほとんどは、ソースコード中のシンボルの検索です。普通に使うと、"M-x grep-find"の後、シンボルを入力する必要があります。これが結構面倒なので、カーソル位置(ポイント)のシンボルの文字列を自動で入るようにしました。
(defun my-grep-find () (interactive) (let ((grep-find-command (concat grep-find-command (thing-at-point 'symbol)))) (call-interactively 'grep-find)))
この関数を使うと目的が達成できます。grep-find-commandに値を設定している前提のコードです。手続き言語的にコードの読み方を説明すると、"(let ((A B)) C)"は、「ローカル変数Aに値BをセットしてCを実行する」、です。関数my-grep-findでは、ローカル変数grep-find-commandにconcat関数の戻り値(これは名前の通り、文字列の連接です)をセットして、grep-findを実行します。grep-findが、内部でgrep-find-commandの文字列をシェルのコマンド列として解釈することを知った上でのコードです。
grep-find-commandはローカル変数(本当は束縛と言うべき)なので、元のgrep-find-commandは影響を受けません。なので、繰り返しmy-grep-findを呼んでも、次々と文字列が連接されたりはしません。
と、この説明ですんなり理解できた人はたぶん頭が柔軟な人です。普通の(と言ったら語弊がありますが)レキシカルスコープと変数代入の感覚のままでは、奇妙な動作に感じると思います。letで束縛した変数は、その変数名と値の組が残り続けてletを抜けるまで参照される、と思って読むと理解しやすいと思います。
キーバインドを変更することには賛否両論ありますが、この関数に名前(my-grep-find)を付ける意味も感じないので、"C-x g"にアサインしてしまいました。
(global-set-key "\C-xg" (lambda () (interactive) (let ((grep-find-command (concat grep-find-command (thing-at-point 'symbol)))) (call-interactively 'grep-find))))
この関数は元のgrep-find同様、ミニバッファにコマンド列が出て、対話的に実行します。せっかちな人のために、カーソル位置にシンボルがあればいきなり実行する例も示します。シンボルが無い場合、通常通りミニバッファで対話的に実行します。
(defun my-grep-find2 () (interactive) (if (thing-at-point 'symbol) (let ((null-device nil)) (grep (concat grep-find-command (thing-at-point 'symbol)))) (call-interactively 'grep-find)))
かなりgrep-findの内部知識を利用したコードです。手続き言語的に読み方を説明すると、ifの次の行がif節相当で、最後の行がelse節相当です。カッコではなくインデントで感じるのがLisp風のようです(伝聞)。どこかのP言語を思い出す人もいるかもしれません。
ふたつ関数を示しましたが、どちらも言わばラッパー関数です。このように既存関数をちょっといじる場合には、AOP的なアドバイスが使える気がします。
Emacs Lispのアドバイスの動作を調べるために簡単な例を示します。
(defun my-func () foo)
これは変数fooの値を返す関数です。fooに値がセットされていないとエラーになります。次のようにaroundアドバイスを作ってみます。
(defadvice my-func (around my-func-around activate) (let ((foo "foo")) ad-do-it))
Cのプリプロセッサマクロ風に読み下すと、my-funcのaroundアドバイスで、ad-do-itの部分が元の関数の本体に置換されます。"my-func-around"はこのアドバイスに付けた名前です。activateはおまじないと思ってください。マクロのプロセッサの気持ちで読み下すと、ad-do-itの部分がfooに置き換わったmy-funcができあがります。my-funcを実行すると、変数fooの値、文字列"foo"を返します。
(defadvice grep-find (around grep-find-around activate) (let ((grep-find-command (concat grep-find-command (thing-at-point 'symbol)))) ad-do-it))
こういうアドバイスを書けば、my-grep-findのような下らないラッパーを書かなくて良いのでは、と思ったわけです。
うまく行きそうな気がしていたのですが、これは意図通りに動作しません。理由はgrep-findがgrep-find-commandを参照している箇所が、関数の本体内ではなく、「interactiveの中」だからです。「interactiveの中」というのは、適切な表現を知らないので適当にでっちあげた言葉です。grep-find自体は引数を受け取る関数として定義されています。引数が無い時、対話的(ミニバッファ)に引数を渡すようになっています。この引数を生成する処理部分が「interactiveの中」と呼んでいる処理です。aroundアドバイスのad-do-itは(元関数の)関数本体で、「interactiveの中」とは別枠です。
defadviceで「interactiveの中」を書き換えることはできますが、単に全体を書き換えるだけの機能のようです(ぼくの理解では)。 次のようなアドバイスを書けないことは無いですが、これは元のコードをコピー&ペーストして一部を書き換える、という最初のラッパーアプローチよりずっとひどいやり方です。
; ugly advice... (defadvice grep-find (around grep-find-around activate) (interactive (if (thing-at-point 'symbol) (list (concat grep-find-command (thing-at-point 'symbol))) (list (read-from-minibuffer "Run find (like this): " grep-find-command nil nil 'grep-find-history)))) ad-do-it)
grep-findに対話的な動作を残したいので、アドバイスではうまく書けなさそうです。たぶんEmacsが悪いのではなく、ぼくの力不足のためでしょう。ラッパー的に書いた"C-x g"でやりたいことはできているので、遊びの領域の話です。ちなみに、defadvice自体のコードは、何がなんだか全く分かりません。Cのプリプロセッサマクロなんてかわいいものだ、という気になります。
- Category(s)
- カテゴリなし
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/inoue/aop-of-emacs/tbping