Hi
Am 2025-12-11 23:21, schrieb Rowan Tommins [IMSoP]:
That sentence you quoted was specifically in the context of the
initial paragraph of that section, contrasting PHP - where block
scoping is expected to be used comparatively sparingly - against
languages where variable declarations are a more “bread and butter”
part of the development process, because formally / explicitly
declaring variables is a necessity for one reason or another.
I don't think that changes anything I said in my previous reply: as
soon as you declare a variable half-way through a block, there is an
ambiguity about its range of visibility. Having more variable
declarations makes that *more* likely to come up, not *less*, so I'm
not sure why you think it "avoids" the problem.
The difference I'm seeing is that for languages where variable
declarations (and block scoping) are a core part of the language, the
scoping rules are “moulding” (if that word makes sense here) how code in
that language is written and how folks reason about the code. This is
different for a language where block scoping is added after-the-fact and
remains an optional part of the language.
There's also an assumption that if PHP added block scoping, it would
only rarely be used. We have no way to know, but I'm not sure that's
true. I can easily imagine code styles adding a rule that all local
variables be declared at an appropriate level. I can also imagine new
users coming from other languages - particularly JS - adding "let" out
of habit, even if seasoned PHP coders wouldn't.
From my experience, a majority of functions in modern code bases are
reasonably short and single-purpose where intermediate variables are
meant to live for the remainder of the function scope. And of course
with additions such as the pipe operator, the number of temporaries will
likely also go down further. From my own PHP code, I would guess block
scoping to be useful for less than 10% of functions. For the ones where
it would be useful, it would be very useful, though, since those are the
functions that are on the more complex end of things.
I feel that the C99 requirements and syntax would still have more
ambiguity compared to the proposed `let()` syntax in cases like this:
{
let $foo = bar($baz); // What is $baz referring to?
Particularly if it is a by-reference out parameter.
let $baz = 1;
}
Probably the simplest solution is to re-use our existing definition of
"constant expression". In fact, we already have variable declarations
using that rule:
function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains
invalid operations
}
Morgan already correctly noted that `static` supports arbitrary
expressions nowadays. I would like to add that supporting arbitrary
expressions within the initializer is also something we expect from
block scoping to avoid boilerplate, since most if we don't store a
dynamically computed value in a variable, we might as well use a
constant or hardcode the value.ö
As an example, is a goto jump label a statement?
{
let $foo = 1;
label:
let $bar = $foo++;
goto label;
}
PHP already limits where "goto" can jump to; I don't know how that's
implemented, but I don't think we need to get into philosophical
definitions to say "you can't jump into the middle of a declaration
list".
Another, perhaps better, example that is not handled well by any
C-derived language that we are aware of is block scoping in combination
with `switch()`:
switch ($var) {
let $tmp;
case "foo":
let $tmp2;
break;
case "bar":
case "baz":
let $tmp2;
let $tmp3;
break;
}
Which of the `$tmp`s is placed at the “start of a block”? What is the
end of the block for each of them? Is it legal for `$tmp2` to be
declared in two locations?
Or, we could just bite the bullet and answer the "which way does it
resolve" question, as loads of other languages have already done.
Other languages have other ecosystems and other user expectations. PHP
has extensive “scope introspection” functionality by means of
`extract()`, `compact()`, `get_defined_vars()` and variable variables.
Folks are used to being able to access arbitrary variables (it's just a
Warning, not an Error to access undefined variables) and there's also
constructs like `isset()` that can act on plain old local-scope
variables. Adding semantics like the “temporal dead zone” from
JavaScript that you suggested in the other thread would mean that we
would need to have entirely new semantics and interactions with various
existing language features that folks already know, adding to the
complexity of the language. The RFC, as currently proposed, avoids all
that by preserving all the existing semantics about “variable existence”
and just adding the “backup and restore old value” semantics that are
known from other languages and reasonably intuitive to understand even
when not intimately familiar with block scoping.
let ($user = $repository->find(1)) if ($user !== null) { }
Skimming down a piece of code, I can spot where code is being run
conditionally without reading the condition itself:
For me this works, because the `let()` is preparing me that “this code
is doing user processing” and the `if()` is just an “implementation
detail” / “means to an end” of that. By the block scoping semantics I
know that when I read the closing brace, the user processing is
finished. The function is a <h1>, the user processing is a <h2> and the
`if()` is a <h3> if that analogy makes sense. If I just want to get an
overview over the function, I only care about the <h2> headings.
Maybe it's also because I've dabbled in Perl, which has post-fix
conditions, so a very similar line would have a very different meaning:
I understand that some languages have postfix conditions, but being able
to place an `if()` after another control structure is not a new thing.
The same would apply to:
foreach ($users as $user) if ($user->isAdmin()) {
echo "User is admin";
}
which is already valid PHP.
In terms of making it less of a special case, some languages have a ","
operator which lets you glue any two expressions together and get the
right-hand result.
In Perl, you can write this:
```
my $a = 'outer', $b = 'whatever';
if ( my $a='inner', $b == 'whatever' ) {
say $a; // 'inner'
}
say $a; // 'outer'
```
This gives the desired scope for $a, but the if statement is still just
accepting a single expression.
The comma would leave ambiguity in cases like `if (let $repository =
$container->getRepository(), $user = $repository->find(1))`. Are both
$repository and $user block-scoped or only $repository of them?
Assignments are valid expressions in a condition. That's probably why
C++ uses the `;` as a delimiter there.
JavaScript has the same operator, but apparently doesn't allow "let" in
an expression, so you can write:
if ( a="inner", b=="whatever" ) { }
but can't use it to declare a local version of "a".
I haven't thought through exactly how to apply that to PHP, but it
might give us an option for "both and": a concise and reusable syntax
for the if use case, and a separate syntax for cases like the closure
example I gave earlier: https://externals.io/message/129059#129075
Adding “inline” support for other control structures certainly is
something that can be done as future scope. But we believe the “top of
the block” semantics are important for block scoping to work well in PHP
due to its unique semantics and 30y history.
Best regards
Tim Düsterhus
PS: With that both Seifeddine and I are going to be enjoying our
end-of-the-year vacations and are expected to be back on the list next
year.