とてもたのしい。
パイプライン演算子ってなに?
関数のネストを左から右に書けるようになります。
とりあえずなんか適当な処理を書いてみましょう。
$string = 'Hello World';
$tmp = htmlentities($string);
$tmp = str_split($tmp);
$tmp = array_map(strtoupper(...), $tmp);
$result = array_filter($tmp, fn ($v) => $v != 'O');
var_dump($result); // ['H', 'E', 'L', 'L', ' ', 'W', 'R', 'L', 'D']
普通に書くと中間変数がたくさんになってたいへん面倒ですね。
まとめて書きたいですね。
$string = 'Hello World';
$result = array_filter(
array_map(
strtoupper(...),
str_split(
htmlentities($string)
)
), fn ($v) => $v != 'O'
);
var_dump($result); // ['H', 'E', 'L', 'L', ' ', 'W', 'R', 'L', 'D']
とてもつらい。
かっこが多すぎてどこからどこまでがどの括弧に当たるのかがわかりづらく、さらに書式は左から順なのに実行は右からと逆になっているせいで分かりづらさが増幅されます。
そこで満を持してパイプラインの出番です。
$string = 'Hello World';
$result = htmlentities($string)
|>str_split(...)
|>(fn($x) => array_map(strtoupper(...), $x))
|>(fn($x) => array_filter($x, fn($v) => $v != 'O'));
var_dump($result); // ['H', 'E', 'L', 'L', ' ', 'W', 'R', 'L', 'D']
区切りが|>なので認識しやすい、書式も処理も左から右に進むので理解しやすい、といいことずくめな文法です。
パイプライン演算子の基礎
|>の左側を、右側に渡す、というのが基本的な動作です。
'string' |> strtoupper(...) |> var_dump(...); // 'STRING'
CallableExpr(...)はPHP8.1で導入された第一級callableという文法です。
$f = strtoupper(...)ってするだけで$fがクロージャになります。
パイプラインの右側にはクロージャを置くことができ、そして左側から来た値がクロージャの引数として渡される、というイメージで考えておけばよいでしょう。
複数の引数を持つ関数
複数の引数を持つ関数については、そのままパイプラインを書くことはできません。
'string' |> substr(..., 1, 4); // エラー
ぱっと見このように書けないかなと思ってしまいますが、これはエラーになります。
パイプライン演算子の制限というわけではなく、第一級callableの制限です。
$s = substr(..., 1, 4); // エラー
$s = substr(...);
$s('string', 1, 4); // OK
CallableExpr(...)という記法全体がcallableを生成する文法であり、...は特に引数などを表しているわけではないということです。
従って、複数引数の関数を使いたい場合は、引数がひとつのクロージャを作ったうえで全体を括弧で包む必要があります。
'string' |> ( fn($x)=>substr($x, 1, 4) );
いまいち文法がもったりしてしまいますね。
実はこの全体を括弧で包むやつ、2025年5月時点のRFCにはなかったのですが、その後問題が見つかったため追加されました。
ざっくり言うと特定のパイプラインを書いたときに演算子の優先順位が不自然になるという問題です。
パイプラインの優先順位
$result = $arr
|> fn($x) => array_map(strtoupper(...), $x)
|> fn($x) => array_filter($x, fn($v) => $v != 'O');
これは
$temp = (fn($x) => array_map(strtoupper(...), $x))($arr);
$result = (fn($x) => array_filter($x, fn($v) => $v != 'O'))($temp);
のように解釈されるのが自然に見えますね。
ところが現在のPHPの演算子の優先順位では
$result = $arr
|> fn($x) => (
array_map(strtoupper(...), $x)
|> fn($x) => (
array_filter($x, fn($v) => $v != 'O')
)
);
と解釈されてしまいます。
最初の|>が、後ろの全てを括っています。
これは明らかに想定していない動き方ですね。
実は面白いことに、全ての関数・クロージャが純粋であれば、これはどちらでも結果が同じになるそうです。
そのため事前のテストでは問題を発見することができなかったとかなんとか。
しかしPHPでは純粋でない関数を書くことが容易にできるため、その場合は想定と異なる結果になり、おそらく現場は大混乱に陥ることでしょう。
そこで、単純にそのような文法を禁止することにしました。
パイプラインのクロージャには必ずかっこを必要とすることで、優先順位の曖昧さは排除されました。
【PHP8.6】部分適用
複数引数パイプラインの優先順位問題はこれで解決したわけですが、そのかわり文法がもったりしていまいち使いづらくなってしまいましたね。
こんな文法でよしとされたのは、次バージョンのPHP8.6で部分適用が導入されることが既に決まっているからです。
// アロー関数
$f = fn ($str) => substr($str, 0, 5);
// 部分適用
$f = substr(?, 0, 5);
関数の引数に?を入れると、それ以外の引数が適用されたクロージャが返ってくるという機能です。
これ単体でも相当便利な機能ですが、これをパイプラインに適用すると、こうなります。
$result = $arr
|>array_map(strtoupper(...), ?)
|>array_filter(?, fn($v) => $v != 'O');
実にすっきりしました。
正直なところ、パイプラインは部分適用が導入されてこそ完成形に至るといっていいのではないでしょうか。
【PHP9?】関数合成
今後PHP9あたりで、関数合成が導入される見込みです。
$f = strtoupper(...) + var_dump(...);
$f('string'); // STRING
クロージャ同士を足し算できます。
これを使うと、さらに複雑なパイプラインを簡潔に書くことができるようになります。
$result = $val
|> func1(...)
|> map(func2(...))
|> map(func3(...))
|> filter(func4(...))
|> filter(func5(...))
;
配列のスキャンを4回もやっており、無駄が多いことになっています。
mapは一回だけで済ませたいですね。
現在のパイプラインでは、このように書く必要があります。
$result = $val
|> func1(...)
|> map(fn($x) => $x |> func2(...) |> func3(...))
|> filter(fn($x) => $x |> func4(...) |> func5(...))
;
実現はできるのですが、かっこが多くなってわかりにくいですね。
関数合成が来ると、このように書けるようになります。
$result = $val
|> func1(...)
|> map(func2(...) + func3(...))
|> filter(func4(...) + func5(...))
;
きれいすっきり。
ここまでくると正直普通にループで書けよって気もしますが。
$result = [];
$val = func1($val);
foreach($val as $k=>$v){
$v = func2($v) |> func3(...);
if(!func4($v)){ continue; }
if(!func5($v)){ continue; }
$result[$k] = $v;
}
特にわかりにくいってほどでもなく、ループ回数が1回になりました。
感想
なんかひとつの値を次々と変形していくような処理が、格段に書きやすくなります。
それでもまだPHP8.5だけでは少々決定打に欠けるのですが、8.6で部分適用が導入されて以降は、こちらが主流になってもおかしくないまでありそうですね。