rfc:closure_self_reference

PHP RFC: Closure self reference

Introduction

Currently, the main way used to call a closure from within that closure, is to bind a variable by reference into the closure, when the closure is created.

However this can lead to shenanigans:

$fibonacci = function (int $n) use (&$fibonacci) {
    if ($n === 0) return 0;
    if ($n === 1) return 1;
    return $fibonacci($n-1) + $fibonacci($n-2);
};
 
echo $fibonacci(10). "\n";
$this_is_the_original_fibonacci = $fibonacci;
// ... many lines of code here
$fibonacci = function (int $n) { return rand(0, $n); };
// ... many lines of code here
 
echo $this_is_the_original_fibonacci(10). "\n";

i.e. a change to the variable outside the closure has modified the behaviour of the closure. That a closure can change behaviour like this violates the Principle of least astonishment

It would be better if it was possible to reference a closure inside itself, without having to `use` an external variable.

Proposal

Allow closures to be aliased to a variable that can be used within the closure:

$fibonacci = function (int $n) as $fn {
    if ($n === 0) return 0;
    if ($n === 1) return 1;
    return $fn($n-1) + $fn($n-2);
};
 
echo $fibonacci(5);

Or as a short closure, aka arrow function:

$factorial = fn(int $num) as $fn : int => $num > 1 ? $num * $fn($num - 1) : $num;

Syntax choices

There are a plethora of possible syntax choices. Those that have been thought about about are:

A constant

It would be possible to define a constant that refers to the current closure e.g. CLOSURE

$fibonacci = function (int $n) {
    if ($n === 0) return 0;
    if ($n === 1) return 1;
    return __CLOSURE__($n-1) + __CLOSURE__($n-2);
};
 
$factorial = fn(int $num): int => $num > 1 ? $num * __CLOSURE__($num - 1) : $num;

Alias after function return type

$fibonacci = function (int $n): int as $fn {
    if ($n === 0) return 0;
    if ($n === 1) return 1;
    return $fn($n-1) + $fn($n-2);
};

Due to parser limitations, this can't be used for short closures.

A static function on the closure class

e.g. Closure::current()

$fibonacci = function (int $n) {
    if ($n === 0) return 0;
    if ($n === 1) return 1;
    return (Closure::current())($n-1) + (Closure::current())($n-2);
};
 
$factorial = fn(int $num) as $fn : int => $num > 1 ? $num * (Closure::current())($num - 1) : $num;

De-anonymize the function

i.e. allow a variable name to be used after the keyword function before the parentheses containing the parameters:

$fibonacci = function $fn(int $n) {
    if ($n === 0) return 0;
    if ($n === 1) return 1;
    return $fn($n-1) + $fn($n-2);
};
 
$factorial = fn $fn(int $num): int => $num > 1 ? $num * $fn($num - 1) : $num;

This has a large aesthetic downside of appearing to create the closure variable in the scope that the closure is declared in, rather than internal to the closure scope.

Alias after use variables

$fibonacci_offset = function (int $n) use ($offset) as $fn: int {
    if ($n === 0) return $offset;
    if ($n === 1) return 1;
    return $fn($n-1) + $fn($n-2);
};
$factorial = fn(int $num): int => $num > 1 ? $num * $fn($num - 1) : $num;

Alias immediately after function parameters

$fibonacci_offset = function (int $n) as $fn use($offset): int {
    if ($n === 0) return $offset;
    if ($n === 1) return 1;
    return $fn($n-1) + $fn($n-2);
};
$factorial = fn(int $num) as $fn : int => $num > 1 ? $num * $fn($num - 1) : $num;

Syntax choice evaluation

Of the syntaxes considered, the following syntaxes are excluded for the reasons listed:

* A constant. Although this could work it has multiple aesthetic downsides of being verbose, ugly, and just not very language-y.

* Alias after function return type. Can't be used for short closures due to parser limitations.

* A static function on the closure class. Just not very language-y, quite verbose due to the extra ()'s needed

* De-anonymize the function. This has a large aesthetic downside of appearing to create the closure variable in the scope that the closure is declared in, rather than internal to the closure scope.

Which leaves the 'Alias after use variables' and 'Alias immediately after function parameters' as the acceptable options.

As the authors find the 'Alias immediately after function parameters' easiest to read, that is the syntax that has been chosen.

Backward Incompatible Changes

None known.

Proposed PHP Versions

8.3

RFC Impact

To Opcache

Unknown.

Future Scope

Questions

Proposed Voting Choices

Accept this RFC and make it possible to reference a closure from within itself using `as $variable` ? Yes/no.

Patches and Tests

Links to any external patches and tests go here.

Implementation

References

Links to external references, discussions or RFCs

Rejected Features

Keep this updated with features that were discussed on the mail lists.

rfc/closure_self_reference.txt · Last modified: 2023/06/03 21:01 by danack