10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPAdvent Calendar 2024

Day 16

【PHP8.5】定数にクロージャを書けるようになる

Last updated at Posted at 2024-12-16

つい先日PHP8.4がリリースされたばかりだというのに、PHP8.5に搭載される新機能が早々とひとつ決まっていました。
ということで以下は該当のRFC、Support Closures in constant expressionsの紹介です。

PHP RFC: Support Closures in constant expressions

Introduction

いくつかのPHP機能は、定数式のみを受け入れるようになっています。
これらの式は、大まかに"不変値"とみなせる限られた演算を含めることができます。
特にアトリビュートの引数は定数式のみを受け入れる構造ですが、クロージャは許可されていません。

クロージャはPHPオペコードであり、一部機能を除けば不変値であるため、定数式とみなすことができます。

定数式としてクロージャを使用できることで、幾つかのユースケースが生まれます。

以下は引数$callbackをnullableにすることなく、デフォルト値としてクロージャを設定した、ユーザランドによるarray_filter()の実装例です。

function my_array_filter(
    array $array,
    Closure $callback = static function ($item) { return !empty($item); },
) {
    $result = [];
 
    foreach ($array as $item) {
        if ($callback($item)) {
            $result[] = $item;
        }
    }
 
    return $result;
}
 
var_dump(my_array_filter([
    0, 1, 2,
    '', 'foo', 'bar',
]));

Proposal

このRFCでは、定数式にクロージャを含めることを提案します。
次の個所が対象となります。

・アトリビュートの引数。
・プロパティと引数のデフォルト値。
・定数、クラス定数。

Constraints

クロージャを定数式として使用する場合、以下の制限がつきます。

スコープがないため、useを用いて変数をキャプチャすることはできません。
また、矢印記号を用いたアロー関数は暗黙的に変数をキャプチャするため、サポートしません。
この制限は、定数式として変数を使うことができないという制約と一致しています。

クロージャはstaticでなければなりません。
つまり$thisにアクセスすることはできません。
$thisはクロージャの再評価が必要となるため、一回しか評価されない定数式として取り扱うことはできません。

これらの制限は、コンパイル時に検証されます。

Scoping

他の定数式と同様に、定数式として定義されたクロージャは、それが配置されているコンテキストのスコープにあります。
即ち、プロパティデフォルト値のクロージャは、同クラスのprivateプロパティ、メソッド、クラス定数にアクセスできます。
これは、コンストラクタでプロパティに定義したクロージャがアクセスできるのと同じ理由です。

同様に、アトリビュート引数のクロージャは、同クラスのprivateメンバーにアクセス可能です。

Closures in sub-expressions

クロージャは定数式内の他の演算と同様に動作するため、演算の一部になる場合があります。
クロージャを式の一部として使うことは特に便利ではありませんが、動作はします。

ただ、デフォルト値としてクロージャのリストを定義することは、便利になる例のひとつです。

function foo(
    string $input,
    array $callbacks = [
        static function ($value) {
            return \strtoupper($value);
        },
        static function ($value) {
            return \preg_replace('/[^A-Z]/', '', $value);
        },
    ]
) {
    foreach ($callbacks as $callback) {
        $input = $callback($input);
    }
 
    return $input;
}
 
var_dump(foo('Hello, World!')); // string(10) "HELLOWORLD"

newにクロージャを渡す例。

class MyObject
{
    public function __construct(private Closure $callback) {}
}
 
const Foo = new MyObject(static function () {
    return 'foo';
});

Use Cases

ユースケース。

アトリビュートベースの検証ライブラリによる、カスタムフィールド検証の例。

final class Locale
{
    #[Validator\Custom(static function (string $languageCode): bool {
        return \preg_match('/^[a-z][a-z]$/', $languageCode);
    })]
    public string $languageCode;
}

テストライブラリによる、テストケースの生成。

final class CalculatorTest
{
    #[Test\CaseGenerator(static function (): iterable {
        for ($i = -10; $i <= 10; $i++) {
            yield [$i, $i, 0];
            yield [$i, 0, $i];
            yield [0, $i, ($i * -1)];
        }
    })]
    public function testSubtraction(int $minuend, int $subtrahend, int $result)
    {
        \assert(Calculator::subtract($minuend, $subtrahend) === $result);
    }
}

アトリビュートベースのシリアライズライブラリによる、カスタムフォーマット。

final class LogEntry
{
     public string $message;
 
     #[Serialize\Custom(static function (string $severity): string {
         return \strtoupper($severity);
     })]
     public string $severity;
}

Backward Incompatible Changes

互換性のない変更点はありません。

以前は、定数式としてクロージャを書くとコンパイルエラーが出ていました。

静的アナライザーとIDEは、無効だった式が有効になる他の全てのRFCと同じく、この式をエラーにしないようにする必要があります。

Proposed PHP Version(s)

PHP8.5

RFC Impact

Opcacheは、SHMに定数式を保存する部分を修正する必要があります。
プルリクエストには既に修正が含まれており、Opcache・JITを有効にした状態で全てのテストに合格します。

Open Issues

既知の問題はありません。

Unaffected PHP Functionality

影響を受けないPHP機能。

影響を受けるのは定数式だけであり、以前はクロージャを書けなかったところに書けるようになるだけです。

Future Scope

将来の展望であり、このRFCには含まれていません。

・staticでないクロージャのサポート
・第一級callableのサポート
・変数キャプチャのサポート

Proposed Voting Choices

本RFCは2024/11/13から2024/11/27まで投票が行われ、賛成19反対0の全員賛成で受理されました。

Patches and Tests

Implementation

感想

一番わかりやすい例は定数だと思うので、RFCには真っ先にこれを乗っけてほしかった。

const CLOSURE = static function () {
    echo "called", PHP_EOL;
};

var_dump(CLOSURE); // object(Closure)
(CLOSURE)();       // called 

// PHP8.4 Fatal error: Constant expression contains invalid operations

これまで定数には固定値、もしくは単純な式しか書くことができませんでしたが、クロージャも置けるようになったのでより複雑なことが可能になります。

プルリクにはより多くのテストケース・使用例が載っているので参考にするとよいでしょう。

ただまあ、私はPHPの特殊な使い方をあんまりしていないせいもありますが、正直メリットがよくわかりませんでした。
メリットとして挙げられている例も、アトリビュート以外はこれまでの書き方をしてもそんなに苦労せずに同じことができますし、アトリビュートは普通に使っているだけでは滅多に遭遇しませんからね。

しかしRFC提出からわずか一ヶ月で受理、さらに全員賛成という結果が示すように支持は圧倒的であり、もっと深い使い方をしている人にとっては非常に役立つ文法なのではないかと思われます。

10
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?