PHP 5.3 のラムダとクロージャーを活用する

ラムダとクロージャーを使って優れた開発者になる

この記事では、PHP 5.3 のラムダとクロージャーの使い方について説明します。なぜラムダとクロージャーを使うことを検討する必要があるのか、またラムダとクロージャーを使うことでどのようにしてコードの複雑さを軽減できるのか、そして最後に、クロージャーのレキシカル・スコープ変数の動作について学びましょう。

Don Denoncourt, Author, Consultant

Don Denoncourt は Java 技術、Groovy、Grails、PHP を専門とするフリーのコンサルタント、トレーナー、メンターであり、著作者でもあります。彼の連絡先は dondenoncourt@gmail.com です。



2010年 12月 07日

ここ数年、プログラミング言語に関して多くの活動が行われてきました。開発者達は Ruby、Groovy、Clojure などの言語を研究しており、その目的は単に開発者自身の転職を有利にするためではなく、彼らの考え方の幅を広げることにもあります。これらの言語の持つ、非常に興味が惹かれる特徴のうちの 2 つ、ラムダとクロージャーが PHP 5.3 で利用できるようになりました。そして、ラムダとクロージャーの優れた実装があるもう 1 つの言語、JavaScript は既にほとんどの PHP 開発者が使用しています。

ラムダとクロージャーが追加されたことにより、PHP は関数型プログラミング言語の要件を満たせるようになりました。具体的には、関数型プログラミング言語は、高階関数とファーストクラス関数を備えている必要があります (ファーストクラス関数と高階関数については、後ほど説明します)。この関数型プログラミングへの関心はこの 2、3 年で急速に高まりました。なぜでしょうか?理由は簡単です。計算処理能力が 2 年ごとに倍になるというムーアの法則は、実際にはもはや真実とは言えないからです。ムーアの法則を真実と言いにくくなった理由は、2、3 年まではシングルコア・プロセッサーの計算処理能力は増大していましたが、コンピューター・メーカーは今や、マルチコア・プロセッサーを作ることでしか計算処理能力を増大させることができなくなっているためです。そのため、デュアルコア、クアッドコア、ヘキサコアのプロセッサーが目につくようになったのです。コンピューター・メーカーにとってマルチコア・プロセッサーの製造は比較的容易ですが、スタック・ベースのステートフルな言語は必ずしもプロセッサーの追加によるメリットを活用できるわけではありません。しかし関数型プログラミング言語は、プロセッサーを追加することによるメリットを活用することができます。

いったいラムダとクロージャーとは何なのでしょう。なぜ PHP 開発者はラムダとクロージャーを使えることで喜ぶのでしょう?この記事では、ラムダとクロージャーをどのような場合にどのように使用するかについて説明し、クロージャーの動作の特徴の一部を紹介します。また、そしてラムダとクロージャーを活用するために使えるコード・イディオムをいくつか紹介します。

ラムダの簡単な紹介

ラムダという用語はラムダ計算に由来しています。ラムダ計算は関数の定義、適用、再帰のための形式体系として、1930年代に Alonzo Church によって紹介されました。Church 氏に関する興味深い事実の 1 つは、彼のスタンフォード大学での同僚にはチューリング・マシンで有名な Alan Turing がいたことです。この 2 人によって考案された概念は現代のプログラミング言語の基礎となりました。

ただし、ラムダ計算が複雑だからといって、ラムダを使用することに尻込みする必要はありません。ラムダは実際には無名関数でしかありません。例えば、以下の関数定義はラムダです。

$horse = function() {return "with no name";};
print $horse();

この関数は変数に割り当てられているため、セミコロン (;) による終了記号が必要なことに注意してください。PHP 5.3 よりも前のバージョンでは、以下のようにして標準関数を作成したはずです。

function horse() {return "Secretariat";}
print horse();

これを見て皆さんは、「だからどうだと言うのですか?どちらの例もコードの行数は同じであり、しかも、無名関数の方は基本的に $horse 変数の中に名前を持っていたではありませんか。」と言うかもしれません。実は、$horse は関数への参照であり、関数の名前ではありません。しかしラムダによる真のメリットがもたらされるのは、オンザフライで関数を作成する場合で、しかも変数参照が必要ない場合です。例えば以下のような場合です。

$a=array('PA'=>"Pennsylvania",'VA'=>"Virginia",'TX'=>"Texas");
print_r(array_filter($a,
                     function($v){return $v==="Virginia";}
                    )
);

