On 23 November 2025 14:23:40 GMT, "Tim Düsterhus" <tim@bastelstu.be> wrote:
We have now added a “Design Choices” section to the RFC explaining why we opted for “declarations need to be at the start of the scope”:
Hi Tim,
Thanks for the updates.
I think the reason comparing to other languages is so important relates to what Steve Klabnik calls "the Language Strangeness Budget": https://steveklabnik.com/writing/the-language-strangeness-budget/
Anything that is going to surprise users coming from other languages carries a cost which we need to justify. Generally, that means either a) the expected way wouldn't work in PHP for some reason; or b) we think we can do better by learning from the problems of other languages.
In basically every language derived from ALGOL and not from Pascal, variable declarations take the form of a statement inside a block, so doing something different definitely costs us some Strangeness Budget.
You've tried to make the case that PHP has unique challenges with variable declarations, but I'm not convinced.
In particular, I don't think this statement is accurate:
Other languages with block scoping, particularly statically typed languages, avoid this ambiguity by requiring all variables to be explicitly declared
In any language with ALGOL-style block scoping, you can write the equivalent of this code:
some_code_here(); // A
{
my_var = something(); // B
let my_var; // C
}
Every such language has to answer the same question: does "my_var" at line B refer to the block-scoped variable declared below it at line C?
If the answer is "yes", then any code outside the block, at section A, is irrelevant. Either the declaration is "hoisted" as though lines B and C were in the opposite order; or an error is raised because the variable is accessed (B) before declaration (C).
If the answer is "no", then line B does whatever it would have done if line C didn't exist. If it's mandatory to declare "my_var" in some surrounding scope, an error will be raised if it wasn't; if the variable can instead be implicitly created in some surrounding scope, it will be.
The only way to *avoid* the ambiguity is to forbid all statements between the start of the scope and a declaration - that is, raise an error even if line B doesn't reference "my_var". Notably, if you keep the ALGOL-style declarations, you can start with this rule, and then relax it later, as happened with C99.
So if it's not because we can't implement ALGOL-style declarations, is there something we think we can do better than them?
As far as I can see, any proposed statement of the form "let($foo) { ... }" is directly equivalent to ALGOL-style "{ let $foo; ... }"
The unique innovation appears to be when using it with a single statement rather than a block, such as in this example from the RFC:
let ($user = $repository->find(1)) if ($user !== null) { ... }
With ALGOL-style declarations, that requires an extra pair of braces:
{ let $user = $repository->find(1); if ($user !== null) { ... } }
Since an if statement can also take a single statement, you can also write this:
if ( $repository !== null ) let ( $user = $repository->find(1) ) { ... }
Which is equivalent to this in ALGOL-style:
if ( $repository !== null ) { let $user = $repository->find(1); ... }
This reversibility would also be there with other block types: "let($foo=something()) while($bar) { ... }" is subtly different from "while($bar) let($foo=something() { ... }", and so on.
It's an interesting feature, but whether it's worth the cost in "strangeness", I'm not sure.
The specific case of foreach-by-reference is a strong one, but as mentioned before, I think it would be better served by a specific syntax like "foreach ( $foo as let &$bar )", which avoids the repetition of "let($bar) foreach ( $foo as &$bar )".
On the other hand, I note that the "process_file" example in the RFC can't make use of the single-statement form: "let ( $lock = $file->lock(LockType::Shared) ) try { ... }" would be legal, but wouldn't release the lock until after the catch block.
In the example given, that's an exit point of the function anyway (when the new Exception is thrown), so a function-scoped variable would be cleaned up at the same time as a block-scoped one.
If there is no code after "// The file lock is released here ...", then the ALGOL style saves a pair of braces:
try {
let $file = File\open_read_only($path),
$lock = $file->lock(LockType::Shared);
$content = $file->readAll();
} catch ...
If there is, it looks very similar:
try {
{
let $file = File\open_read_only($path),
$lock = $file->lock(LockType::Shared);
$content = $file->readAll();
}
more_code_here();
} catch ...
Over all, I'm still not sold on having a special new block for this, rather than just using "let" for optional declarations, as in JavaScript.
--
Rowan Tommins
[IMSoP]