😸

PHPと無名関数〜引数に関数を渡したい〜

2022/03/09に公開

PHPと無名関数

「引数に関数を渡したら良い...」
「それがPHPでうまく出来ないんですよ...」
という会話を近所の人がしていたので、
PHPにおける無名関数と通常の関数・メソッドの違いについて。

PHPにおける無名関数と関数の違い

次の2つを理解しておけばPHPの関数と無名関数について完全に理解したと言えるでしょう。

  • PHPには関数型と呼ばれるプリミティブな型はない
  • 関数は構文、無名関数はオブジェクトである

関数

ユーザー定義関数

関数は次のような構文で定義されます。

<?php
function foo($arg_1, $arg_2, /* ..., */ $arg_n)
{
    echo "関数の例\n";
    return $retval;
}

無名関数

無名関数

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

<?php
$greet = function($name)
{
    printf("Hello %s\r\n", $name);
};

$greet('World');
$greet('PHP');
?>

違い

関数の説明では 構文 と言っているのに対し、無名関数では クロージャとも呼ばれ...実装には Closure クラスを使っている と言っています。
なので次の2つは全くの別物です。

// 関数foo の定義
function foo() {
    echo "foo\n";
}

// 無名関数 (Closure クラス)
$foo = function () {
    echo "foo\n";
};

そしてPHPで関数を渡せば良いと行った場合、
通常は Closure のことを指し、関数名を直接指定することはできません。

function bar(Closure $f) {
    $f();
}

// ok
bar($foo);

// ok
bar(function () { echo "foo\n"; });

// ng
bar(foo);

面白いのは bar(foo); を実行しようとすると Undefined constant "foo" というエラーが出ることでしょうか。
これはクラスメソッドにおいても同じで、PHPは定数 foo を探しに行き、無いよというわけです。 function 構文は相手にされません。
PHPでは function 構文は値とみなされていないと解釈できます。

じゃあ無名関数の function は何なんだよ、という話なんですが、 無名関数 = Closure オブジェクト です。
無名関数は Closure オブジェクト唯一のコンストラクタ、構文糖になります。

Closure

Closure クラス

無名関数 を表すために使うクラスです。
無名関数は、Closure 型のオブジェクトを生成します。 このクラスにはメソッドが用意され、 生成した無名関数をさらにコントロールできるようになっています。

final class Closure {
/* メソッド */
private __construct()
public static bind(Closure $closure, ?object $newThis, object|string|null $newScope = "static"): ?Closure
public bindTo(?object $newThis, object|string|null $newScope = "static"): ?Closure
public call(object $newThis, mixed ...$args): mixed
public static fromCallable(callable $callback): Closure
}

PHPは関数型を表すために Closure クラスが用意しました。
このクラスの見どころは final classprivate __construct() と定義されていることです。
これによって誰もこの Closure クラスを継承できませんし、 Closure のコンストラクを直接呼び出せないので、
事実上無名関数によってでしか Closure クラスを生成できないことになります。
(ここでは Closure::fromCallable や PHP8.1 で追加された構文については無視しています)

function () { /* ... */ };new Closure() に該当すると認識すると良いでしょう。

引数に関数を渡したい

function foo() { /* ... */ }; を callback 関数として引数に渡したい場合、4つの方法があります。
個人的には2つ目の bar2(function () { foo(); }); か、
4つ目の bar2(foo(...)); をお勧めします。

<?php

function foo() {
  echo "foo\n";
}

function bar1($f) {
    // PHP 4
    call_user_func($f);
}

// PHP 5.3
function bar2(Closure $f) {
    $f();
}

bar1('foo');

bar2(function () { foo(); });

// PHP 7.1
bar2(Closure::fromCallable('foo'));

// PHP 8.1
bar2(foo(...));

call_user_func

PHP 4 から存在する記法です。
文字列で変数名を渡し、 call_user_func で呼び出します。
ただし、これは Closure が利用できない状況に限られるかと思います。
Closure を利用できる環境に置いて余程のことがなければ選ぶ必要がないです。

function bar1($f) {
    // PHP 4
    call_user_func($f);
}

bar1('foo');

無名関数によるwrap

PHP 8.1 未満の場合、この方法をお勧めします。
PHP 7.1 で Closure::fromCallable という記法がありますが、
エディタの補完機能によっては typo に気が付けない可能性があります。

// PHP 5.3
function bar2(Closure $f) {
    $f();
}

bar2(function () { foo(); });

Closure::fromCallable

PHP 7.1 で利用できるメソッドですが、前述の理由の通りお勧めしません。

// PHP 5.3
function bar2(Closure $f) {
    $f();
}

// PHP 7.1
bar2(Closure::fromCallable('foo'));

CallableExpr(...)

PHP 8.1 からこの構文を利用できます。
無名関数で wrap するよりもスッキリ書けるので、選択肢がある場合は選んでも良いのかなと思います。

// PHP 5.3
function bar2(Closure $f) {
    $f();
}

// PHP 8.1
bar2(foo(...));

おまけ: final class

Closurefinal class の利用方法を見て final class の使い所を言語化できたので記載。
よく「 final は無用な多段継承を防ぐために取り敢えず付けておけばいい」という主張の記事を見かけるのですが、この考え方はあまり納得できていませんでした。
というのも多段継承が良くないという話は分かるのですが、継承自体を悪いものとは考えていないためです。

final class は他言語で言うところの「 typedef として利用する」と言語化するのがしっくりきました。
なのでただ闇雲に final をつけるのではなく「それがそれ以上継承されることはないか、されたら困るものか」というところを考えて作られていると良いです。

/* e.g. */

// 普遍的なIDの定義
class ID {
    private int $id;

    public function __construct(int $id) {
        $this->id = $id;
    }

    public function value(): int {
        return $this->id;
    }

    public function valid(): bool {
        return $this->id > 0;
    }
}

// ユーサーIDを定義
final class UserID extends ID {};

// グループIDを定義
final class GroupID extends ID {};

Discussion