uzullaがブログ

uzullaがブログです。

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以外にインスタンスをつくれば、それぞれ別の関数を入れられるので、勿論何回でも定義できる。

ということでPHPでもGuardっぽいことはできる!!!*2

懸念点

開放の順序は保障されていないと思うので、設定した順番に実行される保障がない。順序が重要だと、面倒だが$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;

*4


これはこれでだが、複数個登録するときは、開放時に呼ばれるコードが動的に追加できる(Tryをネストすることでもできるが…w)ので、こういうGuardも面白いんじゃないですかね…。


こちらからは以上です。


(ついにレビューが一件つきました!)

*1:指摘もらいましたが、go言語のDeferが似てる感じ有る

*2:正直当方のGuardの認識が間違っている可能性があるので、そうじゃない感もすごい、後述の制限もあるし

*3:えっ、Finallyうごかない?もしかして5.5未満とかいう古くさい環境つかってるんですか!?!?!?!?

*4:「例外をにぎりつぶすな!!!」