パイプ演算子の 実践ビフォー・アフター ~コードはどう変わっていくのか 1
PHP8.5 からパイプ演算子 |> が導入 公式の説明文 PHP 8.5 以降では、callable に直接値を渡す演算子をサポートしています。 |> 演算子、または "パイプ" は、 右辺にパラメーターをひとつ取る callable を受け入れ、 左辺値をそれに渡し、callable の結果を評価します。 右辺の callable は、有効な PHP の callable であれば何でも構いません: つまり、Closure、 第一級callableを生成する記法、 __invoke() を実装したオブジェクトなどです。 公式マニュアルより抜粋: https://www.php.net/manual/ja/language.operators.functional.php 2
今回の紹介内容 パイプ演算子(|>)が導入されて ・何が変わるのか? ・何が嬉しいのか? この点を紹介します 3
自己紹介 発表者 所属 ma@me 最近の業務 不具合分析 不具合対応 品質改善業務をメインに色々やっ てます 4
目次 1. パイプ演算子の基本構文 2. パイプ演算子がもたらす変化 3. パイプ演算子がもたらす3つのメリット 4. 使いどころの提案 5
パイプ演算子の基本構文 6
パイプ演算子の基本構文 従来のPHP 文章中のスペースを取り除くコード $text = "Hello World"; $trimmedText = trim($text); echo $trimmedText; 7
パイプ演算子の基本構文 パイプ演算子無し $text = "Hello World"; $trimmedText = trim($text); echo $trimmedText; パイプ演算子あり その1 $text = "Hello World"; $trimmedText = $text |> trim(...); echo $trimmedText; パイプ演算子あり その2 $text = "Hello World"; $trimmedText = $text |> ($x => trim($x)); echo $trimmedText; 8
パイプ演算子の基本構文 ...はプレースホルダ パイプ演算子 (|>) を軸に、左辺の結果を右辺の関数の引数として渡す目印 $text = "Hello World"; $trimmedText = $text |> trim(...); echo $trimmedText; 変数名を明示的に書きたい場合 プレースホルダを使わずに場合はクロージャーを使用する $text = "Hello World"; $trimmedText = $text |> ($x => trim($x)); echo $trimmedText; 9
パイプ演算子の基本構文 PHPのパイプ演算子 |> は、最近生まれた新しい概念では無い 古くはLinuxのパイプ | と同じ概念 ある処理の結果を次の処理へ渡す、という考え方 Linuxコマンドで試してみるのも1つの手段 Linux のパイプの例 abcを含むファイル/ディレクトリを検索する # 'abc' を含むファイル/ディレクトリを検索 ls | grep abc 10
パイプ演算子がもたらす変化 11
パイプ演算子がもたらす変化 構文を理解した上で、次にどんな変化が起こるか、に着目 主に以下の2点で大きな変化が発生 関数呼び出しのネストが減る 2. 実行順序とコードの流れが一致する(宣言的になる) 1. 12
1. 関数呼び出しのネストが減る 従来のPHP 複数関数を連続適用する場合、関数のネストが深くなる $result = htmlspecialchars(strtoupper(trim("hello world"))); こう書いたり $result = htmlspecialchars( strtoupper( trim("hello world") ) ); こう書いたり $temp = trim("hello world"); $temp = strtoupper($temp); $result = htmlspecialchars($temp); 13
1. 関数呼び出しのネストが減る パイプ演算子を使う このようにネストが統一されて、見通しが良くなる $result = "hello world" |> trim(...) |> strtoupper(...) |> htmlspecialchars(...); or $result = "hello world" |> ($x => trim($x)) |> ($x => strtoupper($x)) |> ($x => htmlspecialchars($x)); 14
実行順序とコードの流れが一致する 従来のコード 2. $result = htmlspecialchars(strtoupper(trim("hello world"))); 実行順序とコードの流れが逆転している 実行順序 ⇦⇦⇦ 右から左へ 1. `trim("hello world")` 2. `strtoupper(...)` 3. `htmlspecialchars(...)` コードの流れ 左から右へ ⇨⇨⇨ 1. `htmlspecialchars` 2. `strtoupper(...)` 3. `trim("hello world")` 15
実行順序とコードの流れが一致する パイプ演算子を使ったコード 2. $result = "hello world" |> trim(...) |> strtoupper(...) |> htmlspecialchars(...); 実行順序とコードの流れが一致している 実行順序 ⇩ 上から下へ 1. `trim(...)` 2. `strtoupper(...)` 3. `htmlspecialchars(...)` コードの流れ ⇩ 上から下へ 1. `trim(...)` 2. `strtoupper(...)` 3. `htmlspecialchars(...)` 16
パイプ演算子がもたらす3つのメリット パイプ演算子をつかうことで、どのように書けるか掴めたでしょうか 使うことで生まれる具体的なメリットを3つ、取り上げます メリット 1. 中間変数のコスト削減 2. 手続きから宣言的へ 3. 型安全な処理フロー構築 17
パイプ演算子がもたらす 3つのメリット 18
中間変数のコスト削減 従来のコード 1. $temp = trim("hello world"); $temp = strtoupper($temp); $result = htmlspecialchars($temp); 例えば、$temp や $data のような安易な名前を付けると、以下のような議論が起き やすいです もっと分かりやすい名前にできないか? もっといい名前があるのでは? ネストが深くなってもいいので、中間変数を無くせないか? etc... 19
中間変数のコスト削減 名前(命名)は大事だけに、議論が尽きない 1. かといって、中間変数を省くと適用する関数の分だけネストが深くなる 例 $result = htmlspecialchars( substr( mb_convert_encoding($str, strtoupper( trim("hello world") ) , 'UTF-8'), 0, 100 ) ); 20
中間変数のコスト削減
パイプ演算子を使って中間変数を減らす
1.
$str = "hello world"
$result = $str
|> trim(...)
|> strtoupper(...)
|> (fn($s) => mb_convert_encoding($s, $toEncoding, 'UTF-8'))
|> substr(..., 0, 100)
|> htmlspecialchars(...);
適用関数が増えても中間変数は無く、ネストも深くならない
21
手続きから宣言的へ 「文」中心の書き方(手続き的) 2. 実装例:乗車中の乗客の行動を追うプログラム $乗客の行動 = '乗車'; $乗客の行動 = 座席を探す($乗客の行動); $乗客の行動 = 読書する($乗客の行動); $乗客の行動 = 降車準備($乗客の行動); $最終状態 = 降車する($乗客の行動); return $最終状態; 一つ一つの行動を順番に書き出し、実行する プログラムの最後まで読めば、何を求めたいプログラムなのかが分かる 22
2. 手続きから宣言的へ ただし、手続き的な書き方には以下のようなデメリットも。。。 開発中に仕様追加が発生 乗車時間を考慮する必要が出てきた 乗車時間に加えて、電車の遅延も考慮したくなった etc... 23
2. 手続きから宣言的へ 1~5のどこに加えてもOKが故に、コードが複雑化しがち $乗客の行動 = '乗車'; // 追加仕様を書ける箇所1 $乗客の行動 = 座席を探す($乗客の行動); // 追加仕様を書ける箇所2 $乗客の行動 = 読書する($乗客の行動); // 追加仕様を書ける箇所3 $乗客の行動 = 降車準備($乗客の行動); // 追加仕様を書ける箇所4 $最終状態 = 降車する($乗客の行動); // 追加仕様を書ける箇所5 return [$最終状態, $乗車時間]; 24
手続きから宣言的へ 「式」中心の書き方(宣言的) 2. パイプ演算子を使うことで、それぞれの仕様を書く箇所を明確に決める $最終状態 = '乗車' |> 座席を探す(...) |> 読書する(...) |> 降車する(...); 25
2. 手続きから宣言的へ 開発中に仕様追加が発生 乗車時間を考慮する必要が出てきた 乗車時間に加えて、電車の遅延も考慮したくなった $最終状態 = '乗車' |> 座席を探す(...) |> 読書する(...) |> 降車する(...); $合計乗車時間 = 0 |> fn($分) => $分 + 1 // 座席探し |> fn($分) => $分 + 15 // 読書 |> fn($分) => $分 + $遅延時間; // 遅延を考慮 return [$最終状態, $合計乗車時間]; 26
2. 手続きから宣言的へ 仕様が増えても挿入箇所が明確なのでコードが複雑化しづらい $最終状態 = '乗車' |> 座席を探す(...) |> 読書する(...) |> 降車する(...); // 1:状態に関する仕様ならここに追加する $合計乗車時間 = 0 |> fn($分) => $分 + 1 // 座席探し |> fn($分) => $分 + 15 // 読書 |> fn($分) => $分 + $遅延時間; // 遅延を考慮 // 2:乗車時間に関する仕様ならここに追加する $他の仕様 = ...; // 3:他の仕様ならここに追加する 27
3.
型安全な処理フロー構築
これまでのPHPであれば、フローの構築にメソッドチェーンを見ることがあった
(new Post(['Title', 'Content']))
->validator()
->save();
new Post {
function validate(array $paramater): PostRepository {~} };
new PostRepository{
function save(
ここに引数を置けないので、Validator::validateとの繋がりが不明瞭
): bool {~} };
(スペースの都合上、アクセス修飾子や詳細な実装は省略しています)
28
型安全な処理フロー構築
メソッドチェーンの課題
3.
しかし次のような課題も抱えることになり、結構使いづらかった
実行淳を表現するあまり、違和感のあるメソッドが生まれがち
例:バリデーションの結果、リポジトリが生まれる?
function validate(array $paramater): PostRepository {~} };
一度決まったフローの順番を変えるのが大変
例:validateの後にmappingを入れたくなったけれど、validateメソッドの戻り値
も変えて影響範囲を見ないと。。。
(new Post(['Title', 'Content']))
->validator()
->mapping() // ここに入れたいが、影響範囲が大きい
->save();
29
型安全な処理フロー構築
パイプ演算子で解決する
3.
引数渡しなので、処理順序の変更にも強い!
引き渡したいデータの型を明確にできる
処理を繋げやすい
new Validator {
function validate(Post $post, array $options = []): ValidatedPost {~} };
new PostRepository{
function save(ValidatedPost $post): bool {~} };
$isSaved = (new Post())
|> (fn($post) => $validator->validate($post, ['strict' => true]))
|> $repository->save(...);
30
3. 型安全な処理フロー構築 フローを追加したくなった場合、追加しやすく、追加箇所も制限できる $isSaved = (new Post()) |> (fn($post) => $validator->validate($post, ['strict' => true])) |> (fn($post) => $converter->convert($post, ['allow_bold' => true])) |> $repository->save(...); 31
使いどころの提案 32
使いどころの提案 合意が取れたのなら積極的に使っていいのでは 考えられる課題 エディタやIDEのサポート状況が未知数 入ったばかりの機能なので、仕様変更が入り易い 既存コードとの兼ね合い パイプ演算子への忌避感 33
懸念点はあるけれど、思ったより良さそう、パイプ演算子 ご清聴ありがとうございました! 34