この場合、ラムダ関数は名前がないままです。この関数は array_filter 関数のパラメーターとしてインラインで作成されています。関数やメソッドのパラメーターとしてオンザフライでラムダを作成する使い方は、ラムダの基本的な使い方です。他の関数のパラメーターとして渡される関数を表すために、通常はコールバックという言葉が使われることに注意してください。コールバックはイベント処理アーキテクチャーの中で頻繁に使われ、そこでは後で呼び出されるコードを指定します。

関数型プログラミングでは、ラムダはファーストクラス関数と呼ばれます。これはラムダが以下の特徴を持っているためです。

  • パラメーターとして渡されます。
  • 実行時に作成、使用されます。
  • 関数から返されます。
  • 変数に割り当てられます。

実際には PHP V4.0 以降、以下のように create_function を使うことで PHP でもラムダを利用できるようになっています。

$horse = create_function('', 'return "with no name";');
print $horse();

しかし create_function の構文はあまり簡潔ではありません。例えば、コードは引用符で囲まれた長い文字列になっています。そのため、PHP エディターを使えば簡単になるというわけではなく、このコード・ストリングをエスケープすることを忘れてはなりません。さらに、create_function のコードは実行時にコンパイルされるので、コンパイルされた関数をキャッシュすることはできません。


ラムダをアサートする

ここではラムダを使った簡単な例を紹介します (この記事のこれから先では、この例を使用して説明します)。この例では、PHP に関する記事にコード・スニペットが含まれている場合、通常は echo または print を使って結果を表示するコードに続いて、想定される出力を説明したコメントがあることが要件になっています。例えば以下のコードを考えてみましょう。

function horse() {return "Secretariat";}
print horse(); // should list: Secretariat

記事を読んでいる人は、このコードを実行して結果を表示しない限り、コードに含められた暗黙的な想定事項を確認することができません。しかし私が以下のコード行を読む場合、コードとコメントの間を行ったり来たりしなければならず、目が回ってしまいそうです。

print horse(); // should list: Secretariat

この記事のコード・スニペットを読みやすいものにするためには、PHP の assert を使います。例えば以下のようにします。

assert(horse() == "Secretariat");

PHP のアサーションの構成では、assert が呼び出されると必ず呼び出される関数を指定することができます。もちろん、私は以下のようにラムダを使いました。

assert_options(ASSERT_CALLBACK, 
	function ($script, $line) {
		echo "assert failed file:$script line:$line ";
        }
);

アサーションが失敗すると、以下のようにスクリプトの名前と行番号が出力されます。

assert failed file:/var/www/closure_behavior.php line:13 PHP Warning: 
assert(): Assertion failed in /var/www/closure_behavior.php on line 13

アサーションでラムダを使う方法に代わる方法は、以下のようになります。

function assertCallback($script, $line) {
	echo "assert failed file:$script line:$line ";}
);
assert_options(ASSERT_CALLBACK, assertCallback);

「なぜラムダを使った方が優れているのでしょうか?」と皆さんは尋ねるかもしれません。その理由を説明しましょう。ラムダを使わない場合に定義している関数は 1 ヶ所でしか使われないことに注意してください。コードは作成する機会よりも読む機会の方がはるかに多いため、コードの単純化が重要であることを理解する必要があります。名前付きの関数を定義すると、そのコードを読む人は、その関数の名前を頭の中の「キャッシュ」に入れようとします。そのため、プログラムを理解する作業が必要以上に複雑になります。1 度しか使われずに終わる関数であれば、ラムダを使ってコードを単純化するようにしてください。

この記事のこれから先で assert を目にしたら、そのアサーションは必ず真です。


クロージャー入門

ラムダが無名関数にすぎないのであれば、クロージャーはラムダに少しコンテキストが追加された程度のものにすぎません。つまりクロージャーは無名関数であり、作成されると、その無名関数を作成したコードのスコープの変数の値を、その無名関数自体に追加します。変数は use キーワードによってクロージャーにバインドされます。例えば以下のような変数があるとします。

$states=array('PA'=>"Pennsylvania",'VA'=>"Virginia",'TX'=>"Texas");
$st = 'Texas';

この場合、以下の例のように use キーワードを使うことで $st 変数をクロージャーにバインドすることができます。

$tx = array_filter($states, function($v) use($st) {return $v==$st;});

すると assert は成功します。

assert(array_keys($tx) == array('TX'));

