世界を動かす技術を、日本語で。

PHP 8.5にパイプ演算子が追加されました

概要

PHP 8.5で新たに導入される パイプ演算子(|>) について解説。 この機能の基本的な使い方と 実用例 を紹介。 他言語との比較や、PHPにおける 歴史的経緯 を整理。 今後予定されている 関連機能(部分適用や関数合成) にも触れる。 PHP開発者にとっての メリットと可能性 を強調。

PHP 8.5のパイプ演算子(|>)とは

  • PHP 8.5 で新たに追加される パイプ演算子(|>) の概要
  • 左側の値を右側の関数(callable)へ 単一引数 として渡す構文
  • 例:
    • $result = "Hello World" |> strlen(...);
    • 上記は $result = strlen("Hello World"); と同義
  • パイプライン として複数回連結することで、処理の流れを簡潔に記述可能
  • Unix/Linuxの シェルパイプ(|) に着想を得た設計

パイプ演算子の実用例

  • 配列操作などの 複雑な処理を直感的に記述 できる利点
    • 例:
      • $result = $arr |> fn($x) => array_column($x, 'tags') |> fn($x) => array_merge(...$x) |> array_unique(...) |> array_values(...);
      • $resultは重複を除去し再インデックスされた配列になる
  • 従来のPHPでは ネストが深くなり可読性が低下、または一時変数が必要
  • パイプチェーンにより、 match()ブロックなど一式で記述可能

パイプ演算子の背景と他言語との比較

  • F#やOCaml、Elixir など関数型言語で普及している構文
  • PHPでも Hack/HHVM で独自のパイプ構文が存在
  • 2016年にSara Golemon氏がPHPへの導入を提案
    • Hack流の$$トークン利用は 非標準的かつ限定的 で不採用
  • 2020/2021年には Partial Function Application (PFA) との連携も議論
    • PFAは複雑さから不採用となったが、 First Class Callables が導入

パイプ演算子の応用と発展性

  • シンタックスシュガー としての役割
    • 実装自体は単純だが、他機能との組み合わせで威力を発揮
  • match()式 やクロージャ返却関数との連携で表現力向上
    • 例:
      • $profit = [1, 4, 5] |> loadSeveral(...) |> filter(isOnSale(...)) |> map(sellWidget(...)) |> array_sum(...);
  • Maybeモナド のようなnull安全処理も簡単に実装可能
    • maybe()関数を使い、nullを自動的に伝播
  • KotlinやC#の拡張関数 に近い柔軟性を獲得
  • ストリーム処理やデータ変換にも応用範囲が広い

今後の展望と関連RFC

  • Partial Function Application の再挑戦
    • 1引数化や柔軟な関数適用を目指す
    • PHP 8.5以降に導入予定、開発中
  • 関数合成演算子 の検討
    • 複数関数の合成でパイプラインの最適化を狙う
    • PHP 8.6以降の実装を目指す
  • PHP Foundationチームの Ilija Tovilo氏、Arnaud Le Blanc氏 らの貢献

パイプ演算子の価値と今後の参加

  • constructor property promotion などと並ぶ高コストパフォーマンス機能
  • 記述の簡潔化、可読性・保守性の向上
  • PHP開発の推進に スポンサーとしての参加 も呼びかけ

Hackerたちの意見

一方で、JS界隈はこの提案を10年間待ってるけど、まだステージ2なんだよね。 https://github.com/tc39/proposal-pipeline-operator/issues/23...

正直、これは必要ないと思う。構文の甘さみたいなもので、ドットを使えばほぼ同じことができるし。PHPにはチェイニングがないし、複雑さを増やしても言語が良くなるわけじゃないよ。

TypeScriptでは、こんな感じで書けるよ。 let res = op1() res = op2(res.op1) res = op3(res.op2) 型推論がすごくうまく働いて、デバッグやリファクタリングもめっちゃ楽。 個人的には、結果をパイプでつなぐよりもこっちの方がいいと思う。JavaScriptには十分な機能があるしね。

10年も待ってるだけじゃなくて、進展の最有力候補が提案された時に求めていたものとは全然違うんだ。 ユニアリ関数(部分関数適用で作られるようなやつ)と相性のいいパイプ演算子が欲しかったのに、それはクロージャを使いすぎるプログラミングスタイルになるって理由で却下されたんだよね。 でもPHPはそういう仮定に縛られず、みんなが欲しかった機能を、最も理にかなった形で提供したんだ。

いいね、でも「JSのパイプの実例」は、正直言って期待外れだと思う。

ゴーランのモナドについては触れたくもないけど、どちらも人気があって進化してるね。

最高でも新しいビルドシステムが3つと新しいフレームワークが10個くらいだろうね。

