PHPでは無名関数とクロージャは同じものとして扱っています。無名関数の返すオブジェクトはClosureクラス。
これを見てもちがいはありません。
PHP公式ドキュメントを見ても、無名関数の説明の大半はクロージャについてで、無名関数のことを『クロージャ』と表現する箇所が多いです。
クロージャの定義は本来ちがうはずだが。
クロージャは実行可能な関数を変数に代入する、別の関数のパラメータや戻り値で渡すときに使います。
クロージャはPHP独自の仕様ではない。
JavaScriptやPythonにもある。
JavaSciprtは多用するので有名。
そのとき渡す関数は、処理が実行できる特殊な型のデータ(オブジェクト)に変換されます。
この関数オブジェクトを作るのが無名関数。関数名がいらないのは変数名で代用できるから。
無名関数のことをクロージャとも言ったりします。PHP公式ドキュメントでも無名関数の説明のど頭に書いてある。
無名関数はクロージャとも呼ばれ、 関数名を指定せずに関数を作成できるようにするものです。
PHP公式ドキュメント - 無名関数
その後の説明でも無名関数のことをクロージャと何度も表現しています。でも、ちゃんとしたクロージャの定義は無名関数とイコールではありません。
しかし、PHPはドキュメントの冒頭でも言ってる通り、無名関数 = クロージャとしか見えない仕様になってます。
今回もそのつもりで話を進めます。
ただし、もう1回言いますが、クロージャの定義は本来はちがうということを頭に入れておいてください。
PHPの無名関数(クロージャ)
PHPの無名関数の定義はかんたんです。関数名を付けないだけ。
サンプルで確認しましょう。
<?php
// 通常の関数
function test($x, $y)
{
return $x + $y;
}
$result = test(1, 2);
var_dump($result);
// 無名関数
$anonymous = function ($x, $y) {
return $x + $y;
};
$result = $anonymous(1, 2);
var_dump($result);
int(3)
int(3)
通常の関数と無名関数の処理はまったく同じです。
無名関数では $anomymous 変数が間に挟まれてるのがポイント。
代入時の無名関数の閉じカッコは '};' でセミコロンが必要
無名関数は通常の名前付き関数と違って、変数に代入できる式として使います。
関数なんだけど式の一部でもある特殊なものなので、関数の閉じカッコ(})のあとに、式の終りを示す ';' が必要です。
式は =, ===, !===, <, >= などを使うこと。
数式をイメージすると分かりやすい。
また、関数コールも式。必要なら戻り値を = で代入するから。
式の最後はセミコロン(;)が必要。
関数やif, for などで使う中括弧({})は文の始まりと終わりを表す。
文は式や文の集合体。
if文、for文って言いますよね?
文は何も返さない処理の集まりのことで、関数も『文』は使わないが、戻り値がなければ文になる。
(戻り値を返さない戻り値の型はvoid。)
関数を文と呼ばないのは、条件によって戻り値を返すから。特殊な文とも言える。
文は中でしか使えない内部変数を定義できる。その変数の領域(文の領域)をスコープという。
クラスも文。メソッドも戻り値の型がvoidなら文中にある分割された子の文になる。
文の終わりにはセミコロンがいらない。
セミコロンがないとシンタックスエラー(文法エラー)になる。このエラーがしんどい。
<?php
$anonymous = function ($x, $y) {
return $x + $y;
}
$test = 1;
PHP Parse error: syntax error, unexpected '$test' (T_VARIABLE) in /home/vagrant/php-test.php on line 7
PHP Parse error: syntax error, unexpected variable "$test" in /home/vagrant/php-test.php on line 7
『$testは予期していない』と言われても...。
慣れてる人は直前にセミコロン(;)が無いのに気づくんですが、経験が足りないと意味不明。
しかも、無名でも関数は関数なので直前にセミコロンが要るというのにも気づきにくい。
また、無名関数がファイル内の最後の処理だとエラー内容が変わります。
PHP Parse error: syntax error, unexpected end of file in /home/vagrant/php-test.php on line 50
訳すと『予期せぬファイルの終了』。line 50 はファイルの最終行 + 1のこと。
これも普通の関数だと思ってるとセミコロンが必要なのに気づきません。
JavaScriptでは、セミコロンがあってもなくてもエラーにならないので余計に迷います。
この迷いに意外と時間がかかってしんどいし、迷った自分にイライラする。
精神衛生上よくありません。
Closureオブジェクトは()を付けて関数を実行する
もうひとつ注意が必要なのは、無名関数を代入した変数は関数の実行結果が入るわけでありません。
この時点ではまだ関数内の処理を実行していないから。
プログラム言語を問わず、この時点で変数に入っているのは関数オブジェクトです。PHPではClosureクラスが用意され、それを関数オブジェクトの型として使用します。
本当にそうなのか、デバッグしてみましょう。
$anonymous = function ($x, $y) {
return $x + $y;
};
var_dump($anonymous);
object(Closure)#1 (1) {
["parameter"]=>
array(2) {
["$x"]=>
string(10) "<required>"
["$y"]=>
string(10) "<required>"
}
}
Closureクラスインスタンスの特長は、なんといってもオブジェクトの後ろに '()' を付けられることでしょう。
これがエラーにならずに無名関数内の処理を実行します。
$result = $anonymous(1, 2);
var_dump($result);
int(3)
無名関数は処理を実行するのに一度オブジェクト生成を挟みます。ここが普通の関数とはちがう。
関数の戻り値を取得したつもりなのに、中身は関数オブジェクトだったなんてことも。
プログラムの記述で、関数名があるかないかの違いしかないのでよく間違えます。
(セミコロン(;)の有無もあるが。)
このへんはJavaScriptも同じなので、慣れてる人は大丈夫でしょうが。JSのサンプルコードはこちらにあるのでどうぞ。
PHP公式ドキュメント
クロージャが参照する上位スコープの変数は use() の指定が必要。
クロージャにはPHP独自の記述方法があります。クロージャは、定義しているスコープ(同じ層)の変数を参照できる。
function main() {
// クロージャが参照できる変数
$a = 3;
$b = 4;
$c = 5;
return function ($x, $y) {
return $x + $y;
};
}
というか本当はこれができることをクロージャと言うんだけど。
JavaScriptは、その変数をクロージャ内であたかも自分のものかのように使えるんですが、PHPはちょっとちがう。
上位の変数はクロージャを定義するときに use() を使って、パラメータっぽく受け取らないといけません。
$a = 3;
$b = 4;
$c = 5;
$anonymous = function ($x, $y) use ($a, $b, $c) {
return $x + $y - ($a + $b + $c) ;
};
$result = $anonymous(1, 2);
var_dump($result);
int(-9)
変数には別の無名関数を指定することも可能。
このへんは、関数内で名前付き関数を定義できるJavaScriptに比べて、PHPではできないのでそれほど複雑じゃありません。
JavaScriptの仕様は特殊で、名前付き関数も変数に代入できる。
ようは関数すべてが関数オブジェクトを生成する。
だから関数内で関数の定義ができる。
他のプログラミング言語ではなかなかない。普通は関数内で書けるのは式と関数を除く文だけ。
無名関数(クロージャ)は関数であり式でもあるから定義できる。
関数の型宣言は最後(use()があるときその後ろ)
PHP8から関数やパラメータ変数の型宣言をきちんとする方向に舵を切り始めました。その関数の型宣言は関数定義の最後です。
use() を入れたらその後ろでないといけません。
(型宣言は関数の開始括弧({)の直前。)
$anonymous = function ($x, $y) use ($a, $b, $c): int {
return $x + $y - ($a + $b + $c) ;
};
試しに型宣言のあとに use() を入れてみたらシンタックスエラー(文法エラー)になりました。
<?php
$a = 3;
$b = 4;
$c = 5;
// use()の前に型宣言を入れてみた
$anonymous = function ($x, $y): int use ($a, $b, $c) {
return $x + $y - ($a + $b + $c) ;
};
PHP Parse error: syntax error, unexpected 'use' (T_USE), expecting '{' in /home/vagrant/php-test.php on line 8
PHP Parse error: syntax error, unexpected token "use", expecting "{" in /home/vagrant/php-test.php on line 8
パラメータと同じ変数名は使えない
ここからはPHP7.1以降 use() で使えなくなったものです。
2022年11月にはPHP7.4がサポート終了するので、これからPHPを勉強しようという人、過去のプログラムを流用しないときは、できないことだと思ったほうがいい。
まず最初は、パラメータと同じ変数名は使えません。
<?php
$a = 3;
$b = 4;
// パラメータと同じものを入れてみた
$anonymous = function ($x, $y) use ($a, $b, $x): int {
return $x + $y - ($a + $b + $x) ;
};
PHP Fatal error: Cannot use lexical variable $x as a parameter name in /home/vagrant/php-test.php on line 7
『$x のパラメータ名はレキシカルな変数では使えない』。
レキシカルは『語彙(ごい)的』『辞書的』と訳され、そのままだと意味不明ですが、『文法的』と意訳すれば分かる。
ここでいうレキシカルは use() のことで、その変数名は use()ではNGだよ、ということ。
スーパーグローバル変数は使えない
2つ目は、スーパーグローバル変数は使えません。グローバルなんだからそのまま使えるっしょ? ってことだと思う。
<?php
// スーパーグローバルを入れてみた
$anonymous = function ($x, $y) use ($_SERVER): int {
return $x + $y;
};
PHP Fatal error: Cannot use auto-global as lexical variable in /home/vagrant/php-test.php on line 4
『グローバルはAuto(自動)だからレキシカルな変数では使えない』(筆者意訳)。
グローバル変数はそのままでも使えるから、わざわざ use() の文法を使わないよ、ということ。
PHP公式ドキュメント
クラス内での$thisは使えない
3つ目は、クラス内で定義する無名関数は、自クラスオブジェクトを表す $this を use() で渡せません。
これもクラス内なんだからそのまま $this を使えよってことだと思う。
わざわざ面倒なことをすな!ってことなのかな?
<?php
namespace Test;
class Test
{
private int $a = 1;
private int $b = 2;
public function getClosure(): \Closure
{
return function ($x, $y) use ($this): int {
return $x + $y + $this->a + $this->b;
};
}
}
PHP Fatal error: Cannot use $this as lexical variable in /home/vagrant/php-test.php on line 12
$thisも前の2つと同じようなシンタックスエラーになります。わざわざ use() を使わないよって。
この処理を行いたいのなら次のコードでいい。
namespace Test;
class Test
{
private int $a = 1;
private int $b = 2;
public function getClosure(): \Closure
{
return function ($x, $y): int {
return $x + $y + $this->a + $this->b;
};
}
}
$test = new Test();
$closure = $test->getClosure();
$result = $closure(3, 4);
var_dump($result);
int(10)
クラス内なので、メソッド内でプロパティを参照するように '$this->' で見れます。
無名関数(クロージャ)だからといって気にするな、ということですね?
この無名関数内で$thisが使えることをドキュメントでは『自動バインド』と呼んでいます。
Closureオブジェクトは、所属するクラスオブジェクトを保存(バインド)する機能を持っているから。
use() の変数値をクロージャで変更しても元の変数には反映されない
ここがPHPとJavaScriptとのちがいなんですが、use()で渡された変数値をクロージャ内で変更しても元の変数には反映されません。
そこは関数のパラメータと同じ仕様。
$a = 3;
$b = 4;
$c = 5;
// クロージャの定義
$anonymous = function ($x, $y) use ($a, $b, $c): int {
$a++;
$b++;
$c++;
return $x + $y - ($a + $b + $c) ;
};
// クロージャの実行
$result = $anonymous(1, 2);
var_dump($result);
var_dump($a);
var_dump($b);
var_dump($c);
int(-12)
int(3)
int(4)
int(5)
元の変数に変更を反映させたいときはリファレンスを使います。
$a = 3;
$b = 4;
$c = 5;
// クロージャの定義
$anonymous = function ($x, $y) use (&$a, &$b, &$c): int {
$a++;
$b++;
$c++;
return $x + $y - ($a + $b + $c) ;
};
// クロージャの実行
$result = $anonymous(1, 2);
var_dump($result);
var_dump($a);
var_dump($b);
var_dump($c);
int(-12)
int(4)
int(5)
int(6)
いわずもがなですが、スーパーグローバル変数やクラス内の $this を使ったプロパティは、use() を介さずに参照するので、変更した値は即座に反映されます。
上位スコープの変数値がクロージャへ反映されるタイミング
use() で渡される上位スコープの変数の値は、クロージャの実行時ではなくクロージャを定義したときです。
// クロージャ内部に反映される上位スコープの値
$a = 3;
$b = 4;
$c = 5;
// クロージャの定義
$anonymous = function ($x, $y) use ($a, $b, $c): int {
return $x + $y - ($a + $b + $c) ;
};
// ここで変更したものはクロージャを実行しても反映されない。
$a = 30;
$b = 40;
$c = 50;
// クロージャの実行
$result = $anonymous(1, 2);
var_dump($result);
int(-9)
クロージャ実行時に常に最新の上位スコープの変数値を反映させたければ、リファレンスを使います。
// クロージャ内部に反映される上位スコープの値
$a = 3;
$b = 4;
$c = 5;
// クロージャの定義(use()はリファレンス)
$anonymous = function ($x, $y) use (&$a, &$b, &$c): int {
return $x + $y - ($a + $b + $c) ;
};
$a = 30;
$b = 40;
$c = 50;
// クロージャの実行
$result = $anonymous(1, 2);
var_dump($result);
int(-117)
ただし、さっきも言ったように、リファレンスはクロージャ内での変更を即座に元の変数にも反映させるので、注意してください。
globalスコープで定義されたクロージャは use() を使わずに変数を参照可能。
上記のサンプルでは、クロージャを globalスコープで定義しています。そのときは、globalキワードを使って変数の参照ができるので use() はいりません。
// globalスコープの変数
$a = 3;
$b = 4;
$c = 5;
// globalスコープでクロージャの定義
$anonymous = function ($x, $y): int {
global $a, $b, $c;
return $x + $y - ($a + $b + $c) ;
};
$a = 30;
$b = 40;
$c = 50;
// クロージャの実行
$result = $anonymous(1, 2);
var_dump($result);
int(-117)
もちろんですが、仮にクロージャ内で $a, $b, $c の値を変更したら即座にglobal変数へ反映されます。
しかもこれ、ただたんにクロージャ内でglobal変数を参照しているだけで、正確にはクロージャじゃないですからね?
しつこいけど。
クラス内でのクロージャのスコープ
クラス内でクロージャを作成するところはメソッド内に限られます。プロパティでも作れると思いますが、それならメソッドにしろよ、という話なので。
(作ろうと思ったこともないし、これからも作ろうと思わない。)
そうなるとクロージャのスコープはどこまで? という話になるんですが、正解はクラス内です。
本来のクロージャならば、スコープはクロージャを定義したメソッド内になるはずですが、クラス内のクロージャは $this が参照可能です。
この $this があることで参照できる範囲はメソッドからクラス全体へ広がります。
staticな無名関数 - 自クラスオブジェクトをバインドしたくないとき
PHPのドキュメントには staticキワードを使った無名関数について書いてあります。
static を付けて無名関数を宣言することができます。 こうすることで、現在のクラスが無名関数を自動的にバインドすることがなくなります。 オブジェクトも、実行時にはバインドされなくなります。
無名関数 - staticな無名関数 - PHP公式ドキュメント
おぉ、そうか。$thisの自動バインドをしないのね? と思って見た次のサンプルがこれ。
<?php
class Foo
{
function __construct()
{
$func = static function() {
var_dump($this);
};
$func();
}
};
new Foo();
このサンプル、PHP7.4, 8系ではError例外が発生します。
PHP Fatal error: Uncaught Error: Using $this when not in object context in /home/vagrant/php-test.php:8
Stack trace:
#0 /home/vagrant/php-test.php(10): Foo::{closure}()
#1 /home/vagrant/php-test.php(13): Foo->__construct()
#2 {main}
thrown in /home/vagrant/php-test.php on line 8
そもそも、クラス内のstaticメソッド内で $this は使えません。staticは静的クラスでインスタンスがないから。
そりゃ、$this がないんだからバインドもされないけど、そもそもコードとしておかしい。
バインドしようとするとエラーにして $this をバインドさせない、ということらしい。
このサンプルは、あえて入れてはいけない $this を入れてバインドできないのを見せてくれてます。
そして次に、無名関数実行時にバインドされないサンプルがこれ。
<?php
$func = static function () {
// 関数の本体
};
$func = $func->bindTo(new StdClass);
$func();
PHP Warning: Cannot bind an instance to a static closure in /home/vagrant/php-test.php on line 6
PHP Fatal error: Uncaught Error: Function name must be a string in /home/vagrant/php-test.php:7
Stack trace:
#0 {main}
thrown in /home/vagrant/php-test.php on line 7
PHP Warning: Cannot bind an instance to a static closure in /home/vagrant/php-test.php on line 6
PHP Fatal error: Uncaught Error: Value of type null is not callable in /home/vagrant/php-test.php:7
Stack trace:
#0 {main}
thrown in /home/vagrant/php-test.php on line 7
staticな無名関数の実行時にはインスタンスがバインドされません。
でもちょっと気になるメッセージがります。
『関数は文字列の名前でないといけない』『nullはコールできない』は、バインド処理に失敗しているので戻り値のClosureオブジェクトがnullになってるから。
確認してみましょう。
<?php
$func = static function () {
// 関数の本体
};
$func = $func->bindTo(new StdClass);
$var_dump($func);
PHP Warning: Cannot bind an instance to a static closure in /home/vagrant/php-test.php on line 6
PHP Warning: Undefined variable $var_dump in /home/vagrant/php-test.php on line 7
...(省略)
これだと static な無名関数が実行できるか分かりません。確認しましょう。
$func = static function () {
return 'static function !!!';
};
$result = $func();
var_dump($result);
string(19) "static function !!!"