PHP 5.3を使ってみました。

todacreate_function,goto,PHP,PHP 5.3,self,static,use,クロージャ,フレームワーク,無名関数,遅延静的バインディング

誰が名付けたのか分からないままに定着してしまった「シルバーウィーク」でしたが、皆様いかがでしたでしょうか。私は結局家でゴロゴロしている間に終わってしまいました。こんにちわ。毎度どうもtodaです。
どうせ来年以降はもう無いでしょうから、名前を付ける必要があったのか甚だ疑問です。

さて、先日PHP 5.3がリリースされました。
2006年11月にリリースされたPHP 5.2からの約3年後のアップデートとなります。

当社でもPHPを活用しておりますので、その更新情報は気になるところです。
今回はそのPHP 5.3をちょっと触ってみて、個人的に注目している新機能をレビューしてみたいと思います。

機能1. クロージャ(無名関数)

無名関数というのは、その名前のとおり名前を持たない関数です。
もともとある一連の流れをまとめたものである関数自体を、一つのオブジェクトとして扱うことができます。

このようにしてオブジェクト化した関数は、他の関数やオブジェクトへのコールバック関数として利用されます。
PHPの例でいえば、array_walk()関数では、各要素にどのような処理を行うかのコールバック関数を指定します。

今までのPHP 5.2では、create_function()関数で無名関数を作成する必要がありました。create_function()は関数の引数部と定義をPHPの文字列として渡すという特徴があります。

$fruits = array("レモン", "オレンジ", "バナナ", "りんご");