上記の例からわかるように、クロージャーは指定された変数に対して「閉じられた」関数です。上記の例はクロージャーの使い方の好例です。この例では、PHP に 18 種類ある、関数を引数に取る標準的な配列関数 (array_walkusortarray_app など) の 1 つを使っています。クロージャーを使用することで、これらのユーティリティー関数の使い道を拡大することができます。クロージャーが登場するまでは、渡された関数の実装は、その関数引数のコンテキストに限定されていました。(実際にはグローバル・スコープを使用することができたので、この表現は必ずしも正しくはありません。ただし一般的な通念としては、グローバル・スコープを使用することは良いプログラミングの習慣ではありません。)

PHP の array_filter は関数型プログラミングで高階関数として知られているものの一例です。(数学とコンピューター・サイエンスの両方での) 高階関数は、関数を入力に取る関数であるか、関数を出力する関数です。ラムダとクロージャーに慣れてくると、独自の高階関数を作成することによってコードを削減できるようになり、また読みやすくて再利用可能なコードを作成できるようになります。

クロージャーは実際には、内部で新しい Closure クラスによって実装される新しいデータ型です。クロージャーについて次のような見方をすることもできます。「オブジェクト」とはメソッドを持つデータであり、そのメソッドによってデータが操作されるのであれば、「クロージャー」はデータを持つ関数であり、そのデータは関数にバインドされたデータです。


クロージャーの動作

クロージャーの use キーワードの構文は容易に理解することができます。ただし use キーワードを使ってクロージャーに渡された変数の動作を理解するためには、クロージャーを実際に少し試してみる必要があります。読者の皆さんは変数のスコープについて十分に理解していることと思います。適切に作成されたコードでは、変数はその変数が必要とされるスコープでのみ定義されています。そのことが念頭にあると、以下のコードの最後にあるアサーションは失敗すると予想することでしょう。

function getNameFunc() {
     $string = 'Denoncourt';
     return function() use ($string) { return $string; };
}
$name = getNameFunc();
$name();
assert ($name() == 'Denoncourt');

しかし、$string がスコープ外で使われているにもかかわらず、このアサーションは成功します。これはクロージャーの use キーワードで指定された変数がレキシカル・スコープだからです。もう少し説明しましょう。通常、PHP の変数のスコープは動的です。つまり変数はスタックに置かれます。これらの変数は、その変数を包含する関数の処理が完了するとスタックからポップされます。レキシカル・スコープの変数は、それらの変数が定義されたローカル環境を参照します。use キーワードによって指定された変数は、スタックに置かれる代わりに静的なストレージに保管されます。

クロージャーの強力な機能として、どこでクロージャーが定義されたかにかかわらず、そのクロージャーを後でどこからでも呼び出すことができ、そして実行することができますが、use によって指定された変数のスコープは有効なままです。ただし、PHP で実装されているレキシカル・スコープでは、use キーワードで指定された変数を静的ストレージ領域にコピーすることに注意してください。大きなオブジェクトを使用する場合には、変数への参照を渡すことで使用するストレージを減らすことができます。例えば以下のようにします。

$string = 'Denoncourt';
$changeName = function() use(&$string) {$string = 'Smith';};
$changeName();
assert ($string == 'Smith');

上の例からもわかるように、参照によって use 変数を渡す方法は、クロージャーによって値を変更するための 1 つの方法です。

「ちょっと待ってください。クロージャーの use オプションに渡した変数を、なぜ単純に関数引数として渡さないのですか?」と言う人がいるかもしれません。ここでレキシカル・スコープの変数の強力さが発揮されます。関数引数の場合、渡されたパラメーターはスタック・スコープでなければなりません。しかし use 変数の場合、クロージャーは基本的に変数のコンテキストを記憶します。リスト 1 の例を考えてみてみましょう。リスト 1 ではクロージャーはクラスの中で定義され、オブジェクト変数がスコープ外になってからクロージャーが呼び出されます。

リスト 1. 変数がスコープ外になってからクロージャーを呼び出す
class Person {
	var $first;
	var $last;
	public function sayHello() {
		$that = $this;
		return function() use ($that) { 
			return $that->first.' '.$that->last; 
        	};
	}
}
function stackFunc1() {
	$me = new Person();
	$me->first = 'Don';
	$me->last = 'Denoncourt';
	return $me->sayHello();
}
$sayHi1 = stackFunc1(); 
function stackFunc2() {
	$me = new Person();
	$me->first = 'Sue';
	$me->last = 'Swartzbaugh';
	return $me->sayHello();
}
$sayHi2 = stackFunc2(); 
assert($sayHi1() == 'Don Denoncourt');
assert($sayHi2() == 'Sue Swartzbaugh');