努力は評価するし、全体的なアプローチも好きだけど、この使い方に関しては、Kotlinみたいな拡張機能や、C#のIEnumerable/イテレーターアプローチがいいな。例えば、こんな感じで: $arr = [ new Widget(tags: ['a', 'b', 'c']), new Widget(tags: ['c', 'd', 'e']), new Widget(tags: ['x', 'y', 'a']), ]; $result = $arr |> fn($x) => array_column($x, 'tags') // 配列の配列を取得 |> fn($x) => array_merge(...$x) // 一つの大きな配列にフラット化 |> array_unique(...) // 重複を削除 |> array_values(...) // 配列を再インデックス。 って書くより、$result = $arr->column('tags')->flatten()->unique()->values()の方がずっとシンプルだと思う。

パイプの利点は、戻り値の型を気にしなくていいことだよね。例えば、そのチェーンの途中にreduceを追加したとする。拡張メソッドだと、それがチェーンの最後に呼び出されることになるけど、パイプだと結果を次の関数にそのまま流せる。

PHPにはトレイトがあるから、そのAPIを考案して、トレイトに入れてデータクラスに追加すればいいんじゃない?

Kotlinには、任意のメソッドをチェーンできる拡張関数let(いくつかのバリエーションもあるよ)があるんだ。例えば、こんな感じで使えるよ:

val arr = ...
val result = arr.let {
    column(it, "tags")
    .let { merge(it) }
    .let { unique(it) }
    .let { values(it) }
}

単一引数の関数も関数参照で追加できるよ:

arr.let(::unique) // または(List::unique)、関数によるけど

特別な言語構文を追加することなくね。

いいね!PHPに一番必要なのは、文字列や配列の関数をもっと一貫性のあるものにして、チェイニングできるようにすることだと思う。今は少なくともチェイニングできるけど、...の構文はあまり好きじゃないな、特にスプレッド演算子と混ざると。

同意する。...の構文は混乱を招くよね。例の中で各fn($x)が$xを引数の名前として使ってるから。最初の直感はこう書くことだな: $result = $arr |> fn($arr) => array_column($arr, 'tags') // 配列の配列を取得 |> fn($cols) => array_merge(...$cols) これだとスコープがどうなるのか気になる。チェーンの中の関数が入力の$arrを参照できない気がするんだけど、参照渡しはできるのかな?

PHPの文字列/配列関数は一貫性があるよ。 文字列関数は(haystack, needle)を使って、配列関数は(needle, haystack)を使うのは、Cライブラリがそういう風に動いてるからなんだ。

構文は、単一引数の関数の場合、(...)の部分を完全に省略できるようにして、追加の引数が必要な関数にはカリー化を使うことで改善できると思う。そうすると、こんな感じになるかもね: $result = $arr |> select_column('tags') // 配列の配列を取得する |> fn($x) => array_merge(...$x) // 一つの大きな配列にフラット化する |> array_unique // 重複を削除する |> array_value // 配列のインデックスを再設定する。

標準ライブラリがこんなに一貫性がないと、地獄を見ることになるよ。オプションで、より良い言語だと、引数の順番がどうなるか分かるけど(array_map / array_filter)、PHPでは運任せだし。これって、全然標準ライブラリに合ってない気がする。PHPの開発者はまず、完全なUnicodeサポートに集中すべきだと思う(いや、mb_real_uppercaseじゃダメだよ)、その後で新しい名前空間の標準ライブラリに取り組むべき。

これだね。もっと良い標準ライブラリが必要だし、適切なデータ構造も必要だと思う。

標準ライブラリが一貫性がないから、これは悪夢になるよ。 コール可能なものはこの文脈では役に立たなくなると思うし、みんながクロージャをパイプで使って、$xを標準ライブラリが求める場所に置くことになると思う。

パイプ演算子が大好きだな。 Elixirの良いところの一つだけど、他の言語にもあるよね。 考えやすくて楽だよ: $result = $arr |> fn($x) => array_column($x, 'tags') |> fn($x) => array_merge(...$x) |> array_unique(...) |> array_values(...) VS array_values(array_unique(array_merge(...array_column($arr, 'tags'))));

これが考えにくいとは思えないな。 変数を使った場合の結果コードはこうなるよ: $tags = ...array_column($arr, 'tags'); $merged_tags = array_merge($tags); $unique_tags = array_unique($merged_tags); $tag_values = array_values($unique_tags); 各ステップの後に値を確認するのも楽になるしね。

すごく考えやすいよ 本当にそうかな?私はそうは思わないけど。

PHPって、誰も褒めたがらない変な言語だけど、使いこなせる人にはすごくうまく機能するんだよね。私自身は触ることはないと思うけど、使える言語がたくさんあるし、今知ってることだけで十分仕事できるから。でも、私の周りではメインストリームじゃないPHPみたいな言語が進化していくのを見るのはすごく楽しみだよ。

