レベル: 初級 John Mertic, Software Engineer, SugarCRM
2008年 12月 09日 この「PHP V5.3 では何が新しいのか」シリーズの記事では、2008年の末までにリリースされる予定の PHP V5.3 の、非常に興味がそそられる新しい機能について説明します。第 1 回では PHP V5.3 でのオブジェクト指向プログラミングに関する変更とオブジェクトの処理方法に関する変更を説明しました。第 2 回ではクロージャーとラムダ関数について説明します。クロージャーとラムダ関数を利用すると使い捨て関数を容易に定義することができ、それらを多くのコンテキストで使用できるため、プログラミングが容易になります。
クロージャーとラムダ関数の概念は決して新しいものではなく、どちらも関数型プログラミングの世界から来たものです。関数型プログラミングは、コマンドの実行よりも式の評価に焦点を当てるプログラミング・スタイルです。これらの式は関数を使って作成され、関数を組み合わせることで求める結果を得ます。このプログラミング・スタイルは学術的な世界でよく使われますが、人工知能や数学の分野にも見られ、また Erlang や Haskell、Scheme などの言語による商用アプリケーションにも見られます。
クロージャーは元々、最も有名な関数型プログラミング言語の 1 つである Scheme の一部として 1960年代に開発されました。ラムダ関数とクロージャーが頻繁に登場する言語としては、関数をファーストクラスの値として扱える言語 (つまり関数がオンザフライで作成され、パラメーターとして他の言語に渡される言語) があります。
クロージャーが最初に開発された時以降、その当時以来、クロージャーとラムダ関数は関数型プログラミングの世界だけではなく、JavaScript や Python、Ruby などの言語にも採用されるようになりました。JavaScript はクロージャーとラムダ関数をサポートする言語の中で最も有名なものです。実際、JavaScript ではクロージャーとラムダ関数を、オブジェクト指向プログラミングをサポートする手段として使用しています (関数が他の関数の中にネストされ、プライベート・メンバーとして動作します)。リスト 1 は JavaScript でクロージャーがどのように使用されるかを示す例です。
リスト 1. クロージャーを使って作成された JavaScript オブジェクト
var Example = function()
{
this.public = function()
{
return "This is a public method";
};
var private = function()
{
return "This is a private method";
};
};
Example.public() // returns "This is a public method"
Example.private() // error - doesn't work
|
リスト 1 を見るとわかるように、Example オブジェクトのメンバー関数はクロージャーとして定義されています。private メソッドのスコープはローカル変数であるため (対照的に public メソッドは this キーワードを使って Example オブジェクトに接続されます)、外の世界からは見えません。
ここまで、クロージャーとラムダ関数の概念の由来を歴史的な面から説明したので、次は PHP でのラムダ関数について調べてみましょう。ラムダ関数の概念はクロージャーの基礎をなしており、PHP に既に用意されている create_function() 関数を使うよりも、ラムダ関数を使った方がずっと適切にオンザフライで関数を作成することができます。
ラムダ関数
ラムダ関数 (「匿名関数」と呼ばれることもよくあります) は単なる使い捨ての関数であり、いつでも定義することができ、通常は変数にバインドされています。ラムダ関数そのものは、その関数を定義している変数のスコープ内にのみ存在するため、その変数のスコープ外ではその関数もスコープ外となります。ラムダ関数の概念は 1930年代の数学の研究から生まれました。ラムダ関数はラムダ計算法として知られ、関数の定義や応用、また再帰の概念を研究するために作られました。ラムダ計算法の研究成果を活用することによって Lisp や Scheme などの関数型プログラミング言語が開発されたのです。
ラムダ関数を使うと便利な状況はいくつもありますが、その好例がコールバック関数を受け付ける多くの PHP 関数の場合です。そうした関数の 1 つに array_map() があります。array_map() を使うと、配列をウォークスルーし、その配列の各要素にコールバック関数を適用することができます。初期のバージョンの PHP では、こうした関数の最大の問題として、簡潔な方法でコールバック関数を定義することができず、以下の 3 つの方法のいずれかを使うしかありませんでした。
- コードのどこか別の場所でコールバック関数を定義し、その関数を使う方法。この方法は呼び出しの実装の一部を別の場所に移動することになるため、あまり美しい方法ではありません。この方法では、この関数を他で使う予定がない場合には特に、コードが読みにくくなり、管理もしにくくなります。
- 同じコード・ブロックの中でコールバック関数を定義し、ただしコールバック関数に名前を付ける方法。こうするとコードが分散することはなくなりますが、名前空間の衝突を避けるためにコールバック関数の定義の周辺に
if ブロックを追加する必要があります。リスト 2 はこの方法の例を示しています。
リスト 2. 同じコード・ブロックの中で名前付きのコールバックを定義する
function quoteWords()
{
if (!function_exists ('quoteWordsHelper')) {
function quoteWordsHelper($string) {
return preg_replace('/(\w)/','"$1"',$string);
}
}
return array_map('quoteWordsHelper', $text);
} |
- V4 以来 PHP の一部となっている
create_function() を使って実行時に関数を作成する方法。機能的には、この方法で目的を果たすことはできますが、この方法には欠点がいくつかあります。大きな欠点の 1 つは、この関数が実行時にコンパイルされ、コンパイル時にコンパイルされるのではない点です。そのためオペコード・キャッシュにこの関数を入れる、ということができません。またこの方法は構文的にも見苦しく、大部分の IDE で利用可能なストリング強調機能を活用することができません。
コールバック関数を受け付ける関数が強力であっても、非常に見苦しい方法を使わない限り、1 度限りのコールバック関数を実行するための適切な方法がありません。しかし PHP V5.3 では、ラムダ関数を使うことで上の例をずっと簡潔な方法で行うことができます。
リスト 3. コールバックにラムダ関数を使った quoteWords()
function quoteWords()
{
return array_map('quoteWordsHelper',
function ($string) {
return preg_replace('/(\w)/','"$1"',$string);
});
}
|
これを見るとわかるように、これらの関数を定義するための構文はずっと簡潔になり、しかもオペコード・キャッシュを使ってパフォーマンスを最適化することができます。またコードの読みやすさも改善され、ストリング強調機能も利用することができます。ではこれらを基に、PHP でのクロージャーの使い方を学びましょう。
クロージャー
ラムダ関数そのものによって、これまでなかったものが新たに大量に追加されるわけではありません。上で説明したように、(構文が見苦しく、パフォーマンスが理想的ではなくなりますが) すべてのことはcreate_function() を使えば可能です。しかしラムダ関数は何と言っても使い捨て関数であり、何の状態も保持しないため、できることは限られます。そこでクロージャーが登場し、ラムダ関数を次のレベルにまで高めるのです。
クロージャーは、そのクロージャー独自の環境の中で評価される関数であり、バインドされた変数を 1 つ以上持っています (この関数を呼び出すと、それらの変数にアクセスすることができます)。クロージャーは、元々はいくつかの概念から構成される関数型プログラミングの世界の概念です。クロージャーはラムダ関数と似ていますが、クロージャーが定義されている環境の外部から変数を操作できるという意味で、ラムダ関数よりも高度な機能を持っています。
では、PHP でクロージャーを定義する方法を調べてみましょう。リスト 4 は外部の環境から変数をインポートして単純に画面に出力するクロージャーの例を示しています。
リスト 4. 単純なクロージャーの例
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };
$closure();
Output:
Hello World!
|
外部の環境からインポートされる変数は、クロージャー関数を定義する use 節の中で規定されています。デフォルトで、変数は値で渡されます。つまりクロージャー関数の定義の中で渡される値を更新した場合、外部の値は更新されません。しかし & 演算子を変数の前に付けると (& 演算子は参照で渡すことを示すために関数定義の中で使われます)、外部の値も更新することができます。リスト 5 はこの例を示しています。
リスト 5. 参照で変数を渡すクロージャー
$x = 1
$closure = function() use (&$x) { ++$x; }
echo $x . "\n";
$closure();
echo $x . "\n";
$closure();
echo $x . "\n";
Output:
1
2
3
|
これを見ると、クロージャーが外部変数 $x を使っており、クロージャーが呼び出されるたびにこの変数をインクリメントしていることがわかります。use 節の中では値渡しの変数と参照渡しの変数を容易に混在させることができ、それらの変数は何も問題なく処理されます。また、直接クロージャーを返す関数を定義することもできます (リスト 6)。この場合のクロージャーの存続期間は、実際にはこのクロージャーを定義したメソッドよりも長くなります。
リスト 6. 関数によって返されるクロージャー
function getAppender($baseString)
{
return function($appendString) use ($baseString) { return $baseString .
$appendString; };
}
|
クロージャーとオブジェクト
クロージャーは手続き型プログラミングのツールとして便利なだけではなく、オブジェクト指向プログラミングのツールとしても便利です。オブジェクト指向プログラミングでクロージャーを使う理由は、クラスの外でクロージャーを使う理由と同じです。つまり特定の関数を小さなスコープ内にバインドするためにクロージャーを使うのです。クロージャーの使い方は、オブジェクトの外で使用する場合と同じように、オブジェクトの中でも容易なのです。
オブジェクトの中でクロージャーを定義すると、1 つ便利なこととして、クロージャーが $this 変数を使ってオブジェクトにフル・アクセスすることができ、明示的にオブジェクトをインポートする必要がなくなります。リスト 7 はこれを示しています。
リスト 7. オブジェクトの中にあるクロージャー
class Dog
{
private $_name;
protected $_color;
public function __construct($name, $color)
{
$this->_name = $name;
$this->_color = $color;
}
public function greet($greeting)
{
return function() use ($greeting) {
echo "$greeting, I am a {$this->_color} dog named
{$this->_name}.";
};
}
}
$dog = new Dog("Rover","red");
$dog->greet("Hello");
Output:
Hello, I am a red dog named Rover.
|
ここでは、greet() メソッドの中で定義されたクロージャーの中で、greet() メソッドに与えられた greeting (挨拶) を明示的に使っています。また、コンストラクターの中で渡されてオブジェクトの中に保存されている犬の色と名前もクロージャーの中で取得しています。
クラスの中で定義されるクロージャーは基本的に、オブジェクトの外部で定義されるクロージャーと同じです。唯一の違いは、$this 変数を使って自動的にオブジェクトをインポートする点のみです。クロージャーを static と定義すれば、この動作を無効にすることができます。
リスト 8. 静的なクロージャー
class House
{
public function paint($color)
{
return static function() use ($color) { echo "Painting the
house $color...."; };
}
}
$house = new House();
$house->paint('red');
Output:
Painting the house red....
|
この例はリスト 5 で定義された Dog クラスと似ています。大きな違いはクロージャーの中でオブジェクトのプロパティーを使わないことですが、これはクロージャーが static と定義されているためです。
オブジェクトの中で非静的なクロージャーではなく、静的なクロージャーを使うことによる大きなメリットは、メモリーを節約できることです。クロージャーの中にオブジェクトをインポートする必要がなければ、メモリーを大幅に節約することができるのです (インポート機能が必要ないクロージャーが多い場合には、特に大幅な節約が可能です)。
オブジェクトに関してもう 1 つ便利なことは、__invoke() というマジック・メソッドが使えることです。このマジック・メソッドを使うと、オブジェクトそのものをクロージャーとして呼び出すことができます。このメソッドを定義すると、このメソッドが定義されているコンテキストでそのオブジェクトが呼び出された場合に、このメソッドが使われます。リスト 9 はこれを示しています。
リスト 9. __invoke() メソッドを使う
class Dog
{
public function __invoke()
{
echo "I am a dog!";
}
}
$dog = new Dog();
$dog();
|
リスト 9 に示すオブジェクト参照を変数として呼び出すと、自動的に __invoke() マジック・メソッドが呼び出され、クラスそのものがクロージャーとして動作します。
クロージャーは、手続き型のコードだけではなくオブジェクト指向のコードともうまく統合することができます。では、PHP の強力なリフレクション API とクロージャーがどのようにやり取りするのかを調べてみましょう。
クロージャーとリフレクション
PHP には、クラスやインターフェース、関数、メソッドなどをリバース・エンジニアリングすることができる便利なリフレクション API があります。設計上、クロージャーは匿名関数ですが、これはリフレクション API の中にはクロージャーが現れないということです。
しかし、PHP では新しい getClosure() メソッドが ReflectionMethod クラスと ReflectionFunction クラスに追加されており、指定された関数やメソッドから動的にクロージャーを作成することができます。この場合、getClosure() メソッドはマクロのように動作します。つまりクロージャーを介して関数のメソッドを呼び出すと、その関数が定義されたコンテキストで関数の呼び出しが行われます。リスト 10 は、この動作を示しています。
リスト 10. getClosure() メソッドを使う
class Counter
{
private $x;
public function __construct()
{
$this->x = 0;
}
public function increment()
{
$this->x++;
}
public function currentValue()
{
echo $this->x . "\n";
}
}
$class = new ReflectionClass('Counter');
$method = $class->getMethod('currentValue');
$closure = $method->getClosure()
$closure();
$class->increment();
$closure();
Output:
0
1
|
この方法による副産物の 1 つとして興味深い点は、クラスの private メンバーや protected メンバーにクロージャーを介してアクセスできる点です。これはクラスのユニット・テストをする際に便利です。リスト 11 は、クラスの private メソッドにアクセスする例を示しています。
リスト 11. クラスの private メソッドにアクセスする
class Example
{
....
private static function secret()
{
echo "I'm an method that's hiding!";
}
...
}
$class = new ReflectionClass('Example');
$method = $class->getMethod('secret');
$closure = $method->getClosure()
$closure();
Output:
I'm an method that's hiding!
|
また、リフレクション API を使うとクロージャー自体をイントロスペクトすることができます (リスト 12)。そのためには単にクロージャーへの変数参照を ReflectionMethod クラスのコンストラクターに渡せばよいだけです。
リスト 12. リフレクション API を使ってクロージャーをイントロスペクトする
$closure = function ($x, $y = 1) {};
$m = new ReflectionMethod($closure);
Reflection::export ($m);
Output:
Method [ <internal> public method __invoke ] {
- Parameters [2] {
Parameter #0 [ <required> $x ]
Parameter #1 [ <optional> $y ]
}
}
|
後方互換性の点から 1 つ注意しなければならないことは、(PHP V5.3 では) Closure というクラス名は PHP エンジンによってクロージャーを保存するための名前として予約されているため、Closure という名前のクラスをすべてリネームする必要があるという点です。
以上のように、リフレクション API では既存の関数やメソッドから動的にクロージャーを作成できる形で、クロージャーが強力にサポートされています。また リフレクション API を使うことで、通常の関数の場合とまったく同じようにクロージャーをイントロスペクトすることもできます。
なぜクロージャーを使う必要があるのか
ラムダ関数の例で見たように、クロージャーを使うことが明らかに適切な場合の 1 つとして、コールバック関数をパラメーターとして受け付ける PHP 関数が挙げられます。その一方でクロージャーは、ロジックをそのロジック独自のスコープ内でカプセル化する必要がある、いかなる状況でも役に立ちます。その一例が、古いコードをリファクタリングして単純化し、読みやすくする場合です。下記の例では、ある SQL クエリーを実行する際に logger が使われています。
リスト 13. SQL クエリーをログに記録するコード
$db = mysqli_connect("server","user","pass");
Logger::log('debug','database','Connected to database');
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails');
Logger::log('debug','database','Insert Hammer into to parts table');
$db->query('insert into parts (part, description) values
('Drill','Puts holes in wood');
Logger::log('debug','database','Insert Drill into to parts table');
$db->query('insert into parts (part, description) values ('Saw','Cuts wood');
Logger::log('debug','database','Insert Saw into to parts table');
|
リスト 13 では、何度も似たような処理が繰り返されている点が目立ちます。Logger::log() の呼び出しでは、いずれの場合も最初の 2 つの引数は同じ値が渡されています。このコードを改善するためには、このメソッド呼び出しをクロージャーの中に入れ、そのクロージャーを呼び出すようにします。そのように変更したコードがリスト 14 です。
リスト 14. SQL クエリーをログに記録するコードをリファクタリングしたもの
$logdb = function ($string) { Logger::log('debug','database',$string); };
$db = mysqli_connect("server","user","pass");
$logdb('Connected to database');
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails');
$logdb('Insert Hammer into to parts table');
$db->query('insert into parts (part, description) values
('Drill','Puts holes in wood');
$logdb('Insert Drill into to parts table');
$db->query('insert into parts (part, description) values ('Saw','Cuts wood');
$logdb('Insert Saw into to parts table');
|
こうすると、1 ヵ所だけ変更すればよくなるため、コードの見た目がすっきりとしただけではなく、SQL クエリーを記録するためのログ・レベルも変更しやすくなっています。
まとめ
この記事では、PHP V5.3 コードの中での関数型プログラミングの構成体として、クロージャーがいかに便利なものかを説明しました。またラムダ関数と、クロージャーがラムダ関数よりも優れている点について説明しました。そしてオブジェクトとクロージャーの相性が非常によいことを、オブジェクト指向コードの中でのクロージャーの特殊な処理を通して説明しました。さらに、動的なクロージャーを作成したり、既存のクロージャーをイントロスペクトしたりするにはリフレクション API を使うのが非常に適しているということも説明しました。
参考文献 学ぶために
製品や技術を入手するために
- 皆さんの次期オープンソース開発プロジェクトを IBM ソフトウェアの試用版を使って革新してください。ダウンロード、あるいは DVD で入手することができます。
- IBM 製品の評価版をダウンロードし、DB2® や Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品をお試しください。
議論するために
著者について  | |  | John Mertic は Kent State University をコンピューター・サイエンスの学位で卒業し、現在は SugarCRM のソフトウェア・エンジニアです。彼は多くのオープンソース・プロジェクトに貢献しており、そのうち最も重要なプロジェクトは PHP プロジェクトです。彼は PHP の Windows インストーラーの作成者でありメンテナーでもあります。 |
記事の評価
|