// 名前''で囲んで表示する無名関数
// 関数内の'は\でエスケープする必要があることに注意。
$function = create_function('$fruit', '
	echo "\'$fruit\'\n";
');

// $fruits の各果物に、$functionを適用して、すべての名前を表示する。
array_walk($fruits, $function);

実行結果:
'レモン'
'オレンジ'
'バナナ'
'りんご'

この引数部と本体が文字列であることには、以下のような不便さがありました。

・可読性の低下
・記号類(特に$, ', “)でエスケープが必要な場合がある。
・eclipseなどの統合環境やエディタのキーワード強調表示が無効になる。

・パフォーマンスの低下
・実行時にcreate_function()が評価されるときに、初めて本体のコンパイルが行われるので、実行中のオーバヘッドが生じる。
・(Zend GuardやAPCなど)バイトコードキャッシュの対象とならない。

・メモリ効率の低下
・スコープを抜けても、create_function()で確保したメモリは開放されない。
・多重ループ内で呼び出すと、あっという間にメモリの上限を超えてしまう。

これに対して、PHP 5.3ではいわゆるクロージャと呼ばれる無名関数を作成することができます。といっても、その構文は通常の関数定義から関数名を省略しただけです。

$fruits = array("レモン", "オレンジ", "バナナ", "りんご");

// 名前を表示する無名関数。
// あくまでもこれは文としては代入文なので、最後にセミコロンが必要。
$function = function($fruit)
{
	echo "'$fruit'\n";
};

// $fruits の各果物に、$functionを適用して、すべての名前を表示する。
array_walk($fruits, $function);

この形式の利点は、あくまでも通常の構文の範囲で記述できるため、可読性が低下せずエディタの強調表示にも影響を与えません。また、スクリプトのロードの時点でコンパイルが完了しますので、create_function()のように実行時のオーバーヘッドはありません。バイナリコードキャッシュの対象にもなるでしょう。

create_function()とクロージャ形式の無名関数の場合のベンチマークをとってみました。用意したソースは以下の2つで、0 ~ 10000000まで単純に合計するのにかかる時間を計ります。それまでの合計($total)に数を加える部分が無名関数($func)になります。

create_function()版:

$func = create_function('&$t, $i', '
	$t += $i;
');

$start = microtime(true);
$total = 0;
for($i = 0; $i < 10000000; $i++){
	$func($total, $i);
}

$end = microtime(true);

echo "合計: $total\n";
echo $end - $start . "sec.\n";

クロージャ形式版:

$func = function(&$t, $i){
	$t += $i;
};

$start = microtime(true);
$total = 0;
for($i = 0; $i < 10000000; $i++){
	$func($total, $i);
}

$end = microtime(true);

echo "合計: $total\n";
echo $end - $start . "sec.\n";

計測はWindows XP上のPHP 5.3.0(cli)で5回行い、最大値と最小値を捨てた残り3回の平均を算出しました。

create_function版: 4.781秒
クロージャ形式版: 2.727秒

この結果ではクロージャ形式のほうが、約半分の処理時間で済んでいることが確認できました。可読性も向上して、処理速度も向上するのですから良いことづくめですね。

またcreate_function()には無くて、クロージャ形式にある特徴として、無名関数の外の変数を参照することができます。
たとえば、create_function()では以下のようなコードは動作しません。

$i = 1;

$function = create_function('', '
	echo ($i == 1) ? "one" : "other";
');

call_user_func($function);

create_function()の定義部の"$i"が、その外である1行目の"$i = 1;"とリンクしていないため、別物として扱われるからです。
このコードを意図通り動作させるには、create_function()の中で"global $i;"を宣言して、グローバル変数としての"$i"を参照することを宣言するか、

$i = 1;

$function = create_function('', '
	global $i;
	echo ($i == 1) ? "one" : "other";
');

$function();

無名関数に引数として引き渡すかの方法が考えられます。

$i = 1;

$function = create_function('$i', '
	echo ($i == 1) ? "one" : "other";
');

$function($i);

しかし、今回のように必要とする変数がグローバルであるとは限りませんし、引数として渡すとしても際限なく数が増えると大変です。

これを解決するため、関数定義にuse()宣言を置くことで、外部の変数を参照することができるようになります。

$i = 1;

$function = function() use ($i){
	echo ($i == 1) ? "one" : "other";
};

$function();

他言語にはuse()のような宣言は不要で、暗黙的に外のスコープの変数を参照できるものもありますが、PHPではどの変数が参照されているかを明示的に指定することにしたようです。

この無名関数を上手に利用すると、従来のクラスでは表現が困難だった条件分岐、ループを含む一連の動作を変数化して、他の関数・オブジェクトに渡すことができます。

機能2. goto

今更goto? と思われたかもしれませんが、gotoは本来非常に強力な制御構文です。あまりにも強力すぎて上から下へ、下から上へ制御を飛ばすことができ、処理の流れが複雑に絡み合ったスパゲッテイコードを生み出す諸悪の根源とも言われてきました。

そのスパゲッティなコードの撲滅のために設計された構造化言語では、while, do ~ wihle, switch, forなどのループ構文(= 副作用の少ないgoto)によってgotoの排除を試みています。
ほとんどの場合これらの制御構文は十分な表現力を持っていますが、今日になってPHPにもgotoが導入されたのは、gotoの方が有用かつ、簡潔な場合があるからに他なりません。

gotoの典型的な利用の仕方として、以下の3つがあります。
1. 多重ループから一気に抜ける。

while(1){
	while(1){
		while(1){
			if(条件){
				ループを抜ける;
			}
		}
	}
}
// ここに抜けたい。

PHPの場合はbreakに抜けるループの数を指定するという方法もあります。

while(1){
	while(1){
		while(1){
			if(条件){
				break 3;	// ループを3個抜ける。
			}
		}
	}
}
// ここに抜ける。

ただ、この方法はループの数を増減するとbreakの数も変更する必要があるという欠点があり、この修正を忘れると発見のしにくいバグとなる可能性があります。

while(1){
	while(1){
		while(1){
			while(1){
				if(条件){
					break 3;	// ループを3個抜ける。
				}
			}
		}
	}
	// ここに抜ける。
}

ここでgotoの出番です。抜けたい地点にラベルを置いて、そこへgotoすれば良いのです。この方法であれば、ループの段数の変更があってその他の修正は要りません。

while(1){
	while(1){
		while(1){
			if(条件){
				goto done;	// ループを3個抜ける。
			}
		}
	}
}
done:
// ここに抜ける。

2. 共通エラー処理に飛ばす。
メインルーチン処理中のエラーチェックで、エラーがあったときに後始末(openしたファイルをcloseするなど)をする必要があることがあります。
このエラーチェックが何度もあったりすると、後始末のコードがメインルーチンのコードに混ざってしまい、可読性が低下することがあります。

たとえば、内部で3つのファイルを開いて処理をする関数を考えます。エラーが発生した箇所によって後始末(fclose)の中身が異なることに注目してください。

function func()
{
	if(($fp1 = fopen("file1.txt", "r")) === false){
		return;
	}
	if(($fp2 = fopen("file2.txt", "r")) === false){
		fclose($fp1);
		return;
	}
	if(($fp3 = fopen("file3.txt", "r")) === false){
		fclose($fp1);
		fclose($fp2);
		return;
	}

	// メインルーチン
	if(エラー発生?){
		fclose($fp1);
		fclose($fp2);
		fclose($fp3);
		return;
	}
	// メインルーチン (ここまで)

	fclose($fp1);
	fclose($fp2);
	fclose($fp3);
	return;
}

ここでもgotoの出番です。最後に用意した共通後始末処理(エラー処理)に飛ばすことで、どこでエラーが発生しても1行で済ますことができます。メインルーチンがずいぶんとすっきりしたようには思えませんか?

function func()
{
	$fp1 = $fp2 = $fp3 = false;		// まずはfalseで初期化

	if(($fp1 = fopen("file1.txt", "r")) === false){
		goto finish;
	}
	if(($fp2 = fopen("file2.txt", "r")) === false){
		goto finish;
	}
	if(($fp3 = fopen("file3.txt", "r")) === false){
		goto finish;
	}

	// メインルーチン
	if(エラー発生?){
		goto finish;
	}
	// メインルーチン (ここまで)

finish:
	// 後始末。openされているファイルをcloseしていく。
	// 初期値(false)でないものをcloseすればよい。
	if($fp1 === false){
		fclose($fp1);
	}
	if($fp2 === false){
		fclose($fp2);
	}
	if($fp3 === false){
		fclose($fp3);
	}
}

3. メインルーチンをもう一回やりなおす。
ループの終了条件が実行時にしか分からない場合があります。たとえば、ファイルの中を一行ずつ読んで、特定の文字列があったらループを抜けるなど。
この場合、メインルーチン全体をwhile(1){}で括って、ループの終了条件に合致したらbreakで抜けるというコードになると思います。

function func()
{
	if(($fp = fopen("file1.txt", "r")) === false){
		return;
	}

	while(1){
		if(($line = fgets($fp)) === false){
			break;
		}
		if(preg_match("/tricorn/", $line)){
			break;
		}

		// その他いろいろな処理
	}

	fclose($fp);
}

このコードの良くないところは、メインルーチン全体がwhile(1)で括られて字下げされてしまうことです。
特にメインンルーチンが長くなると、字下げされた部分も長くなってしまい、せっかくの字下げの効果が破壊されます。

これもgotoを使うことができます。メインルーチンの先頭に逆戻りするgoto文を置きます。見ての通り、字下げが発生しません。(ついで言えば、ラベル"again"が関数内の前処理とメインルーチン、ラベル"finish"がメインルーチンと後始末のブロックを分離している暗黙のコメントになっているという効果も見逃せません)

function func()
{
	if(($fp = fopen("file1.txt", "r")) === false){
		return;
	}

again:
	if(($line = fgets($fp)) === false){
		goto finish;
	}
	if(preg_match("/tricorn/", $line)){
		goto finish;
	}

	// その他いろいろな処理
	goto again;

finish:
	fclose($fp);
}

gotoは不要であると主張する人も多いのですが、私は全てのそれぞれのツールには、それぞれの使い道があるものであると考えています。私はgotoの導入には歓迎です。

機能3. 遅延静的バインディング

クラス変数(プロパティ)にstatic宣言を付けることができます。この変数はクラス全体で共有される制限付きグローバル変数のようなもので、"クラス名::変数名"で参照することが可能です。

このクラス名の代わりに"self"というキーワードを指定すると、自分自身のクラス名に置き換えられて評価されるのですが、PHP 5.2ではクラスを継承した場合に"self"の評価が意図通り行われないことがありました。

class Base
{
	static protected $_data = 1;

	public function showData()
	{
		echo self::$_data."\n";
	}
}

class Sub extends Base
{
	static protected $_data = 2;		// Baseのstaticクラス変数を上書きしている。

}

$base = new Base();
$base->showData();	// 1

$sub = new Sub();
$sub->showData();	// 上書きした2が表示されるはずなのに、1が表示される。

これはPHP 5.2では"self"の評価がコンパイル時に行われるためです。つまり、BaseクラスのshowData()のコンパイル時に"self"が"Base"に置き換わりBase::$_dataが参照され、そのメソッドがSubクラス側に継承されてしまったのです。

これを解決するには、Base側でもshowData()を明示的に上書きする必要がありました。

class Base
{
	static protected $_data = 1;

	public function showData()
	{
		echo self::$_data."\n";
	}
}

class Sub extends Base
{
	static protected $_data = 2;

	public function showData()
	{
		echo self::$_data."\n";		// この"self"はコンパイル時に"Sub"に置き換わるので、Sub::$_dataを参照できる。
	}
}

$base = new Base();
$base->showData();	// 1

$sub = new Sub();
$sub->showData();	// 2

確かに意図通りですが、Baseを継承する全てのクラスで、構文としては全く同一のshowData()を再定義しなければならず、継承のメリットが失われます。

PHP 5.3ではこの問題を解決するための遅延静的バインディングが導入されました。「遅延」というのはコンパイル時ではなく、ランタイムにという意味です。
“self"の代わりに"static"キーワードを使うことで、"static"の指すクラス名がランタイムに解決されます。

class Base
{
	static protected $_data = 1;

	public function showData()
	{
		echo static::$_data."\n";		// この"static"はランタイム時にクラス名が解決される。
	}
}

class Sub extends Base
{
	static protected $_data = 2;
}

$base = new Base();
$base->showData();	// 1

$sub = new Sub();
$sub->showData();	// Base::showData()が呼び出されて、static::$_data が Sub::$_dataとして解決されるため、2が表示される。

継承を前提としたフレームワークを作成する場合において、この問題は深刻な問題でした。PHP 5.3で遅延静的バインディングが導入されたことで、より本格的なフレームワークを構築できるもののと期待しています。