発端はこのissueです。
うおー無名再帰! いいですね!
雑に訳してみましょう。
Named anonymous functions (名前付き無名関数) #1816
概要
JavaScriptは無名関数の名付けをサポートしており、周囲のスコープを汚染することなく関数が再帰的に自分自身を参照できます。その上、わかりやすいスタックトレースも提供しています。
たとえば:
function f(cb) { console.log(cb.name); // "funName" と出力される cb(0); } f(function funName(i) { // funName はここでは有効 if (i < 3) { console.log(i); funName(i + 1); } }); // funName はここからは参照できない
出力
funName 0 1 2
PHPでも実現できればいいでしょう。以下のように… (後略)
(後略) の部分ではPHPでの実装例や「名前付き無名関数」のメリットが語られているのですが、最終的に実現されたものとは関係ないので一旦省略します。
どんなときに自分自身を呼び出したいかというと、以下のようなフィボナッチ数の定義を考えてみましょう。
{\displaystyle {\begin{cases}F_{0}=0&\,\\F_{1}=1&\,\\F_{n}=F_{n-1}+F_{n-2}&(n\geqq 2)\end{cases}}}
この数学的な定義をPHP+名前付き無名関数が追加されたと仮定して直訳してみましょう。
$fibonacci = fn F($n) => match ($n) {
0 => 0,
1 => 1,
default => F($n-1) + F($n-2),
};
かなり数学的な定義に近い、美しいプログラムになりますね!
このコードが実際に動くなら素晴らしいですが、現実はうまくきません。
これからいろいろと説明していきたいのですが、まずは先に提案されたで示されたJavaScriptのコードがなぜこのように振る舞うかについては説明が必要でしょう。
式、文と宣言
上のfunction f
は関数宣言(function
宣言)と呼ばれ、PHPにもある普通関数定義文とまったく同じではないのですが、いったん似たようなものと捉えておいてもいいでしょう。
問題はfunction funName
の方です。これはf(...)
という関数呼び出しの中に書かれています。この記法はJavaScriptの関数式(function
式)であり、PHPでいう無名関数(Closure
)の仲間です。
式と文、さらに宣言の違いとはなんでしょうか。
非常に乱暴にいうと…
- 「式」(expression)とは…
- 値を返すコードのまとまりで、他の文や式の一部になるものです
- たとえば、変数に代入したり、引数に渡したりできます
- 「文」(statement)とは…
- それ以外の値を返さないコードのまとまりです
-
if
やfor
のような制御構造やreturn
のようなフロー制御を行う文があります-
if
文はif ($a === 1)
のように、構造の一部に式(判定式)を要求します
-
- また、式は
;
で区切ることによって式文(expression-statement)という文になります- JavaScriptの
console.log(
Hello ${name});
、PHPのprintf("Hello %s!", $name);
のようなものです- ただしJavaScriptは自動セミコロン挿入という仕様で
;
を省略できます
- ただしJavaScriptは自動セミコロン挿入という仕様で
- JavaScriptの
- 「宣言」(declaration)とは…
- 文と近いですが、記述できる場所が限定されています
例えば式は1 + 2 * 3
のようなもので、これは$n = 1 + 2 * 3
のような代入式の一部になることができます。通常、代入は$n = 1 + 2 * 3;
のように末尾に;
を付ける式文として書かれますが、if (n = 1 * 2 * 3)
のようなif
文の一部として条件式に書くこともできます。
また、JavaScriptにおいては始めて使われる際にvar n = 1 + 2 * 3;
またはconst n = 1 + 2 * 3;
のように変数宣言されます。これは代入式(式文)ではなく、宣言にあたります。一方でPHPにおいてもconst N = 1 + 2 * 3;
という定数宣言ですが、JavaScriptとも使われかたが異なります。
さらに理解を深めたい方は、暇なときにこの記事(スライド)も見ておいてください。
PHPとJavaScriptのfunction
さて、以下の関数定義および関数式は、JavaScriptでもPHPでも動きます。
// これは関数宣言
function add1($a, $b) {
return $a + $b;
}
// これは関数式を $add2 に代入している
$add2 = function ($a, $b) {
return $a + $b;
};
どちらの言語でもadd1(1, 2)
$add2(3, 4)
のように呼び出せるところは同じですが、以下のコードでは差分が生まれます。
// これは関数宣言
function add0($a, $b) {
return ($b === 0) ? $a : add0($a + 1, $b - 1);
}
console.log(add0(1, 2));
// これは関数式を $add$ に代入している
$add$ = function add3($a, $b) {
// 関数自分自身をコールするために add3 という名前を使っている
return ($b === 0) ? $a : add3($a + 1, $b - 1);
}
console.log($add$(1, 2));
console.log(add3(1, 2)); // ← add3 関数は定義されていないので、これはエラー
これが今回の発端となった「名前付き無名関数」というやつです。これは、function
の前に$add3 =
が置かれている以外はまったく同じ構造になっているのがわかりますでしょうか。add3
は関数の中で自分自身をコールするための仮の名前に過ぎません。
この場合は$add$
という変数名で自己を再帰呼び出しできるのでこの例に限ってはadd3
という名前をつける必要は特にないのですが、変数名と独立して関数自身を指し続けることができます。
名前付き無名関数の重要な点は、変数代入なしで別の関数の引数として渡しても、即時実行しても、自分自身を再帰呼び出しできるという点です。
PHPの変数スコープと無名関数の制約
PHPの無名関数(関数式)はJavaScriptと同じく「関数閉包」と呼ばれる関数の外側の変数を掴まえる(キャプチャする)という性質を備えているという点で共通しているのですが、その解決方法が大きく異なるのです。
JavaScriptの関数式は外側の環境(変数スコープ)をそのまま引き継ぐ、つまりすべての変数にそのままアクセス(読み取り・変更)でき、ESモジュールとして分離しない限りはスコープを引き継ぎます。キャプチャする際も環境を共有しないという選択肢がありません。
対するPHPは基本的にキャプチャ時に変数をコピーして、変数の参照を共有する際はキャプチャする変数を明示的に指定するという方式をとっています4。
これまでのPHPでは以下のように実装できました。
// これは関数宣言
function add0($a, $b) {
// これは問題なく自分自身を呼び出せる
return ($b === 0) ? $a : add0($a + 1, $b - 1);
}
var_dump(add0(1, 2));
// これは関数式を $add3 に代入している
$add3 = function($a, $b) use (&$add3) {
// 関数自分自身をコールするために add3 という名前を使っている
return ($b === 0) ? $a : $add3($a + 1, $b - 1);
};
var_dump($add3(1, 2));
なぜuse ($add3)
では動かずuse (&$add3)
にする必要があるのかというと、function () { ... }
で関数オプジェクトを作成する時点ではまだ$add3
には何も代入されておらず、関数そのものを捕まえることができないからです。
さらには、従来のfunction(){...}
によるタイプの関数式は上記のようにリファレンスでキャプチャできるのですが、fn () =>
による短い関数式ではPHP 8.x現在ではキャプチャを制御する方法が提供されていません。
正確には不動点コンビネータ(Zコンビネータ)とかいう必殺技みたいな謎テクを使えば無名再帰も実現できるのですが… 実用で使うことはないので忘れてください!
みたいな話をもっと長々と書いたので、暇で暇で仕方ない人だけ読んでくださいね。
あと、この記事でもZコンビネータをライブラリとして実装しています。
どちらにせよ過剰なのでリアルワールドでの出番はありません。
Implement Closure::getCurrent()
ここからがようやく本題です。
このissueはすぐに「PHP 8.4からスタックトレースに乗る関数名が"{closure}"
より明示的になったよ」「RFCでhttps://wiki.php.net/rfc/closure_self_referenceが提案されているよ」というレスがついて、起票者本人がクローズします。
8.4 以降、クロージャはファイル名とクロージャが定義されている行番号を参照するため、単なる「{closure}」よりも適切な名前が付けられています。
いいですね
いいね、閉じます
そこに来たのがPHPコア開発者のIlija Toviloです。
この機能は私にとって本当に奇妙です。再帰的な匿名関数がどの程度あり得るかはわかりませんが、それでも
__FUNCTION__
マジック定数の動作を改善する方がよい計画のように思えます。クロージャもキャプチャされた変数を通じてコンテキストを運ぶので、
__FUNCTION__
は有効な代替手段ではありません。__CLOSURE__
はおそらく機能して、ZEND_CLOSURE_OBJECT(fbc)
として評価される可能性があります。これもあるけど、だいぶ古びてる。
そして次の発言
実際、
Closure::getCurrent()
関数を使えばかなり簡単に解決できて、参照渡しの変数のシンプルな代替手段になるかもしれない。すぐにPRを作成して、internalsの人たちの意見を聞いてみます。
internalsというのはPHPの開発用メーリングリストのphp.internalsのことです。
閲覧用としてはexternals.ioから見るのが検索もしやすくて便利。
ということで作られたのがこれ。
「かなり簡単」って言ってるだけあって、実装の変更は17行だけ。
名前がClosure::getCurrent()
なのはFiber::getCurrent()
の前例踏襲。
次いで、internals(メーリングリスト)でもいちおう議論はされてる。
この解決策で懸念はありますか?
なかったら数週間のうちにmasterに入れようと思います。Ilija
ということで、RFCなしでmasterにダイレクトでマージ5する流れ。
このスレッドでの懸念らしい懸念は「相互再帰」どうするの? ということで、こういうレスをされています。
相互再帰的なクロージャが必要な段階であれば、名前付き関数や匿名クラスを使うのが良いでしょう。
自己再帰的なクロージャは既にやや疑問視されており、ほとんど使われていないため (私自身は過去に何度か使ったことがありますが)、その場合のために「DXを最適化する」ことは有用ではないと思います。
いちおう「相互再帰」という概念にも触れておきましょう。Schemeというプログラミング言語では関数の再帰呼び出し(特に末尾再帰)がよく表れるのですが、二つの関数がどう名前解決されるかという、一見やっかいそうな問題が実際にはSchemeでは問題が起こらないことを説明されています。
そもそも再帰呼び出しはコールスタックの制限がつきまといます。Schemeは末尾再帰にさえなっていればコールスタックを消費しないことが保証されていますが、PHPもJavaScriptでもそのような最適化機構は基本的にありません6。
そもそも… PHPに再帰なんて必要なのか?
えー、致命的な問題として、こういう再帰の仕方は数学的な漸化式の定義と一致して美しいのですが、どうやっても非効率なのです…
$fibonacci = fn fib($n) => match ($n) {
0 => 0,
1 => 1,
default => fib($n-1) + fib($n-2),
};
いちおうメモ化再帰(自動メモ化)というテクを使えばそこそこパフォーマンスは出せるとは思うのですが、まあロマン7ですね… (PHP 7.3以降でめちゃくちゃ書きやすくなったのは嬉しい)
<?php
function memoizer(Closure $f) {
$store = [];
return $memo = function (int $n) use ($f, &$memo, &$store) {
return $store[$n] ??= $f($n, $memo);
};
}
$fib = memoizer(fn (int $n, Closure $fib) => match ($n){
0 => 0,
1 => 1,
default => $fib($n-1) + $fib($n-2),
});
var_dump($fib(20));
// => int(6765)
効率性を求めるならそもそもこの実装は二重再帰してしまっているので、さらに効率的な実装方法はあるが… 今回の記事で説明したい無名再帰についての本筋ではないので読者への課題とします(?) キミだけの最強のfib関数を見付けだせ!
それでも無名再帰がほしい理由
そんなこんなでinternalsでも特に異論はなく、この記事を書いている7月22日の日本時間で日付が変わったくらいの時間にマージされましたとさ。
PHPなんかで無名再帰なんか使うことはないし特に役にも経たんだろうと私も思います。
が、さきほどのメーリングリストでIlijaが言及しています。
相互再帰的なクロージャが必要な段階であれば、名前付き関数や匿名クラスを使うのが良いでしょう。
自己再帰的なクロージャは既にやや疑問視されており、ほとんど使われていないため (私自身は過去に何度か使ったことがありますが)、その場合のために「DXを最適化する」ことは有用ではないと思います。私も同感です。私の隠れた動機を明かすと:参考文献[1]。
PHPプログラマーはリファレンス(参照
&
)を比較的稀にしか使用しませんが、エンジン内では予想以上に多くの場所に潜り込んでいます。また、ユーザーを混乱させることが多く、オプティマイザーの負荷も高く、要求の高い機能(例えば型付き配列)の実装を非常に困難にしています。私はリファレンスが必要なもののリストを収集し始めましたが、自己再帰クロージャもそのひとつです。
これは非現実的な目標かもしれませんが、リファレンスを廃止することを検討したい場合、リファレンスが現時点で唯一の実行可能なオプションである場合に備えて、安定した代替手段が必要になります。
私は以前こういう記事を書きました。これは我ながらよくまとまった記事だと思うので読んでいただきたいのですが、ここでは使わない方がいいリファレンスの使い方と使わざるを得ない/使っても害が少ないであろうパターンを列挙しています。
リファレンスがPHPの言語機能の中でも混乱を呼んでいてPHPの最適化の壁になっているということは間違いなく、フェードアウトさせていきたいというのは非常に理解できます。今回のその結論を下す時期に至る前に「リファレンスがなければできない」ということを減らして外堀の埋めようというのが今回のClosure::getCurrent()
だということですね。
実際にHack/HHVMは2019年にリファレンスを捨て去ったので、PHPもこれに続ける明日が来るのか、来ないのか、最終的な廃止のためにはかなりハードな議論が待っていると思いますが、とにかく手を動かさなければ終わらないので、PHP先生の未来にご期待ください!
(PHPの何かを変えようと思うと、何につけてもWordPressというフレーズが脳内に響くようになってしまったのでよくない)
結論
まあとりあえず、RFCなしで入った変更なんで致命的な問題があればサクっとrevertされる可能性はありますが、まあPHP 8.5で使えるようになるのではないのでしょうか。PHPStanの実装はまだですが、magoという新手の静的解析ツールは既にサポートしています。
Q. 結局これは、覚えるべき機能なの?
A. いいえ。いままで無名再帰の概念を知らなかった人がこれから使うことは98%ないと思うので、忘れても全然問題ないです。
おそらく普通にループで書いた方がいい場合がほとんどです。
それでも再帰沼に飛び込んでみたいみなさんは、私といっしょにEmacs LispでSICPをやりましょう。
-
たとえばRubyは制御構造やクラス定義・メソッド定義などの宣言も式であり、文とはそれらを改行または
;
で区切ったものという意味しかありません。さらには、定義の中に書けるコードの制約もほとんどありません。 ↩ -
Pythonにおいては変数宣言はありませんが、
n = 1 + 2*3
のような「代入文」になっています。かつてはPythonには代入式はありませんでしたが、Python 3.8ではif n := 1 + 2*3:
のように関数式が書けるようになりました。 ↩ -
歴史的にはjQueryのショートハンドな関数名として
$
が使われていますが、これは$
が通常の変数名や関数名として利用可能な識別子だからです。現代でも利便性のために、Chrome DevToolsのConsoleでは$
がdocument.querySelector
のショートカットとして使えます。また、PHPとは違って$
は名前の一部なので$foo$bar$buz$ = 1
のような変数も問題なく使えます。 ↩ -
PHP以外の言語ではC++がキャプチャを明示的に制御できる方式のクロージャ(ラムダ式)を提供しています: ラムダ式 [N2927] - cpprefjp C++日本語リファレンス ↩
-
剛腕だなあと思ったけど、よく考えるとRFCで審議しなければいけない機能の基準ってなさそうなんですよね…? すくなくともこの変更で後方互換性は壊れていない。(
Closure
を継承した独自定義クラスを実装することはできないので) ↩ -
JavaScriptではいろんな試みはずっとあるのですが… 説明するのがだるいので「JavaScript TCO」とかでググって! ↩
-
ここでは「かっこいいだけで実用的ではない」ロマン武器 (ろまんぶき)とは【ピクシブ百科事典】を指す ↩