Laravelのコレクション(その6:要素の追加・削除)

0
1143

Laravelコレクション掘り下げシリーズ第六弾。今回は要素の追加・削除を行うメソッドに関して確認していきます。

concat

$col1 = collect([1,2,3]);
$col2 = $col1->concat([4,5,6]);

元のコレクションの末尾に引数で指定された配列(コレクション)に含まれる複数要素を追加します。
結果は以下の通り。

array:6 [
  0 => 1
  1 => 2
  2 => 3
  3 => 4
  4 => 5
  5 => 6
]

追加側が連想配列でも実行可能です。

$col1 = collect([1,2,3]);
$col2 = $col1->concat([
    'a' => 4,
    'b' => 5,
    'c' => 6
]);

結果は以下の通り。

array:6 [
  0 => 1
  1 => 2
  2 => 3
  3 => 4
  4 => 5
  5 => 6
]

キーは無視されるようです。

では、元コレクション側が連想配列だった場合はどうでしょう。

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col2 = $col1->concat([
    'd' => 4,
    'e' => 5,
    'f' => 6
]);

結果は以下の通り。

array:6 [
  "a" => 1
  "b" => 2
  "c" => 3
  0 => 4
  1 => 5
  2 => 6
]

追加側のキーは無条件に失われるようです。

forget

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col1->forget('b');

指定したキーの要素を削除します。
結果は以下の通り。

array:2 [
  "a" => 1
  "c" => 3
]

多次元構造に関しても試してみましょう。

$col1 = collect([
    'a' => [
        'a-1' => 1,
        'a-2' => 2,
    ],
    'b' => [
        'b-1' => 3,
        'b-2' => 4,
    ],
    'c' => [
        'c-1' => 5,
        'c-2' => 6,
    ],
]);
$col1->forget('b.b-1');

結果は以下の通り。

array:3 [
  "a" => array:2 [
    "a-1" => 1
    "a-2" => 2
  ]
  "b" => array:2 [
    "b-1" => 3
    "b-2" => 4
  ]
  "c" => array:2 [
    "c-1" => 5
    "c-2" => 6
  ]
]

二次元以下の要素の指定はできないようです。

pad

$col1 = collect([1,2,3]);
$col2 = $col1->pad(5, 0);

第一引数で指定した要素数になるまでパディングを行います。不足分は第二引数で指定した値で埋めます。
結果は以下の通り。

array:5 [
  0 => 1
  1 => 2
  2 => 3
  3 => 0
  4 => 0
]

第一引数にマイナスの値を指定すると、後ろ詰めにします。

$col1 = collect([1,2,3]);
$col2 = $col1->pad(-5, 0);

結果は以下の通り。

array:5 [
  0 => 0
  1 => 0
  2 => 1
  3 => 2
  4 => 3
]

第二引数には数値や文字以外も指定できそうです。

$col1 = collect([1,2,3]);
$col2 = $col1->pad(5, [
    'a' => 1,
    'b' => 2,
]);

結果は以下の通り。

array:5 [
  0 => 1
  1 => 2
  2 => 3
  3 => array:2 [
    "a" => 1
    "b" => 2
  ]
  4 => array:2 [
    "a" => 1
    "b" => 2
  ]
]

本体配列が連想配列のコレクションに対しても適用可能です。

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col2 = $col1->pad(5, 0);

結果は以下の通り。

array:5 [
  "a" => 1
  "b" => 2
  "c" => 3
  0 => 0
  1 => 0
]

まぁ、連想配列に対してこのような使い方はしないでしょうけど。

prepend

$col1 = collect([1,2,3]);
$col2 = $col1->prepend(4);

元のコレクションの先頭に引数で指定された要素を追加します。
結果は以下の通り。

array:4 [
  0 => 4
  1 => 1
  2 => 2
  3 => 3
]

インデントが振り直される点に要注意です。

第二引数にキーを指定することもできます。

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col2 = $col1->prepend(4, 'plus');

結果は以下の通り。

array:4 [
  "plus" => 4
  "a" => 1
  "b" => 2
  "c" => 3
]

追加する要素は数値や文字以外でも良いです。

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col2 = $col1->prepend([
    'plus-1' => 4,
    'plus-2' => 5,
    'plus-3' => 6,
], 'plus');

結果は以下の通り。

array:4 [
  "plus" => array:3 [
    "plus-1" => 4
    "plus-2" => 5
    "plus-3" => 6
  ]
  "a" => 1
  "b" => 2
  "c" => 3
]

push

