PHP
無名関数
クロージャ

phpのクロージャ・無名関数の使いどころについて考えてみた

導入

phpに無名関数が実装されていることは知っていましたが、いまいち使いどころが思いつかず、モヤモヤしていました。javascriptでは頻繁に使っているはずですが、phpでは使用例を含めてなかなかいい記事が見つかりませんでした。
つい最近コードレビュー中にふと思い浮かんだので共有します。使い方として微妙な部分もあると思いますので、より良いアイデアをお持ちの方はコメントいただけると幸いです。

無名関数とは

まず、前提として無名関数について軽く触れておきましょう。

無名関数とはphp5.3より導入された機能で、公式ドキュメントでは以下のように記述されています。

無名関数はクロージャとも呼ばれ、 関数名を指定せずに関数を作成できるようにするものです。 コールバック パラメータとして使う際に便利ですが、用途はそれにとどまりません。

使い方

まず、代表的な使用例としてはphp標準関数のarray_mapでしょう。この関数は配列にコールバック関数を適用し、新しい配列を生成する関数です。
第一引数にcallback関数、第二引数にコールバック関数を適用するarray(この配列の各要素にcallback関数が適用されます)、それ以降にはコールバック関数への引数として与えるための配列を渡すことができます。通常は第二引数と同じ要素数の配列を渡しますが、異なる要素数の配列を渡すことも可能で、その場合、大きい方の配列の要素数だけコールバック関数が呼ばれます。

example1.php
// 変数に無名関数を入れたものをラムダ関数と呼びます
$area = function ($x, $y) {
    return 'Area is: ' . $x * $y;
}

$x = [1, 3, 5, 7, 9];
$y = [2, 4, 6, 8, 10];

var_dump(array_map($area, $x, $y));

Array
(
    [0] => Area is: 2
    [1] => Area is: 12
    [2] => Area is: 30
    [3] => Area is: 56
    [4] => Area is: 90
)

使い方というよりは、「使われ方」の説明のような気もしますが、数少ない代表的な登場シーンだと思います。

では次に、実際に遭遇したコードで無名関数が役立ちそうな形を紹介します。

無名関数が使えそうなシーン

遭遇したのは、ある処理を実行し失敗した場合、定義回数分だけリトライするようなシーンでした。対象クラス内のほとんどすべてのメソッドにリトライのために重複したコードが書かれていたので、DRYに書ける方法を考えていました。

example2.php
class InputOutput
{
    const RETRY_COUNT = 10;

    protected $manager;

    public function get()
    {
        for ($i = 0; $i < self::RETRY_COUNT; $i++) {
            $data = $this->manager->get();
            if ($data !== null) {
                return $data;
            }
        }

        throw new \Exception();
    }

    public function add() 
    {
        for ($i = 0; $i < self::RETRY_COUNT; $i++) {
            $this->manager->add();
            if ($this->manager->getErrorCode() === 0) {
                return;
            }
        }

        throw new \Exception();
    } 
}

コードはある程度簡略化していますが、だいたい上のようなリトライ処理が全メソッドに記述されていました。
そこで、上のコードを次のように書き換えてみてはどうでしょうか。

example3.php
class InputOutput
{
    const RETRY_COUNT = 10;

    protected $manager;

    public function get()
    {
        $try = function () {
            $this->manager->get();
            if ($data !== null) {
                return $data;
            }
            return new \Exception();
        }
        $this->retry($try);
    }

    public function add()
    {
        $try = function () {
            $this->manager->add();
            if ($this->manager->getErrorCode() === 0) {
                return;
            }
            return new \Exception();
        }
        $this->retry($try);
    }

    // 共通のリトライ処理を別メソッドとして用意しておき、それぞれの処理を噛ませる。
    // Traitなどで用意しておくとグローバルに再利用可能
    public function retry(callable $try)
    {
        // リトライの回数が10なので、最大計11回トライすることになります
        for ($i = 0; $i <= self::RETRY_COUNT; $i++) {
            $result = $try();
            if (!( $result instanceof Exception )) {
                return $result;
            }
        }

        // throwの前に共通のlog処理などを入れてもOK
        Log::error('error!!!!!!!!');

        throw $result;
    }
}

まとめ

いかがでしょうか。今回は具体例としてはあまり良いものではないかもしれませんが、うまくコードをまとめることができました。
このような関数を高階関数?と呼ぶようで、純粋な無名関数・クロージャの利用というよりは「phpで高階関数を実装した」という方が正しいかもしれません。