tl;dr: 実装したけど限定的にしかうまく動かなかった。PerlのScope::Guardについて既に良く知っている人は、最後の節まで飛ばしてもよい。
Server::Starter
読んでた
サーバプロセスを起動するツールであるServer::Starterの内部がどうなっているのか知りたくなってきたので、コードリーディングをやることにした。プロセスを扱うツールなだけあって、システムコールやシグナルを活用する本場のUNIXプログラミングを目の当たりにし、普段は見ない語彙がたくさん出てきてたじろいでしまった。たじろぎつつもコードを読み進めていると、ちょっと不思議なコードを見付けた。それは[Guard](https://github.com/kazuho/p5-Server-Starter/blob/master/lib/Server/Starter/Guard.pmというクラスで、ちょっと不思議な見た目をしている。以下コードを引用する。
package Server::Starter::Guard; use strict; use warnings; sub new { my ($klass, $handler) = @_; return bless { handler => $handler, active => 1, }, $klass; } sub dismiss { shift->{active} = 0 } sub DESTROY { my $self = shift; $self->{active} && $self->{handler}->(); } 1;
このGuard
は本体ファイルの中で、ファイルを削除したりするサブルーチンレファレンスを引数として呼び出されていた。
別ファイルに分離されているのだから複雑なことをやっているのだろうと身構えていたが、あまりに簡素に定義されているので拍子抜けしてしまった。一体これは何を行うクラスだろう?
不思議なコード
コードをよく見てほしい。大文字でDESTROY
というメソッドが定義されているのが見える。一般的なコーディング規約では大文字でサブルーチンを書くことはないように思われる。これは何らかの特殊な操作を行うメソッドではないか?という勘が働き、perl DESTROY
で検索したところ、perldocに以下のような記述を見出すことができた。
デストラクタ
あるオブジェクトに対する最後のリファレンスが消滅したとき、そのオブジェクトは 自動的に破棄されます(これはあなたがリファレンスを大域変数に格納していて、 プログラムを終了するときでもそうです)。 もしオブジェクトが解放される直前に制御を横取りしたいのであれば、クラスの 中で DESTROY メソッドを定義することができます。
つまりGuard
はデストラクタに何か細工をしているクラスだということがわかる。しかも、new
されるときにサブルーチンへのリファレンスを受け取り、デストラクタでそれを呼び出している!最後のリファレンスが消滅したときにコードを呼び出すようなクラスにどういった使い道があるというのか?
これはGuard
が定義されたスコープを抜けるときに特定のコードを呼び出すためのコードだ。何を言っているかわからない?では同じようなコードで構成されている別のライブラリのドキュメントを見てみよう。名前はまさしくScope::Guard
だ。
Scope::Guard - レキシカルスコープにおけるリソース管理
このモジュールは, スコープ終了地点でのクリーンアップまたはリソース管理などを行う 使い勝手のよい手段を提供します. これは例外を扱う場合に特に便利です: Scope::Guardのコンストラクタはサブルーチンリファレンスを受け取り, スレッドの実行が早い段階でアボートしたときでもそれを呼び出すことを保証します. この機能は, perlのガーベッジコレクタが, レキシカルスコープの処理を自動的に引き受けることを保証するので, 実現できます.
つまりこういうふうに使うライブラリだ。
- サブルーチンなどのブロックでスコープをつくる。
- スコープの中で、後から必ず開放しなければならないリソース、例えばファイルハンドルをオープンする。
- その直後に
Scope::Guard
をBless
(インスタンス化)し、リソース(ここではファイルハンドル)をクローズするためのサブルーチンを与える。 - スコープから抜ける時に、自動的に
Scope::Guard
に渡したサブルーチンが呼び出される。
コードを見たほうがわかりやすいかもしれない。
use Scope::Guard; sub foo { print "opening resource...\n"; # ここでファイルハンドルなどのリソースを開く my $g = Scope::Guard->new(sub { print "eject!!!\n" }); # ← リソースを閉じる処理をsubに渡しておく print "using resource...\n"; # ここでリソースを使う } foo(); print "resource should be closed\n";
これの実行結果は以下の通りだ。
$ perl tmp.pl opening resource... using resource... eject!!! resource should be closed
foo
脱出時に自動的にScope::Guard
に渡したコードが動作した。これでファイルハンドルの開放忘れは起きなくなるだろう。
ちなみにfoo
内でエラーが発生してもScope::Guard
に渡したコードは必ず呼び出されるという優れ物だ。
use Scope::Guard; sub foo { print "opening resource...\n"; # ここでファイルハンドルなどのリソースを開く my $g = Scope::Guard->new(sub { print "eject!!!\n" }); # ← リソースを閉じる処理をsubに渡しておく print "using resource...\n"; # おおっと、ひどいことが発生したようだ die "oops"; } foo(); print "resource should be closed\n";
$ perl tmp.pl opening resource... using resource... oops at tmp.pl line 8. eject!!!
どうしてこういったことができるのだろう?
ガーベジコレクション
これは以下のようなメカニズムで実現されている。
foo
が呼び出される。$g
がbless
され、メモリに$g
の領域が確保される。- リソースが使われた後、
foo
から処理が戻ろうとする。 $g
への参照がどこにも存在しなくなる。Perlのガーベジコレクションは参照カウント方式なので、$g
を破棄すべきことが検知される。$g
にはDESTROY
が定義されているので、$g
が破棄される前にDESTROY
が呼び出される。DESTROY
でリソースの開放が行われる。$g
が破棄される。foo
から処理が戻る。
ガーベジコレクションをうまく利用して、スコープ脱出時に処理を行わせることに成功している。とてもうまくできたやり方だ。
Server::Starter
で定義されているGuard
もこれとほぼ同じもので、ファイルハンドルやプロセスファイルの削除を保証する目的などに使われている。
Common Lispでやってみる
ここからが本題。Common Lispの勉強もかねて、Scope::Guard
同様のものを・・・つまりスコープを抜けるときに必ず一定の処理を行うようなコードを書いてみることにした。
とはいえガーベジコレクションに頼る必要はない。Common Lispには、与えられたフォームから脱出する際に必ず一定の処理を行うことができる特殊オペレータUNWIND-PROTECT
が用意されている。
Special Operator UNWIND-PROTECT
unwind-protect evaluates protected-form and guarantees that cleanup-forms are executed before unwind-protect exits, whether it terminates normally or is aborted by a control transfer of some kind. unwind-protect is intended to be used to make sure that certain side effects take place after the evaluation of protected-form.
UNWIND-PROTECT
で先ほどのコードと同じことを実現するには、次のように書く。
(defun foo () (unwind-protect (progn ;; 第一引数: 本体処理 (format t "opening resource...~%") ;; ここでリソースを開き、使う (format t "using resource...~%")) (format t "eject!!!~%"))) ;; 第二引数: クリーンアップ処理 (foo) (format t "resource should be closed")
UNWIND-PROTECT
は、第一引数として処理本体となるフォーム(protected-form)を、第二引数として後始末に用いるフォーム(cleanup-form)を受け取る。
protected-formが実行されてUNWIND-PROTECT
から処理が抜ける際、必ずcleanup-formが実行されるようになる。エラー(コンディション)の発生などでスタックが巻き戻されるときにも、cleanup-formは必ず実行される。
これを実行する*1と、Perl版と同様の結果が得られる。
$ ros tmp.ros opening resource... using resource... eject!!! resource should be closed
前回同様に、エラーを起こしてみよう。
(defun foo () (unwind-protect (progn (format t "opening resource...~%") (format t "using resource...~%") (error "oops")) ;; うわわ!ゼロ除算だ! (format t "eject!!!~%"))) (foo) (format t "resource should be closed")
$ ros tmp.ros opening resource... using resource... Unhandled SIMPLE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING {10019876A3}>: oops Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {10019876A3}> 0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-ERROR "oops" {10037CB703}> #<unused argument>) 1: (SB-DEBUG::RUN-HOOK SB-EXT:*INVOKE-DEBUGGER-HOOK* #<SIMPLE-ERROR "oops" {10037CB703}>) 2: (INVOKE-DEBUGGER #<SIMPLE-ERROR "oops" {10037CB703}>) 3: (ERROR "oops") 4: (FOO) ...長いスタックトレース... unhandled condition in --disable-debugger mode, quitting eject!!!
処理系が発するエラーメッセージがたくさん表示されるが、クリーンアップ処理がきちんと動作していることがわかる。これで気兼ねなくリソースが開ける!やったね!
ちょっと待って!たくさんリソースを開く場合
一つリソースを開いたら、複数開きたくなるものだ。早速たくさんのリソースをガード付きで開いてみよう。Perlでは・・・
sub foo { # ここでファイルを開いた my $g1 = Scope::Guard->new(sub { print "file closed!!!\n" }); # パイプも開こう! my $g2 = Scope::Guard->new(sub { print "pipe closed!!!\n" }); # ソケットも開いた!! my $g3 = Scope::Guard->new(sub { print "socket closed!!!\n" }); } foo();
実に良い。foo
を抜けるタイミングで、3つのリソースを閉じることができるし、万一どこかでエラーが発生しても、その時点までに定義されたガードは、全て必ず実行される。
ではCommon Lispで同じことをやろうとするとどうなる?
(defun foo () (unwind-protect (progn ;; open file ここで開いたファイルが・・・ ;; use file (unwind-protect (progn ;; open pipe ;; use pipe (unwind-protect (progn ;; open sock ;; use sock ) (format t "sock closed!!!~%"))) (format t "pipe closed!!!~%"))) (format t "file closed!!!~%"))) ;; ここで閉じられる (foo)
う~ん・・・いたずらにインデントが深くなってしまっているのが否めない。もちろんUNWIND-PROTECT
による「保護膜」が三重になっているのはわかる。
でもScope::Guard
に倣えば、「保護膜」はそのスコープが閉じられるまでをカバーすれば良いはずだ。同じスコープで複数のガードを定義したとしても、それらが有効であるのはどれも同じ、そのスコープの終わりまでだ。それなのにUNWIND-PROTECT
は、どこまでが守られるべきフォームなのかを明示しなければならないから、Scope::Guard
とまったく同じようには使えない。必ず明示的に守備範囲を教えてあげる必要があるのだ。それとは対照的に、Scope::Guard
はその守備範囲を自動的に決定する。このメリットを活かせないのは残念だ。
しかも、最初にリソースをオープンした箇所(ここではファイル)と、それを閉じる箇所がかけ離れてしまっている。あまり読みやすいコードではない。
これを読みやすくする方法を考えようというのが今回のお話。
マクロを使ってみる
そもそも、クリーンアップ処理が最後に来てしまうのが見難さの原因になっている。マクロを使って順序を入れ替えてみよう。
(defmacro guard (cleanup protect) `(unwind-protect ,protect ,cleanup)) (defun bar () (guard (format t "file closed!~%") ;; open file (guard (format t "pipe closed!~%") ;; open pipe (guard (format t "sock closed!~%") ;; open sock ;; use file, pipe and sock (format t "using...~%") )))) (bar)
さっきより随分と見やすくなった。これを実行してみる。
$ ros tmp.ros using... sock closed! pipe closed! file closed!
マクロをつかって、UNWIND-PROTECT
を使ったガードを綺麗に書けるようになった。でも相変わらずインデントが掘られているし、UNWIND-PROTECT
に守備範囲を教えてあげる必要がある。
カッコの数をなんとか減らせないだろうか?なんとかしてScope::Guard
を再現したい!
cl-annot
でインデントを減らす
ここで満を持してcl-annot
というライブラリが登場する。このライブラリの詳細な解説はREADMEを見てもらうとして、簡単にこのライブラリの機能を説明すると、おおむね以下の通りになる。
(foo bar)
というフォームを@foo bar
として表現できるようになる。(Pythonのアノテーションに影響を受けたらしい)- デフォルトで、クラスや関数のエクスポートや型宣言等を実現する各種のアノテーションを提供する。
- 自分で複雑なアノテーションを定義するための
defannotation
マクロを提供する。
ここでdefannotation
を使って先ほどのguard
を定義し直してみよう。
(ql:quickload :cl-annot) ;; ライブラリ読み込み ;; アノテーションを定義する ;; アリティは2つ、インライン展開を有効化する (annot:defannotation guard (cleanup protect) (:arity 2 :inline t) `(unwind-protect ,protect ,cleanup)) (annot:enable-annot-syntax) ;; アノテーション記法を有効化する (defun qux () @guard (format t "file closed!~%") @guard (format t "pipe closed!~%") @guard (format t "sock closed!~%") (format t "using...~%"))
ここで定義したqux
は、前掲のfoo
と全く同じ形に展開される。これでインデントを深くせずにガードを定義することができた。ほとんど見た目もScope::Guard
と同じだ。
cl-annot
をうまく活用して、自然な形でガードを定義できるようになった。これをうまく使ってServer::Starter
の写経を行ってみようと思う。
だめでした
実は先ほど定義したアノテーションは、@guard
の間に何らかの処理が挟まるとうまくスコープ全体を守備範囲にできなくなってしまう。試しにガードの間に幾つかの処理を挟んでみた。マクロを展開して見るために、macroexpand-1
を使っている。
(defun hoge () (format t "~S~%" (macroexpand-1 '(progn (format t "file opened!~%") @guard (format t "file closed!~%") (format t "pipe opened!~%") @guard (format t "pipe closed!~%") (format t "file and pipe using~%") @guard (format t "sock closed!~%") (format t "using...~%")))))
このコードを実行すると、以下のように展開されたマクロが表示される。
(PROGN (FORMAT T "file opened!~%") (UNWIND-PROTECT (FORMAT T "pipe opened!~%") (FORMAT T "file closed!~%")) (UNWIND-PROTECT (FORMAT T "file and pipe using~%") (FORMAT T "pipe closed!~%")) (UNWIND-PROTECT (FORMAT T "using...~%") (FORMAT T "sock closed!~%")))
パイプを開いたとおもったら、ファイルが閉じられてしまった。このコードを実行すると、以下のような表示になる。
$ ros tmp.ros file opened! pipe opened! file closed! file and pipe using pipe closed! using... sock closed!
これではファイルを使うべき箇所でファイルを使えない。これは、cl-annot
のアノテーションが:arity
として渡しただけのシンボルしか引数として読み込まないために起きる。:arity
に2を渡せば、シンボルは2つしか読み込まれないので、それ以降の呼び出しはUNWIND-PROETCT
のprotected-formには入らない。qux
のときにはうまくいったのは、マクロ展開の過程で連鎖的にアノテーションが展開され、結果的に@guard
以降のコードがそれぞれ1つのフォームになり、これをそれぞれのアノテーションが取り込んだからだ。つまり、一番最後の@guard
が展開された結果UNWIND-PROTECT
という1つのフォームが形成され、これを後ろから二番目の@guard
がprotected-formとして取り込み、そうしてできた1つのフォームを最初の@guard
がprotected-formとして取り込んだのだ。
(:arity
にt
を渡すなどして)スコープが終わるまで読み込めるだけのシンボルを読み込む動作があればいいのだが、今のところ存在していないようだ。わりと便利な挙動だと思うので、がんばってcl-annot
のコードを読んで、あわよくば機能を増やしてみたい。今のままでも充分役立つと思うけどね。
もしcl-annot
に手を入れずにうまくやる方法を見付けたら教えてください。
Common Lisp知見
- 作者: Peter Seibel,佐野匡俊,水丸淳,園城雅之,金子祐介
- 出版社/メーカー: オーム社
- 発売日: 2008/07/26
- メディア: 単行本(ソフトカバー)
- 購入: 8人 クリック: 192回
- この商品を含むブログ (69件) を見る
これを見ればCommon Lispがだいたい書けるようになる。
処理系はこれで動かす。ようするにperlのplenvとcpanmが合体したみたいなやつ。
これはようするにperlのcarton。
*1:ここでは処理系を直接呼び出す代わりにroswellを使った。