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...

" ; my $g = Scope::Guard->new( sub { print "eject!!!

" }); print "using resource...

" ; } foo(); print "resource should be closed

" ;

これの実行結果は以下の通りだ。

$ 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...

" ; my $g = Scope::Guard->new( sub { print "eject!!!

" }); print "using resource...

" ; die "oops" ; } foo(); print "resource should be closed

" ;

$ 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!!!

" }); my $g2 = Scope::Guard->new( sub { print "pipe closed!!!

" }); my $g3 = Scope::Guard->new( sub { print "socket closed!!!

" }); } foo();

実に良い。 foo を抜けるタイミングで、3つのリソースを閉じることができるし、万一どこかでエラーが発生しても、その時点までに定義されたガードは、全て必ず実行される。 ではCommon Lispで同じことをやろうとするとどうなる？

( defun foo () ( unwind-protect ( progn ( unwind-protect ( progn ( unwind-protect ( progn ) ( 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!~%" ) ( guard ( format t "pipe closed!~%" ) ( guard ( format t "sock closed!~%" ) ( 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のアノテーションに影響を受けたらしい)

というフォームを として表現できるようになる。(Pythonのアノテーションに影響を受けたらしい) デフォルトで、クラスや関数のエクスポートや型宣言等を実現する各種のアノテーションを提供する。

自分で複雑なアノテーションを定義するための defannotation マクロを提供する。

ここで defannotation を使って先ほどの guard を定義し直してみよう。

( ql:quickload :cl-annot ) ( 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知見

これを見ればCommon Lispがだいたい書けるようになる。

github.com

処理系はこれで動かす。ようするにperlのplenvとcpanmが合体したみたいなやつ。

github.com

これはようするにperlのcarton。