Person::sayHello メソッドを実装すると特別な変数 $this のエイリアスが作成され、そのエイリアスがクロージャーの定義に渡されることに注意してください。これはクロージャーが内部で Closure クラスによって実装されており、Closure クラスが独自の $this コンテキストを持っているためです。実際、$thisuse キーワードに渡すと、以下のようなエラーが発生します。

PHP Fatal error:  Cannot use $this as lexical variable

関数を返すクラス・メソッドの使い方は、ほとんど無限です。クロージャーの創造的な使い方の例として、Pawel Turlejski によるユーティリティー・クラスを調べてみてください。このユーティリティー・クラスには Groovy ライクな配列メソッドが用意されています (「参考文献」を参照)。

クロージャーを使用すると、動的にメソッドを作成できるようにクラスをコーディングすることさえできます。リスト 2 はその一例です。

リスト 2. 動的にメソッドを作成するコードの例
class Person {
    var $first;
    public function __call($method, $args) {
        return call_user_func_array( $this->$method, $args);
    }
}
$me = new Person();
$me->first = 'Don';
$me->sayGoodbye = function() use ($me) {
    return 'Bye '.$me->first;
};
assert($me->sayGoodbye() == 'Bye Don');

この場合も、たとえオブジェクトがスコープ外になってもクロージャーは適切に呼び出されます。例えば上記のクロージャーは、たとえ PHP の unset メソッドを使って $me オブジェクト参照を削除した場合でも実行されます。

$bye = $me->sayGoodbye;
unset($me);
assert($bye() == 'Bye Don');

高階関数に渡された引数が、その後クロージャーの use キーワードに引数として渡された場合も、その引数の値は保持されます。

$greeting = function($greet) {
	return function($name) use($greet) {
		return $greet.' '.$name;
	};
};
$hi=$greeting('Yoh');
assert($hi('Don') == "Yoh Don");
assert($hi('Sue') == "Yoh Sue");

クラスを呼び出す

先ほど、クロージャーは内部で Closure というクラス (この Closure クラスは PHP の今後のバージョンでは変更される可能性があるため、開発の際に Closure クラスを直接使用してはなりません) によって実装されていることに触れましたが、このクロージャーの実装には、Closure クラスの内部にあって利用できる機能が 1 つあります。それは新しい __invoke メソッドを使用する機能です。__invoke マジック・メソッドを使用することで、クラスを呼び出し可能にすることができます。

class Invoker {
	public function __invoke() {return __METHOD__;}
}
$obj = new Invoker;
assert ($obj() == 'Invoker::__invoke');

__invoke の実装を含むクラスがインスタンス化されると、オブジェクトの名前に続いて括弧 (()) を指定することで、そのクラスを呼び出すことができます。このタイプのオブジェクトはファンクターまたは関数オブジェクトと呼ばれます。


まとめ

アプリケーションにラムダとクロージャーを使用するメリットはいくつかあります。正式な関数定義を無名関数で置き換えることにより、コードを単純化することができます。ユーティリティー関数やメソッドを高階関数として作成すれば、アプリケーションが堅牢になります。また、クロージャーとレキシカル・スコープを理解することにより、新しい開発パターンやイディオムを考えられるようになります。さらに、関数型プログラミングに関して学ぶことにより、開発者としての幅を広げることができます。

PHP は関数型プログラミング言語として使うべきなのでしょうか。一般的に言えば、答えは「ノー」です。PHP は Web アプリケーション開発のための簡潔で単純な言語として進化してきました。純粋に関数型のセマンティクスを使って PHP ベースの Web アプリケーションを開発することは、PHP の本来の趣旨に反しています。いずれにせよ、ラムダとクロージャーに関する PHP の構文は少しスマートさに欠けています (Ruby、Groovy、Clojure などの言語と比較した場合はなおさらです)。ただし関数型プログラミングの概念を PHP アプリケーションに適用できるシナリオがあり、そうしたシナリオでは、関数型プログラミングの概念によってコードを単純かつ簡潔で理解しやすいものにすることができます。


ダウンロード

内容ファイル名サイズ
Sample PHP scripts for this articleos-php-lambda-Closure_example.zip4KB

参考文献

学ぶために

製品や技術を入手するために

  • 皆さんの次期オープンソース開発プロジェクトを IBM ソフトウェアの試用版を使って革新してください。ダウンロード、あるいは DVD で入手することができます。
  • IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=617666
ArticleTitle=PHP 5.3 のラムダとクロージャーを活用する
publish-date=12072010