あなたにやってみてほしいわけじゃないけど、あなたの視点を考えると、もしある日クルード+アプリが必要になって、Laravelを使おうとしたら、現代のPHPが実際にどうなってるかに驚くかもしれないよ。以前はこの言語とそのエコシステムがダメになってると思ったけど、そこから復活して、現代のPHPは90%がやりたいことを実現できて、どうやるかは心配しなくていい、簡単だから。最近はあまり使わないけど、使うたびに可能性を感じるよ。

例にラムダが必要なのには驚いた… |> foo(...) の構文の意味は何なの?関数がちょうど一つのオペランドを取らなきゃいけないなら、これを書く必要があるの? $arr |> fn($x) => array_column($x, 'tags') これがなぜ動かないの? $arr |> array_column(..., 'tags') そして、これが動かないときは、なぜこれもダメなの? $arr |> array_unique

これは、関数内でチェーンされた値を適切な位置に挿入するためのものなんだって。エリクサーにはもう少しおしゃれなバージョンがあるらしいけど、たぶんそれに関して言ってるんだと思う(エリクサーは引数が1以上の関数をファーストクラスでサポートしてるから)。

どうやら「foo(...)」は、記事にリンクされている「ファーストクラスコール可能」RFC [1] によると、PHPの関数参照の構文らしい。Pythonでは例えば callbacks = [f, g] って書くところを、PHPでは $callbacks = [f(...), g(...)]; って書かなきゃいけない。全体的な機能の目的については、記事の最後で言及されているように、関数合成で置き換えられるかもしれないけど、関数合成は専用の構文ではなくユーティリティ関数で実装できる。これらの演算子を追加する利点は、どうやら [2] パフォーマンス(関数呼び出しが少なくなる)と静的型チェックを促進することらしい。 [1] https://wiki.php.net/rfc/first_class_callable_syntax [2] https://wiki.php.net/rfc/function-composition#why_in_the_eng...

パイプオペレーター |> を見た最初の型付きプログラミング言語はF#だった。こんな感じで書けるんだ: sum 1 2 |> multiply 3 これが動くのは、|>が左側の式の出力を右側の関数の最後のパラメータとして渡すから。multiplyはこう定義しなきゃいけない: let multiply b c = b * c これでbが3になり、cにはsum 1 2の結果が入る。右側もラムダ式にできるよ: sum 1 2 |> (fun x -> multiply 3 x) これは単なる構文糖ではなく、実際には標準ライブラリでこう定義されてる: let (|>) x f = f x 関数合成のために、F#は>>(前方合成)と(後方合成)を提供していて、それぞれこう定義されてる: let (>>) f g x = g (f x) これを使って再利用可能な合成関数を作れるよ: let add1 x = x + 1 let multiply2 x = x * 2 let composed = add1 >> multiply2 F#は本当に美しい言語だね。M$がこの言語への投資をやめてしまったのは残念だし、(型付き)関数型プログラミング言語全般にあまり興味がないのも悲しい。

F#は素晴らしい言語だよ。ツールやエコシステム、コンパイル時間が理由で使ってないけどね。OCamlと一緒に学んで、OCamlのコンパイル速度に甘やかされちゃった。F#が一級市民になれなかったのは本当に残念だね。

PHPコミュニティでこの議論をしたことがあるけど、構文が読みにくくて、理解するために戻って読み直さなきゃいけないと思う。書くのは簡単かもしれないけどね。 知らないコードをスキャンして、シンボルを特定しようとしていると想像してみて。入力と出力を理解しようとすると、こんな感じになる。 $result = $arr |> fn($x) => array_column($x, 'values') |> fn($x) => array_merge(...$x) |> fn($x) => array_reduce($x, fn($carry, $item) => $carry + $item, 0) |> fn($x) => str_repeat('x', $x); 自分が書いてない大きなコードのセクションを読んでいると想像してみて。この操作を見て、「result」が何か理解できる? すぐに最後の行に目が行って、戻り値の型を確認する?最初に知りたいのは、一般的に$resultsが何かってことだよね。実はそれは文字列なんだけど、それを知るには最後の行まで飛ばさなきゃいけない。コードを理解する時は、そこにたどり着くまでの道よりも、目的地の方が重要なんだ。これを理解するには、逆から読む必要がある。古い考え方かもしれないけど、いくつかの変数が何をしているのかを定義する自己文書化の性質は、メンテナブルなコードを書くために重要だと思うし、メンテナの認知負荷を下げるのに役立つと思う。 $values = array_merge(...array_column($arr, 'values')); $total = array_reduce($values, fn($carry, $item) => $carry + $item, 0); $result = str_repeat('x', $x);

君の考えには賛成だけど、このパイプは適切な名前の関数の中にあった方がいいと思ってたよ(少なくとも、Elixirではそう使うかな)。

Dの統一関数呼び出し構文を思い出すな。これを使うと、bar(foo(sort(myArray)))myArray.sort().foo().bar()に書き換えられるんだ。