PHPでもPerlでいうところのGuardをするやで!!
人と話していて、「PerlにはGuardがある、Guardは最高だ、無い言語は不便!」と、もう片手では聞かない回数は聞いたような気がするので、PHPで実装出来ないかという話です。
Guardとは
https://metacpan.org/pod/Guard
あるブロックスコープから復帰(returnなど)する際に行う処理を、そのブロックスコープ内で動的に設定することができる。
多くの場合、テンポラリな設定を戻す為や、開いているファイルを閉じる為とか、色々後処理するコードを先頭にかいておけば、どこでそのブロックが復帰(終了)しても、忘れずに片付けができる、とかそういうやつである。
Perlではしばしばつかわれているが、たとえば以下のようなコードである。
#!perl use strict; use warnings; use Guard; use Cwd; chdir '/tmp'; # カレントディレクトリを/tmpに変更 print Cwd::getcwd()."\n"; # カレントディレクトリを表示 => /tmp { scope_guard { chdir '/tmp' }; # Guardで、ブロック抜けた後に/tmpに変更 chdir "/"; # カレントディレクトリを/に変更 print Cwd::getcwd()."\n"; # カレントディレクトリを表示 => / } print Cwd::getcwd()."\n"; # カレントディレクトリを表示 => /tmp
(皆さんご存じだとおもうが、perlの{~}はブロックである。function(Perlだとsubだろ)などに置き換えてもよい)
これは、最初にカレントディレクトリを/tmpにし、ブロックの中にはいり、カレントディレクトリを/tmpにもどすのをGuardに設定した後、ディレクトリを/に変更している。その後、上から読むかぎりブロック終了時では/tmpにもどしていないが、Guardがきいて戻っている。
というやつである。
$ exec.pl /tmp / /tmp
こんな実行結果が期待される。
PHPにはないのか?
GuardはPHPではあんまり聞かない。というか他の言語であんまりきかない。*1
PHP5.5においては、後述するがFinallyが実装されたので、今後もあんまり活用されない気はする。
しかしながらPHPでも書けるぞ!というのがこのエントリなのでここはへこたれずに以下をどうぞ。
<?php class Guard{ public $destruct; public function __construct($callable){ $this->destruct = $callable; } public function __destruct(){ call_user_func($this->destruct); } } chdir('/tmp'); # カレントディレクトリを/tmpに変更 echo getcwd().PHP_EOL; # カレントディレクトリを表示 call_user_func(function(){ $g = new Guard(function(){ chdir('/tmp'); }); chdir('/'); # カレントディレクトリを/に変更 echo getcwd().PHP_EOL; # カレントディレクトリを表示 }); echo getcwd().PHP_EOL; # カレントディレクトリを表示
結果の出力は上のPerlと同様である。
まず目をひくのがcall_user_func(function(){〜})だろう、これは無名関数を即時実行してるやつである。PHPは関数スコープなので、局所的にスコープをつくるには、こうやって関数にするしかない。
(function(){〜})(); などと長く書くことで大不評なJSを上回る文字数である。
(勿論、普通の関数にしたほうがよいケースなら、こんなことしなくてよい。ここでは「局所的に」使うことができるか、という例も兼ねてる)
ちなみに、あくまで関数なので、中に外の変数を送り込むのにはuse ($param)などとuse句を追加する必要がある。
具体的にいえば、残念なことに以下のようにさらに伸びるのだ。
$param = 'abc'; call_user_func(function()use($param){ echo $param; });
「面倒でもスコープが使えるって便利だし!」とかそういうのはあるが、今回の本筋ではないので話を戻す。
$g = new Guard(function(){ 〜 }); ここがお察しの通り、Guard相当の部分である。
コンストラクタで開放時に実行する無名関数をつめこみ、実際にインスタンスが開放される(無名関数が終了するときに$gへの参照がなくなるので、開放される)と、デストラクタがよばれ、そいつが実行される。
$g以外にインスタンスをつくれば、それぞれ別の関数を入れられるので、勿論何回でも定義できる。
懸念点
開放の順序は保障されていないと思うので、設定した順番に実行される保障がない。順序が重要だと、面倒だが$gをもってつかいまわす(配列で関数を保持する)必要があるかもしれない。
上の例だと$gに参照が何らかの理由でのこって開放されないようにすると、当然デストラクタが呼ばれない(まあこれは気を付ければ良いが)
「今の」PHPは変数は、Perl同様に参照カウントなのでスコープをぬけたら開放されるが、他の実装においてはそれが保障されるとはかぎらない。
また、他の可能性として、もしかすると将来gcが実装され、PHPでも性能の為に、例えばGCを止める必要があるかもしれない。
そんな場合は明示でunsetを呼ばないとダメだろう。(そうなると相当意味がない)
あと、$gは必須というか、$gへの代入をはぶくとうまくうごかなくなるのだが、$gは参照カウンタのためだけにあるので、なににもつかわれない、すると怒り出すのが我が友PHPStormさんである。
つかってない変数宣言するなアホが!!!といわれて右上の緑四角が黄色になってたいそうさびしい。
そもそも…
この程度のことをやらせたいなら、Finallyでいいのでは?という話はありますね*3、ハイ。
<?php chdir('/'); echo getcwd().PHP_EOL; try{ chdir('/tmp'); echo getcwd().PHP_EOL; }catch(\Exception $e){}finally{ chdir('/'); } echo getcwd().PHP_EOL;
これはこれでだが、複数個登録するときは、開放時に呼ばれるコードが動的に追加できる(Tryをネストすることでもできるが…w)ので、こういうGuardも面白いんじゃないですかね…。
こちらからは以上です。
(ついにレビューが一件つきました!)