y_uti のブログ

統計、機械学習、自然言語処理などに興味を持つエンジニアの技術ブログです

PHP の配列を操作する

PHP Advent Calendar 2015 - Qiita の 12/22 の記事が話題になっていました。このアドベントカレンダーは毎日チェックしているのですが、通勤前に読んで、これは盛り上がるだろうな、と思いながら夕方に再度アクセスしたところ案の定という展開で、寄せられているコメントも合わせて楽しませてもらいました。
qiita.com

記事では、PHP のイマイチな点をいくつか指摘した後、PHP でプログラムを書くときの指針として、標準の関数ではなくライブラリやフレームワークを利用するという意見が述べられています (引用部分)。記事へのコメントなどを見ても、この意見には賛成という方が多いようです。

もう少しスマートな方針としては 標準関数を利用せず、それをラップした外部ライブラリを利用するのが良いでしょう。

そこで今回は、標準関数をラップする簡単なコードを自作して、PHP でも簡潔にプログラムを書けることを確かめてみます。元記事にある次のコードを例題にします。

#rubyの場合
p [2,4,6,8,10]
  .select{|num| num <= 8; } # 1. ↑の配列から8以下の数だけを選択した配列を作る
  .map{|num| num**2 }       # 2. ↑の配列から各要素を2乗した配列を作る
  .select{|num| num >= 20 } # 3. ↑の配列から20以上の数だけを選択した配列を作る
  .reduce(:*)               # 4. ↑の配列の値を掛け合わせる
PHP の配列をラップする

まず、PHP の配列をラップするクラスを定義して、Ruby の実装と同様にメソッドチェーンで処理する方法を試します。PHP の配列はオブジェクトではないので、Ruby の配列のようにメソッドを持つこともなく、当然メソッドチェーンも利用できません。

そこで次のように、PHP の配列と配列関数をラップする独自のクラスを作成してみます。コンストラクタの引数として PHP の配列を渡します。渡された配列は内部のフィールドに隠蔽されます*1。例題のコードで利用される各操作をメソッドとして定義し、それぞれ内部で PHP の配列関数を呼び出して処理を行います。filter, map では、配列関数の戻り値を再び MyArray オブジェクトにして、クラスの外部に PHP の配列を露出しないようにします。product*2PHP の整数を戻します*3

<?php
class MyArray
{
    public $array;
    function __construct($array) {
        $this->array = $array;
    }
    function filter($f) {
        return new MyArray(array_filter($this->array, $f));
    }
    function map($f) {
        return new MyArray(array_map($f, $this->array));
    }
    function product() {
        return array_product($this->array);
    }
}

作成した MyArray クラスを利用すれば、例題のコードは PHP でも次のように簡単に書けます。

<?php
echo (new MyArray([2, 4, 6, 8, 10]))
    ->filter(function ($num) { return $num <= 8; })
    ->map(function ($num) { return $num * $num; })
    ->filter(function ($num) { return $num >= 20; })
    ->product();

実装したコードを見てみると、PHP の配列関数である array_filter, array_map, array_product の呼び出しは MyArray クラスのメソッドに閉じ込められ、MyArray クラスを利用するコードから直接呼ばれることはないことがわかります。MyArray という「フレームワークが提供するクラス」を利用することで、より分かりやすいコードを書けるようになりました。

配列の操作を無名関数で表現する

別のアプローチとして、配列に対する操作をそれぞれ無名関数で表現し、それらの一連の無名関数に array_reduce を適用する方法を実装してみます。先ほどの実装がオブジェクト指向的な方法だとすれば、こちらは関数的な方法だと言えるでしょうか。

まず、次のような関数を定義します。たとえば、filter は関数 $f を受け取り「配列 $array を受け取って array_filter($array, $f) の結果を戻す関数」を戻します。

<?php
function filter($f) {
    return function ($array) use ($f) { return array_filter($array, $f); };
}
function map($f) {
    return function ($array) use ($f) { return array_map($f, $array); };
}
function product() {
    return function ($array) { return array_product($array); };
}

