本連載においては,PHP界隈の最新情報を紹介しています.今年11月の正式リリースに向けてPHP 8.6の開発が本格化しています.8月に予定されるフィーチャーフリーズに向けて,PHP 8.6の新機能がほぼ出そろってきています.今回は,PHP 8.6の新機能の目玉である関数の部分適用について紹介します.
関数の部分適用とは?
PHP 8.6では,「関数の部分適用(Partial Function Application, PFA)」が導入されます。この関数の部分適用とは、関数呼び出し時に一部の引数だけを先に渡し、残りの引数を後から受け取る関数(クロージャ)を生成する機能です。
従来、コールバック関数などで一部の引数を固定したい場合は、アロー関数(fn())などを使ってラップする必要がありましたが、PFAの導入によりこれを非常に簡潔に記述できるようになります。
PHPにおける関数の部分適用の実装では,引数を省略したい場所に,以下の2種類のプレースホルダーを使用します.
- ?(単一引数のプレースホルダー): その位置に正確に1つの引数が期待されることを示します。
- ...(可変長引数のプレースホルダー): その位置に0個以上の引数が供給されることを示します。
部分適用(PFA)の使い方
部分適用(PFA)は.PHP 8.1で導入された第一級コーラブル(First-class callable,FCC)の概念をさらに拡張したものと言えます。FCCは,...のみをプレースホルダーとして使い,以下のように関数のクロージャを簡単に作成する機能です.
$a = str_replace(...);
この場合,$a は,str_replace 関数と全く同様に動作します.以下の簡単なサンプルを実行してみましょう.
$text = "Hello World";
echo $a("Hello", "Goodbye", $text), "\n";
出力は,”Goodbye World”となり,置換対象文字列$textの”Hello”が”Goodbye”に置換されています.
部分適用(PFA)を利用すると,FCCよりも自由度が高くなり,関数の一部の引数を固定することができます.先ほどの例についてPAFを使って書き換えてみましょう.
$a = str_replace("Hello", "Goodbye", ?);
echo $a($text), "\n";
出力は同じになりますが,関数のクロージャを作成する際に,str_replace 関数の第1引数と第2引数を指定(固定)し,第3引数のみ?を指定して省略しています.これにより,クロージャ$aは,第3引数のみを引数とする関数となります.
従来,このように一部の引数を固定するには,アロー関数を用いて,以下のように記述する必要がありました.
$a = static fn($text) => str_replace("Hello", "Goodbye", $text);
echo $a($text), "\n";
クロージャ$aの動作はPFAを用いた場合と同様になりますが,PFAを用いた場合にはより簡易的に記述できることがわかります.
上記の例は以下のように名前付きの引数で指定することも可能です.
$a = str_replace(subject: ?, search: "Hello", replace: "Goodbye");
echo $a($text), "\n";
出力は上記の例と同じになります.subject: のように名前で指定する場合,引数の順序は任意となります.
FCCの使い方に似ていますが,PFAでプレースホルダーとして ... を使うと,残りの引数(引数の数は任意)を指定することができます.上記の例を少し書き換えてみましょう.
$a = str_replace("Hello", ...);
echo $a("Goodbye", $text), "\n";
この場合,第1引数のみが固定され,クロージャ$aは第2引数以降を引数とする関数となります.なお,この例では,PHPの組込み関数(str_replace)を使用しましたが,任意のユーザ定義関数でも同様の効果が得られます.
オプションパラメータの扱い
部分適用(PFA)における非常に重要な仕様として、? プレースホルダーによって生成された引数は,元の関数でデフォルト値を持つオプション引数であったとしても,常に「必須(non-optional)」のパラメータとして扱われるという点があります.
これは、部分適用された関数は特定のシグネチャを期待するコールバックとして渡されることが多く,引数が省略可能であることに依存すべきではないためです。この仕様により,予測不可能な挙動が防がれ、静的解析ツールによる型チェックも正確に行えるようになります. ただし、... で引き継がれたパラメータに関しては、元のオプション性が維持されます.
このオプションパラメータの扱いに関する仕様は,オリジナルの提案(RFC)の後に開発者の議論を経て一部変更・明確化されたものとなります.少しわかりにくいので具体的な例を見てみましょう.
function concat(string $a=’a’, string $b = 'b', string $c = 'c') {
return $a . $b . $c;
}
$foo = concat(?, ?);
echo $foo("A", "B"), "\n";
まず,引数文字列を結合して返すユーザ定義関数concatを定義します.この関数の第2引数および第3引数には,デフォルト値が指定されています.部分適用(PFA)を用いて第1,第2引数にプレースホルダー”?”を指定してクロージャ$fooを作成します.この例の出力は,”ABc”となります.3番目の引数には元の関数concatにおけるデフォルト値(optional)が適用されます.
しかし,元の関数の第2引数にデフォルト値が適用されているからといって,以下のように第2引数を省略することはできません.
echo $foo("A"), "\n";
これを実行するとエラーを発生します.この理由は,本節の冒頭で述べたように,?をプレースホルダーとして指定した場合,デフォルト値の指定によらず必須のパラメータとして定義されるためです.
一方,以下のようにプレースホルダーとして...を用いた場合,省略することが可能です.
$foo = concat(?, ...);
echo $foo("A"), "\n";
この場合の出力は Abc となり,エラーを発生しません.
アロー関数との評価順序の違い
従来のアロー関数(fn())からPFAに乗り換えようとする場合,注意が必要な差異が一つあります.従来のアロー関数(fn())でラップした場合、内部の関数に渡される引数の評価は「クロージャが実行されたタイミング」で行われます。 一方、PFAではクロージャが「生成されたタイミング」で引数の式が事前に評価され、バインドされます。
例として,以下のように2個の整数を引数とする関数
fooを定義し,現在時刻time()を引数として第1引数に指定したクロージャ$fを定義してみましょう.
function foo(int $a, int $b): string {
return "$a => $b";
}
$f = foo(time(), ?);
このクロージャを1秒毎に呼び出してみます.
for ($i = 0; $i < 5; $i++) {
echo $f($i), "\n";
sleep(1); // 1秒待つ
}
出力は例えば以下のようになり,時刻の部分が変わらない(1785637693のまま一定)ことがわかります.なお,時刻の部分は実行のタイミングに依存して異なります.
1785637693 => 0
1785637693 => 1
1785637693 => 2
1785637693 => 3
1785637693 => 4
次に,同様に1番目の引数を時刻で固定するアロー関数を定義します.
$f = fn($x) => foo(time(), $x);
この関数の出力は,例えば,以下のようになり,時刻の部分が1秒毎に変化していることがわかります.
1785637698 => 0
1785637699 => 1
1785637700 => 2
1785637701 => 3
1785637702 => 4
これは,先述の通り,アロー関数の場合は,実行時に引数(time())が評価されるためです.一方,PFAの場合は,クロージャが生成されたタイミングで引数が一度だけ評価されるため,時刻の値は一定となります.
パイプ演算子への適用時の最適化
PFAは,PHP 8.5で導入されたパイプ演算子と組み合わせることも可能です.例えば,以下のような使い方になります.ここでは,str_replace関数で文字列置換を行った後,strtoupper関数で大文字に変換しています.
$test = "Hello World";
echo $test |> str_replace("Hello", "Goodbye", ?) |> strtoupper(?);
出力は,GOOBYE WORLDとなります.パイプ演算子でPFAを利用する際,引数が1つのみのケースでは,クロージャの生成を省略してFCCと同様の直接呼出しを行うように,コンパイル時に最適化を行います.これにより,性能上のオーバーヘッドをほぼゼロに抑えることができます.
今回は、次期マイナーバージョンであるPHP 8.6で導入される新機能のうち,最大の目玉になると思われる部分適用(PFA)について紹介しました.今回紹介した変更点も含めて,フィーチャーフリーズまでは変更が行われる可能性があることに注意してください.次回以降もPHP 8.6の新たな機能や変更点について紹介していく予定です.