$col1 = collect([1,2,3]);
$col2 = $col1->push(4);

元のコレクションの末尾に引数で指定された要素を追加します。prependの逆ですね…と言っておきます(意味深)。
結果は以下の通り。

array:4 [
  0 => 1
  1 => 2
  2 => 3
  3 => 4
]

prependと同様にキーの指定も試してみましょう。

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col2 = $col1->push(4, 'plus');

結果は以下の通り。

array:4 [
  "a" => 1
  "b" => 2
  "c" => 3
  0 => 4
]

キーは無視されてしまいました…
根本的に第二引数に意味はないのでしょう。
コレクションに関して対称的な位置付けのメソッドのインタフェースおよび機能の不整合(非対称性)は時折見かけますが、これは何なんでしょうね?

一応、数値や文字以外の要素の追加も試してみます。

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col2 = $col1->push([
    'plus-1' => 4,
    'plus-2' => 5,
    'plus-3' => 6,
]);

結果は以下の通り。

array:4 [
  "a" => 1
  "b" => 2
  "c" => 3
  0 => array:3 [
    "plus-1" => 4
    "plus-2" => 5
    "plus-3" => 6
  ]
]

こちらは問題なさそうです。

put

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col2 = $col1->put('plus', 4);

第一引数にキー、第二引数に値を指定することで、そのセットをコレクションの末尾に追加します。
結果は以下の通り。

array:4 [
  "a" => 1
  "b" => 2
  "c" => 3
  "plus" => 4
]

つまりpushで実現できなかったキーを指定しての要素追加はこちらで可能なんですが、これって別メソッドにしないとダメなんですかね?
しかもキーと値の順序が逆になるので、この辺も間違いを助長するように思います。

蛇足ながら、数値や文字以外の要素の追加も試してみます。

$col1 = collect([
    'a' => 1,
    'b' => 2,
    'c' => 3
]);
$col2 = $col1->put('plus', [
    'plus-1' => 4,
    'plus-2' => 5,
    'plus-3' => 6,
]);

結果は以下の通り。

array:4 [
  "a" => 1
  "b" => 2
  "c" => 3
  "plus" => array:3 [
    "plus-1" => 4
    "plus-2" => 5
    "plus-3" => 6
  ]
]

と言うことで問題ないんですが、先に書いたようにprepend,push,putのバランスの悪さの印象が強過ぎてこの辺どうでも良くなってきます。

結論としては、元コレクションの本体配列が配列(キーがインデックス)であればpushを、連想配列であればputを使いましょう、ってことになるかと思いますが、しばしば触れるようにPHPでは配列と連想配列の境界は曖昧(配列は連想配列のキーを連番に限定した特殊パターン)と言うことから考えると、両者を区別しないのがデフォルトではなかったんでしょうかね?

総括

今回はprepend,push,putの三角関係に全て持って行かれた印象です。
Laravelは多数の便利機能を有しますが、時折不思議なインタフェースも見かけますし、特に上記のように対称的な機能を比較した場合に何故こうなった?と思う不整合もしばしば見受けられます。
何となく上位互換性を保ちながら機能追加してきた過程でこのような不整合が生じたのではないかと推測しますが。

なお、メソッド名から見た場合pushはpop、putはgetとも対になりそうな気がします。先の対称性が要素の挿入位置に着目したものであれば、こちらは入出力の観点での対称性と言うことになります。ただ、popは配列(キーがインデックス)のみを対象としているような機能ではありませんのでputの対とも見なせますし、push,putが末尾への要素追加であるのに対しpopが末尾からの削除を伴う要素取得と言うことで要素の位置や増減と言う意味でも対称性があるのに対し、getは要素の削除を伴ないませんし取得する要素の位置もコレクションの末尾に限定する機能ではないので、これをput(push)の対となる機能と捉えること自体に無理があるような印象です。

となると、私的結論としてはpopの対象としてpushが存在し、pushが現在のputの機能(キーを含む要素の追加)を包含していると一番整合性があるように思うのですが。この形であればpopとの対称性でもprependとの対称性でも落ち着きが良いように思います。難を言えば、名称的に「prepend」の対は「append」だと分かりやすいのですが、前述したように挿入位置か入出力かの視点の違いがある中で両方の視点でぴったりの命名はなかなか難しいと思うので、この辺は妥協するしかないかと。
と言ってもあくまで仮定の話に過ぎませんが…

どのような機能を用意するか、およびその機能を何と命名するかはなかなかに深いテーマですが、Laravelでもこの辺は苦戦しているような印象です。