これらの関数を用いると、例題の処理を次のように実装できます。一見すると簡潔になっている気もしますが、関数をネストしているため、内側から外側に向かって処理が進む構造になっています。短くなったこともあり何となく読めてしまいますが、このコードは元記事にある PHP のコードと同等の構造を持っています。

<?php
$le8 = filter(function ($num) { return $num <= 8; });
$square = map(function ($num) { return $num * $num; });
$ge20 = filter(function ($num) { return $num >= 20; });
$product = product();

echo $product($ge20($square($le8([2, 4, 6, 8, 10]))));

ネストした関数の内側から外側に向かって処理が流れるというのは、あまり読みやすいコードではないと感じます。そこで、この構造を変えるために次のような関数を定義します。array_reduce を利用して、一連の操作を順番に適用する仕組みを実装したものです。

<?php
function chain($initialArray, $functions) {
    return array_reduce(
        $functions,
        function ($array, $f) { return $f($array); },
        $initialArray);
}

これを使うと例題の処理は次のように書けます。$le8 から $product に向かって、左から右の方向に読めるコードになりました。

<?php
echo chain([2, 4, 6, 8, 10], [$le8, $square, $ge20, $product]);
中間変数を利用する

さて、元記事を改めて見てみると、例題を提示している箇所に次のような記述があることに気がつきます。

以下のような処理を配列への中間変数を用いず行うコードを例に考えてみます。

実際のところ、今回の例題のような処理で中間変数*4の使用をためらう理由は特にないように思います。中間変数を使って書けば、次のような実装になるでしょうか。関数名がいちいち array_* だったり、ラムダ式をすっきりと書ける記法がなかったりと、見た目はどうにも冴えないコードですが、それ以上の問題は特になさそうです。

<?php
$array = [2, 4, 6, 8, 10];
$array = array_filter($array, function ($num) { return $num <= 8; });
$array = array_map(function ($num) { return $num * $num; }, $array);
$array = array_filter($array, function ($num) { return $num >= 20; });
$answer = array_product($array);
echo $answer;

このコードから中間変数を除去すれば以下のようになり、元記事で挙げられているものと同様なコードが得られます。これをスラスラ読める人もいるのかもしれませんが、私は、愚直に中間変数を用いて書かれた先ほどのコードの方が読みやすいと思います。

<?php
echo array_product(
    array_filter(
        array_map(
            function ($num) { return $num * $num; },
            array_filter(
                [2, 4, 6, 8, 10],
                function ($num) { return $num <= 8; })),
        function ($num) { return $num >= 20; }));

ここでマーチン・ファウラー氏の書籍『リファクタリング』を参照してみると、「一時変数のインライン化」という項目と「説明用変数の導入」という項目が両方示されています*5。一時変数のインライン化は、不必要な一時変数を除去する方向のリファクタリングを指している一方で、説明用変数の導入は、その逆に、複雑な式の部分的な結果を一時変数に代入するリファクタリングを指しています。それぞれにメリットデメリットがあり、プログラムを書くときにはどちらの極端にも寄らず適度なバランスが求められるのが難しいところですね。
Amazon.co.jp: リファクタリング―プログラムの体質改善テクニック (Object Technology Series): マーチン ファウラー, Martin Fowler, 児玉 公信, 平澤 章, 友野 晶夫, 梅沢 真史: 本

*1:このコードでは public にしているので実際は隠蔽されていません。本来は private にしてアクセサを適切に定義すべきです。

*2:元記事では array_reduce が使われていますが、PHP には array_product があるので、今回の例題の処理で array_reduce は使わないと思います。

*3:厳密には、これも MyInteger といったクラスでラップすべきかもしれませんが、面倒なのでそこまでは実装しませんでした。

*4:私の今回の記事では、一連の処理において、途中の状態を一時的に保持する目的で導入される変数、という意味で使っています。

*5:新装版が出版されているようですが、手元にあるのは初版なので新装版とは目次が違っているかもしれません。