# Examples comparing Block Scoped RAII and Context Managers

- Thread: https://externals.io/message/129251
- Original archive: https://news-web.php.net/php.internals/129251
- Started by Rowan Tommins [IMSoP] on 2025-11-15T23:11:44+00:00
- Messages: 21
- RFC: Context Managers (Under discussion) — https://externals.io/rfc/context-managers.md
- RFC: let construct (Block Scoping) (Declined) — https://externals.io/rfc/optin_block_scoping.md
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #129251 — Rowan Tommins [IMSoP] — 2025-11-15T23:11:44+00:00

URL: https://externals.io/message/129251#129251

Hi all,

The Block Scoping RFC and the Context Manager RFC cover a lot of similar 
use cases, and a lot of the discussion on both threads has been 
explicitly comparing them.

To try to picture better how they compare, I have put together a set of 
examples that implement the same code using both features, as well as 
some other variations, in this git repo: https://gitlab.com/imsop/raii-vs-cm

A few notes:

- The syntax for the two proposals is based on the current RFC text. If 
they are updated, e.g. to use different keywords, I will update the 
examples.

- I have included examples with closures which automatically capture by 
value, since a lot of the same use cases come up when discussing those.

- There are many scenarios which could be included, and many ways each 
example could be written. I have chosen scenarios to illustrate certain 
strengths and weaknesses, but tried to fairly represent a "good" use of 
each feature. However, I welcome feedback about unintentional bias in my 
choices.

- Corrections and additional examples are welcome as Merge Requests to 
the repo, or replies here.


With that out of the way, here are my own initial thoughts from working 
through the examples:

- RAII + block scope is most convenient when protecting an existing 
object which can be edited or extended.

- When protecting a final object, or a native resource, RAII is harder 
to implement. In these cases, the separation of Context Manager from 
managed value is powerful.

- Context Managers are very concise for safely setting and resetting 
global state. RAII can achieve this, but feels less natural.

- An "inversion of control" approach (passing in a callback with the 
body of the protected block) requires capturing all variables *not* 
scoped to the block. Even with automatic by-value capture, those needed 
*after* the block would need to be listed for capture by reference.

- Building a Context Manager from a Generator can lead to very readable 
code in some cases, and closely mimics an "inversion of control" 
approach without the same variable capture problems.


I would be interested in other people's thoughts.

Regards,

-- 
Rowan Tommins
[IMSoP]

---

## #129277 — Rowan Tommins [IMSoP] — 2025-11-16T22:32:54+00:00

URL: https://externals.io/message/129251#129277 · In reply to: #129251

> 
> > […]
>
>
> Another suggestion would be to follow the Java try-with-resources 
> syntax. It does not require a new keyword to be introduced, as with 
> the Context Manager syntax. Moreover, it aligns with current 
> try-catch-finally usage already implemented by PHP developers.
>
> try ($transaction = $db->newTransaction()) {
>     $db->execute('UPDATE tbl SET cell = :cell', ['cell'=>'value']);
> }


I did have a look into that when somebody mentioned it earlier, and I 
believe it is almost exactly equivalent to C#'s "using" statement:

- C#: keyword "using", interface "IDisposable", method "Dispose": 
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/using

- Java: keyword "try", interface "AutoCloseable", method "close": 
https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html


The main difference I've spotted is how it combines with other blocks.

In Java, you can use the same block as both try-with-resources and 
try-catch:

try ( Something foo = new Something ) {
     blah(foo);
}
catch ( SomeException e ) {
     whatever();
}

In C#, you can instead use a statement version of using within any 
existing block:

try {
     using ( Something foo = new Something );
     blah(foo);
}
catch ( SomeException e ) {
     whatever();
}



Both are very similar to RAII, but because both languages use 
non-immediate garbage collection, the method is separate from the normal 
destructor / finalizer, and other references to the "closed"/"disposed" 
object may exist.

At the moment, I haven't included examples inspired by these, because I 
thought they would be too similar to the existing RAII examples and 
clutter the repo. But if there's a difference someone thinks is worth 
highlighting, I can add one in.



> Any object that implements TryWithContext can be used with such 
> syntax. The function returns the exit context operation as callback.
>
> interface TryWithContext {
>    public function tryWith(): \Closure;
> }
>

I can't find any reference to this in relation to Java; did you take it 
from a different language, or is it your own invention?

Either way, it looks like an interesting variation on the Python-based 
enterContext/exitContext. Do you have any thoughts on what it's 
advantages or disadvantages would be?


> Rather auto-capture I'd suggest explicit complete scope capture, by 
> using the use keyword without parenthesis. 

This is a completely separate discussion I was hoping not to get into. 
Although I've personally advocated for "function() use (*) {}" in the 
past, I've used "fn() {}" in the "auto-capture" examples because it is 
the syntax most often proposed.

It's irrelevant for this example anyway, so I've edited it out below.


> For a transaction it might look like this.
>
> class Transaction implements TryWithContext {
>     public function tryWith(): \Closure
>     {
>         $this->db->beginTransaction();
>         return function (?\Throwable $e = null) {
>             if ($e) {
>                 $this->db->rollbackTransaction();
>                 return;
>             }
>
>             $this->db->commitTransaction();
>         };
>     }
> }


Looking at this example, it feels like it loses the simplicity of RAII 
without gaining the power of Context Managers. In particular, tryWith() 
as shown can't return a separate value to be used in the loop, like 
beginContext() can in the Python-inspired proposal.

The class would have a separate constructor and tryWith() method, with 
no clear distinction. Making the cleanup function anonymous prevents the 
user directly calling dispose()/close()/__destruct() out of sequence; 
but it doesn't stop the object being used after cleanup, which seems 
like a more likely source of errors.


Still, it's interesting to explore these variations to see what we can 
learn, so thanks for the suggestion.


-- 
Rowan Tommins
[IMSoP]

---

## #129288 — Larry Garfield — 2025-11-18T17:23:35+00:00

URL: https://externals.io/message/129251#129288 · In reply to: #129251

> Hi all,
>
> The Block Scoping RFC and the Context Manager RFC cover a lot of similar 
> use cases, and a lot of the discussion on both threads has been 
> explicitly comparing them.
>
> To try to picture better how they compare, I have put together a set of 
> examples that implement the same code using both features, as well as 
> some other variations, in this git repo: https://gitlab.com/imsop/raii-vs-cm
>
> A few notes:
>
> - The syntax for the two proposals is based on the current RFC text. If 
> they are updated, e.g. to use different keywords, I will update the 
> examples.
>
> - I have included examples with closures which automatically capture by 
> value, since a lot of the same use cases come up when discussing those.
>
> - There are many scenarios which could be included, and many ways each 
> example could be written. I have chosen scenarios to illustrate certain 
> strengths and weaknesses, but tried to fairly represent a "good" use of 
> each feature. However, I welcome feedback about unintentional bias in my 
> choices.
>
> - Corrections and additional examples are welcome as Merge Requests to 
> the repo, or replies here.
>
>
> With that out of the way, here are my own initial thoughts from working 
> through the examples:
>
> - RAII + block scope is most convenient when protecting an existing 
> object which can be edited or extended.
>
> - When protecting a final object, or a native resource, RAII is harder 
> to implement. In these cases, the separation of Context Manager from 
> managed value is powerful.
>
> - Context Managers are very concise for safely setting and resetting 
> global state. RAII can achieve this, but feels less natural.
>
> - An "inversion of control" approach (passing in a callback with the 
> body of the protected block) requires capturing all variables *not* 
> scoped to the block. Even with automatic by-value capture, those needed 
> *after* the block would need to be listed for capture by reference.
>
> - Building a Context Manager from a Generator can lead to very readable 
> code in some cases, and closely mimics an "inversion of control" 
> approach without the same variable capture problems.
>
>
> I would be interested in other people's thoughts.
>
> Regards,
>
> -- 
> Rowan Tommins
> [IMSoP]

Thank you to Rowan for the in depth comparison!

One thing I definitely do not like is the need for a `FileWrapper` class in the RAII file-handle example.  That seems like an unnecessary level of abstraction just to squeeze the `fclose()` value onto the file handle.  The fully-separated Context Manager seems a more flexible approach.

Suppose we were dealing with a considerably more involved object than a file handle, with a large interface.  You'd need to either

1. Extend the class, and we all know about extends...
2. Make a wrapper that passes through the calls (which could be very verbose)
3. Do as is done here, with a simple dumb wrapper, which means in the body of the context block you have to do `$var->val` all the time, which is just clunky.

Fully separating the Context Manager from the Context Variable completely avoids that issue.


I also noted that all of the examples wrap the context block (of whichever syntax) in a try-catch of its own.  I don't know if that's going to be a common pattern or not.  If so, might it suggest that the `using` block have its own built-in optional `catch` and `finally` for one-off additional handling?  That could point toward the Java approach of merging this functionality into `try`, but I am concerned about the implications of making both `catch` and `finally` effectively optional on `try` blocks.  I am open to discussion on this front.  (Anyone know what the typical use cases are in Python?)


Regarding `let`, I think there's promise in such a keyword to opt-in to "unset this at the end of this lexical block."  However, it's also off topic from everything else here, as I think it's very obvious now that the need to do more than just `unset()` is common.  Sneaking hidden "but if it also implements this magic interface then it gets a bonus almost-destructor" into it is non-obvious magic that I'd oppose.  I'd be open to a `let` RFC on its own later (which would likely also make sense in `foreach` and various other places), but it's not a solution to the "packaged setup/teardown" problem.

---

Another thing that occurs to me, from both this writeup and the discussion in both threads, is that "escaped variables" are an unsolvable problem.  If a context variable is "open" (for some generic definition of open; that could be a file handle or an unflushed buffer or DB transaction or just a large memory sink), and then a reference to it is saved elsewhere, then when the context block ends, there's two problems that could happen:

* If the context block force-closes the variable, then the escaped reference is no longer valid.  This may or may not cause problems.
* If the context block does not force-close the variable, then we can't know that the end of the context block has flushed/completed the process.  This may or may not cause problems.

Which one is less of a problem is going to vary with the particular situation.  I don't think we can make a language-wide statement about which is always less problematic.  That means we need to allow individual cases to decide for themselves which "leak problem" they want to have.

Which is exactly the benefit of the separation of the Context Manager from the Context Variable.  The CM can be written to rely on `unset()` closing the object (risk 2), or to handle closing it itself (risk 1), as the developer determines.

Moreover, it's possible to have two different strategies for the same context variable, as either 2 separate Context Managers or one manager with a constructor parameter.

Suppose the context variable is a `Buffer` instance of some kind, which has a `flush()` method and a destructor that calls `flush()`.  Both `ForcedBufferContext` and `LazyBufferContext` could return the same Buffer class, but have different approaches to when the flush happens.  That's a level of flexibility that's impossible to achieve if the "exit" logic is on the context variable itself, whether in the destructor or a separate interface method.

Alternatively, `new BufferContext(force: true)` (or whatever) would avoid the need for 2 classes, depending on the specifics of the use case.

To, me, that's a strong argument in favor of the Context Manager approach.

---

## #129315 — Rowan Tommins [IMSoP] — 2025-11-19T22:19:23+00:00

URL: https://externals.io/message/129251#129315 · In reply to: #129288

> One thing I definitely do not like is the need for a `FileWrapper` class in the RAII file-handle example.  That seems like an unnecessary level of abstraction just to squeeze the `fclose()` value onto the file handle.  The fully-separated Context Manager seems a more flexible approach.


Yes, exploring how exactly that flexibility could be used was part of my 
motivation for the examples I picked.

The downside is that it is slightly harder to understand at first 
glance: someone reading "using (file_for_write('file.txt') as $fh)" 
might well assume that $fh is the value returned from 
"file_for_write('file.txt')", rather than the value returned from 
"file_for_write('file.txt')->enterContext()".

What made sense to me was comparing to an Iterator that only goes around 
once - in "foreach (files_to_write_to() as $fh)", the 
"files_to_write_to()" call doesn't return $fh either, 
"files_to_write_to()->current()" does.


> I also noted that all of the examples wrap the context block (of whichever syntax) in a try-catch of its own.  I don't know if that's going to be a common pattern or not.  If so, might it suggest that the `using` block have its own built-in optional `catch` and `finally` for one-off additional handling?  That could point toward the Java approach of merging this functionality into `try`, but I am concerned about the implications of making both `catch` and `finally` effectively optional on `try` blocks.  I am open to discussion on this front.  (Anyone know what the typical use cases are in Python?)


Looking at the parser, I realised that a "try" block with neither 
"catch" nor "finally" actually matches the grammar; it is only rejected 
by a specific check when compiling the AST to opcodes. Without that 
check, it would just compile to some unnecessary jump table entries.


I guess an alternative would be allowing any statement after the using() 
rather than always a block, as in Seifeddine and Tim's proposal, which 
allows you to stack like this:

using ($db->transactionScope()) try {
     // ...
}
catch ( SomeSpecificException $e ) {
     // ...
}

Or, the specific combination "try using( ... )" could be added to the 
parser. (At the moment, "try" must always be followed by "{".)


As I noted in one of the examples 
(file-handle/application/1b-raii-with-scope-block.php), there is a 
subtle difference in semantics between different nesting orders - with 
"try using()", you can catch exceptions thrown by enterContext() and 
exitContext(); with "using() try", you can catch exceptions before 
exitContext() sees them and cleans up.

It seems Java's try-with-resources is equivalent to "try using()":

 >  In a try-with-resources statement, any catch or finally block is run 
after the resources declared have been closed.


> Regarding `let`, I think there's promise in such a keyword to opt-in to "unset this at the end of this lexical block."  However, it's also off topic from everything else here, as I think it's very obvious now that the need to do more than just `unset()` is common.  Sneaking hidden "but if it also implements this magic interface then it gets a bonus almost-destructor" into it is non-obvious magic that I'd oppose.  I'd be open to a `let` RFC on its own later (which would likely also make sense in `foreach` and various other places), but it's not a solution to the "packaged setup/teardown" problem.


I completely agree. I think an opt-in for block scope would be useful in 
a number of places, and resource management is probably the wrong focus 
for designing it. For instance, it would give a clear opt-out 
for capture-by-default closures:

function foo() {
    // ... code setting lots of variables ...
    $callback = function() use (*) {
         let $definitelyNotCaptured=null;
         // ... code mixing captured and local variables ...
     }
}


> Which is exactly the benefit of the separation of the Context Manager from the Context Variable.  The CM can be written to rely on `unset()` closing the object (risk 2), or to handle closing it itself (risk 1), as the developer determines.


Something the examples I picked don't really showcase is that a Context 
Manager doesn't need to be specialised to a particular task at all, it 
can generically implement one of these strategies.

The general pattern is this:

class GeneralPurposeCM implements ContextManager {
     public function __construct(private object $contextVar) {}
     public function enterContext(): object { return $this->contextVar; }
     public functoin exitContext(): void {}
}

- On its own, that makes "using(new GeneralPurposeCM(new Something) as 
$foo) { ... }" a very over-engineered version of "{ let $foo = new 
Something; ... }"

- To emulate C#, constrain to "IDisposable $contextVar", and call 
"$this->contextVar->Dispose()" in exitContext()

- To emulate Java, constrain to "AutoCloseable $contextVar" and call 
"$this->contextVar->close()" in exitContext()

- To throw a runtime error if the context variable still has references 
after the block, swap "$this->contextVar" for a WeakReference in 
beginContext(); then check for "$this->contextVarWeakRef->get() !== 
null" in exitContext()

- To have objects that "lock and unlock themselves", constrain to 
"Lockable $contextVar", then call "$this->contextVar->lock()" in 
beginContext() and "$this->contextVar->unlock()" in exitContext()


The only things you can't emulate are:

1) The extra syntax options provided by other languages, like C#'s 
"using Something foo = whatever();" or Go's "defer 
some_function(something);"

2) Compile-time guarantees that the Context Variable will not still have 
references after the block, like in Hack. I don't think that's a 
realistic goal for PHP.


Incidentally, while checking I had the right method name in the above, I 
noticed the Context Manager RFC has an example using "leaveContext" 
instead, presumably an editing error. :)


Regards,

-- 
Rowan Tommins
[IMSoP]

---

## #129461 — Larry Garfield — 2025-11-26T18:08:11+00:00

URL: https://externals.io/message/129251#129461 · In reply to: #129315

> On 18/11/2025 17:23, Larry Garfield wrote: 
> > […]
>
>
> Yes, exploring how exactly that flexibility could be used was part of 
> my motivation for the examples I picked. 
>
> The downside is that it is slightly harder to understand at first 
> glance: someone reading "using (file_for_write('file.txt') as $fh)" 
> might well assume that $fh is the value returned from 
> "file_for_write('file.txt')", rather than the value returned from 
> "file_for_write('file.txt')->enterContext()".
>
> What made sense to me was comparing to an Iterator that only goes 
> around once - in "foreach (files_to_write_to() as $fh)", the 
> "files_to_write_to()" call doesn't return $fh either, 
> "files_to_write_to()->current()" does.

That's a good analogy, I like it.

> > […]
>
>
> Looking at the parser, I realised that a "try" block with neither 
> "catch" nor "finally" actually matches the grammar; it is only rejected 
> by a specific check when compiling the AST to opcodes. Without that 
> check, it would just compile to some unnecessary jump table entries.
>
>
>
> I guess an alternative would be allowing any statement after the 
> using() rather than always a block, as in Seifeddine and Tim's 
> proposal, which allows you to stack like this:
>
> using ($db->transactionScope()) try {
>     // ...
> }
> catch ( SomeSpecificException $e ) {
>     // ...
> }
>
> Or, the specific combination "try using( ... )" could be added to the 
> parser. (At the moment, "try" must always be followed by "{".)
>
>
>
> As I noted in one of the examples 
> (file-handle/application/1b-raii-with-scope-block.php), there is a 
> subtle difference in semantics between different nesting orders - with 
> "try using()", you can catch exceptions thrown by enterContext() and 
> exitContext(); with "using() try", you can catch exceptions before 
> exitContext() sees them and cleans up.
>
> It seems Java's try-with-resources is equivalent to "try using()":
>
> > […]

Thanks.  I'll discuss these options with Arnaud.  Anyone else want to weigh in here?


> > […]
>
>
> Something the examples I picked don't really showcase is that a Context 
> Manager doesn't need to be specialised to a particular task at all, it 
> can generically implement one of these strategies.
>
> The general pattern is this:
>
> class GeneralPurposeCM implements ContextManager {
>     public function __construct(private object $contextVar) {}
>     public function enterContext(): object { return $this->contextVar; }
>     public functoin exitContext(): void {}
> }
>
> - On its own, that makes "using(new GeneralPurposeCM(new Something) as 
> $foo) { ... }" a very over-engineered version of "{ let $foo = new 
> Something; ... }"

True!  It may make sense eventually to provide a "UnsetThis(mixed $var)" CM in the stdlib.  Not something to include now, but I've no issue with it existing eventually.

> Incidentally, while checking I had the right method name in the above, 
> I noticed the Context Manager RFC has an example using "leaveContext" 
> instead, presumably an editing error. :)

Indeed.  Fixed now, thanks.

--Larry Garfield

---

## #129463 — tim@bastelstu.be — 2025-11-27T08:34:21+00:00

URL: https://externals.io/message/129251#129463 · In reply to: #129251

Hi

I've had the opportunity to take a look now.

Am 2025-11-16 00:11, schrieb Rowan Tommins [IMSoP]:
> - The syntax for the two proposals is based on the current RFC text. If 
> they are updated, e.g. to use different keywords, I will update the 
> examples.

The block scoping RFC has been updated to `let()`.

> - There are many scenarios which could be included, and many ways each 
> example could be written. I have chosen scenarios to illustrate certain 
> strengths and weaknesses, but tried to fairly represent a "good" use of 
> each feature. However, I welcome feedback about unintentional bias in 
> my choices.

1.

For db-transaction/implementation/1-raii-object.php I'd like to note 
that it is not necessary to proxy execute() through the transaction 
object. It could also be used as a simple guard object which only 
purpose is to be constructed and destructed.

     let ($txn = $db->begin()) {
         $db->execute('…');
     }

or possibly:

     let (
         $db = connect(),
         $txn = $db->begin(),
     ) { … }

In that way it is similar to the 2x-context-manager-anon-class.php 
example. The same “guard object” possibility also exists for the other 
examples as far as I can tell.

2.

The RAII object in 'file-handle' serves no purpose. PHP will already 
call `fclose()` when the resource itself goes out of scope, so this is 
existing behavior with extra steps. The same is true for the 
0-linear-code example in file-object. You don't need the `fclose()` 
there.

3.

The docblock in locked-pdf/application/2-context-manager.php is 
incorrectly copy and pasted.

> With that out of the way, here are my own initial thoughts from working 
> through the examples:
> 
> - RAII + block scope is most convenient when protecting an existing 
> object which can be edited or extended.
> 
> - When protecting a final object, or a native resource, RAII is harder 
> to implement. In these cases, the separation of Context Manager from 
> managed value is powerful.
> 
> - Context Managers are very concise for safely setting and resetting 
> global state. RAII can achieve this, but feels less natural.
> 
> - An "inversion of control" approach (passing in a callback with the 
> body of the protected block) requires capturing all variables *not* 
> scoped to the block. Even with automatic by-value capture, those needed 
> *after* the block would need to be listed for capture by reference.
> 
> - Building a Context Manager from a Generator can lead to very readable 
> code in some cases, and closely mimics an "inversion of control" 
> approach without the same variable capture problems.
> 
> 
> I would be interested in other people's thoughts.

I was about to comment that the 'file' examples were not equivalent, 
because the context manager and IOC ones didn't include the error 
logging, until I noticed it was hidden away in the implementation. This 
probably suggests that I implicitly expected to see all relevant control 
flow. Exception handling and logging in particular probably greatly 
depend on the surrounding context to be useful (e.g. to adapt the log 
message or to enhance it with additional context data). So while it 
might superficially look cleaner / simpler, I feel that this kind of 
generic handling will bite you sooner or later. So with the context 
manager example, I would expect the “exception introspection” capability 
to be used to properly tear down the context, but not for cross-cutting 
concerns such as logging. I'm not sure if this would qualify as 
“unintentional bias”, but it's certainly something that affected by 
perception of the code.

Best regards
Tim Düsterhus

---

## #129464 — tim@bastelstu.be — 2025-11-27T09:08:14+00:00

URL: https://externals.io/message/129251#129464 · In reply to: #129315

Hi

Am 2025-11-19 23:19, schrieb Rowan Tommins [IMSoP]:
> 
> > […]
> 
> 
> Yes, exploring how exactly that flexibility could be used was part of 
> my motivation for the examples I picked.
> 
> The downside is that it is slightly harder to understand at first 
> glance: someone reading "using (file_for_write('file.txt') as $fh)" 
> might well assume that $fh is the value returned from 
> "file_for_write('file.txt')", rather than the value returned from 
> "file_for_write('file.txt')->enterContext()".
> 
> What made sense to me was comparing to an Iterator that only goes 
> around once - in "foreach (files_to_write_to() as $fh)", the 
> "files_to_write_to()" call doesn't return $fh either, 
> "files_to_write_to()->current()" does.

For me the relevant keyword that indicates that the value is not used 
directly is not the 'as', but the 'each' part of the 'foreach'. Just by 
reading it as a English sentence, it becomes clear to me what is 
happening.

The same is not true for me for `using (file_for_write('file.txt') as 
$fh)` or even worse `using (new Manager() as $notActuallyTheManager)` 
(which is part of the RFC). AFAICT the latter is not so much a problem 
in Python, because there is no difference between constructors and 
factory functions and also because there is no actual type declaration. 
This means `open()` could be a function returning a file handle or it 
could be the “constructor“ for a context manager (that then returns the 
file handle as part of entering the context) and the difference is 
effectively indistinguishable from the outside, which is not the case in 
PHP.

Best regards
Tim Düsterhus

---

## #129466 — Rowan Tommins [IMSoP] — 2025-11-27T14:43:45+00:00

URL: https://externals.io/message/129251#129466 · In reply to: #129464

>For me the relevant keyword that indicates that the value is not used directly is not the 'as', but the 'each' part of the 'foreach'. Just by reading it as a English sentence, it becomes clear to me what is happening.
>
>The same is not true for me for `using (file_for_write('file.txt') as $fh)` or even worse `using (new Manager() as $notActuallyTheManager)` (which is part of the RFC).


Hi Tim,

Thanks for your thoughts. I will definitely go over your other email in detail when I have some more time and energy, and update some of my examples. 

Regarding this point, I think it's a really interesting observation, and I wonder if we should be looking for different keywords that read more clearly. For instance: 

using(new SomeManager() for $someResource)

using($someResource from new SomeManager())

context(new SomeManager() giving $someResource)

Regards,

Rowan Tommins
[IMSoP]

---

## #129512 — Larry Garfield — 2025-12-02T19:57:00+00:00

URL: https://externals.io/message/129251#129512 · In reply to: #129466

> 
> > […]
>
>
> Hi Tim,
>
> Thanks for your thoughts. I will definitely go over your other email in 
> detail when I have some more time and energy, and update some of my 
> examples. 
>
> Regarding this point, I think it's a really interesting observation, 
> and I wonder if we should be looking for different keywords that read 
> more clearly. For instance: 
>
> using(new SomeManager() for $someResource)
>
> using($someResource from new SomeManager())
>
> context(new SomeManager() giving $someResource)
>
> Regards,
>
> Rowan Tommins
> [IMSoP]

We're very open to tweaking the keywords.  I can see the argument for "as" being a little misleading in PHP's case.  Though I'd prefer to have the EXPR first and VAR second, whatever the keyword is.

Another potential we thought of: Just plain =>.  It already doesn't imply equals, would have no keyword breaks, and is still only 2 characters.

using (new SomeManager() => $someResource) {
  // ...
}

Thoughts?

--Larry Garfield

---

## #129577 — Rowan Tommins [IMSoP] — 2025-12-09T23:19:12+00:00

URL: https://externals.io/message/129251#129577 · In reply to: #129463

Hi Tim,

As promised, I eventually got back to this e-mail properly, and have 
updated the examples in response.

For convenience, here's the link again: https://gitlab.com/imsop/raii-vs-cm



>
> The block scoping RFC has been updated to `let()`.


Updated.


> 1.
>
> For db-transaction/implementation/1-raii-object.php I'd like to note 
> that it is not necessary to proxy execute() through the transaction 
> object. It could also be used as a simple guard object which only 
> purpose is to be constructed and destructed.


That's true. The "wikimedia-at-ease" example illustrates that style, 
because there aren't any methods we'd want there at all.

The drawback I see is that on a longer block, you have to come up with a 
name for that unused variable, and make sure you don't accidentally 
unset or overwrite it.


Apparently Java's workaround for that is to allow "unnamed variables", 
which have the expected lifetime, but can't be accessed: 
https://openjdk.org/jeps/456

try-with-resources is given as one of the example use cases ("var" 
infers the type; "_" in place of the name makes it an "unnamed variable"):

try (var _ = ScopedContext.acquire()) {
     ... no use of acquired resource ...
}

Notably, this is *not* the same meaning for "_" as, say, C#, where "_ = 
foo()" tells the compiler to discard the return value of foo(): 
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards


In the case of a transaction, it feels logical for at least the commit() 
and rollback() methods to be on the Transaction class, but other methods 
would be a matter of style. I've removed execute() to take the simplest 
route.


>
> 2.
>
> The RAII object in 'file-handle' serves no purpose. PHP will already 
> call `fclose()` when the resource itself goes out of scope, so this is 
> existing behavior with extra steps. The same is true for the 
> 0-linear-code example in file-object. You don't need the `fclose()` 
> there.


This is true as long as nothing stores an additional reference to the 
file handle. Having a separate "guard object" doesn't fully prevent this 
- a reference to the guard object itself could leak - but does make it 
less likely.

It also gives somewhere to customise the open and close behaviour, such 
as adding locking, or converting false-on-error to an exception (more on 
that below).


To see what it looks like, I've added a "file-handle-unguarded" 
scenario, which exposes the handle much more directly: it doesn't force 
 `fclose()`, and leaves the application to handle the failure of `fopen()`

The CM example benefits from being able to "break" out of the using{} 
block; as far as I can see, the current RFC doesn't allow that from a 
let{} block, is that correct?



>
> 3.
>
> The docblock in locked-pdf/application/2-context-manager.php is 
> incorrectly copy and pasted.


Well spotted. Fixed.


> I was about to comment that the 'file' examples were not equivalent, 
> because the context manager and IOC ones didn't include the error 
> logging, until I noticed it was hidden away in the implementation. 
> This probably suggests that I implicitly expected to see all relevant 
> control flow. Exception handling and logging in particular probably 
> greatly depend on the surrounding context to be useful (e.g. to adapt 
> the log message or to enhance it with additional context data). So 
> while it might superficially look cleaner / simpler, I feel that this 
> kind of generic handling will bite you sooner or later. So with the 
> context manager example, I would expect the “exception introspection” 
> capability to be used to properly tear down the context, but not for 
> cross-cutting concerns such as logging.


That's a reasonable comment. Looking at those examples more closely, I 
realise I actually messed up the error handling in most of them - they 
never check for the false returned by `fopen()`

I've moved the logging out into the "application", but updated all the 
"implementations" to throw a "FileOpeningException" if `fopen()` returns 
false.

This really demonstrates the value of the "try using() { ... }" 
short-hand Larry & Arnaud have added to the CM RFC - we need the try to 
be on the *outside* of the block to catch the new exception. I presume a 
similar "try let() { ... }" could be added to yours?


It's interesting how similar the example end up for simple scenarios, 
but I think if we had block-scoped variables, I'd still like Context 
Managers for some of the more complex cases.


-- 
Rowan Tommins
[IMSoP]

---

## #129582 — tim@bastelstu.be — 2025-12-10T14:25:48+00:00

URL: https://externals.io/message/129251#129582 · In reply to: #129577

Hi

Am 2025-12-10 00:19, schrieb Rowan Tommins [IMSoP]:
> As promised, I eventually got back to this e-mail properly, and have 
> updated the examples in response.

Great timing. After being on a company retreat last week and doing day 
job stuff earlier this week, I wanted to get back to the RFCs today and 
planned to send a reminder.

> > […]
> 
> Updated.

You missed db-transaction/application/1b-raii-with-scope-block.php.

Let me begin with some more obvious fixes I found in this new iteration:

- In db-transaction/application/1c-raii-with-scope-declaration.php I am 
noticing that the comment mentions “extra indent” which is not a case 
for the 1b version either, due to the “single statement” variant being 
used.
- file-handle/application/3a-ioc-current-closure.php is missing a 
closing parens and semicolon in line 20.
- file-handle/application/3b-ioc-auto-capture.php is missing the same.
- Both also applies to the unguarded version and the file-object 
version.
- file-handle-unguarded/application/1b-raii-with-scope-block.php is 
missing a closing parens in line 11.
- file-object/application/1a-raii-no-helpers.php is unsetting a 
`$fileWrapper` variable, this should probably be `$fh`.
- file-object/application/2-context-manager.php has broken indentation.

FWIW: Using VS Code to view the examples with syntax highlighting worked 
surprisingly well despite the new keywords. It allowed me to easily spot 
these typos.

Less obvious issues with the locked-pdf example:

- locked-pdf/application/0-linear-code.php: In this case you are closing 
the lock before writing into the same output file, which makes locking 
useless. Probably a good case in point that the “naive” implementation 
is insufficiently safe. The “save” arguably also belongs into the try 
rather than the finally, since we probably don't want to save in case of 
exception.
- Looking further I notice that the locking issue also exists for the 
other implementations.
- I'd argue that the ->save() belongs into the caller, rather than the 
RAII object or context manager, since actually saving a file is business 
logic that should not be hidden away.
- If you would make the changes, this would effectively become 
equivalent to the Transaction example or the file-object example just 
with the extra `flock()` call and some extra business-specific logic.

I think this example should be adjusted to make use of “external 
locking”, i.e. using a dedicated reusable single-purpose lock object 
that is independent of the resource in question, so that it is 
sufficiently dissimilar from the transaction example (though I guess it 
would then be equivalent to the wikimedia-at-ease example). For 
reference the RAII example would be just this:

     <?php

     class Lock {
         private $lock;

         public function __construct(string $file) {
             $this->lock = fopen($file, 'r');
             flock($this->lock, LOCK_EX);
         }

         public function __destruct() {
             fclose($this->lock);
         }
     }

     let ($lock = new Lock()) {
         perform_operation_under_lock();
         perform_operation_under_lock($lock); // or possibly this to 
“prove” to the function that a lock is held.
     }

 From what I see, the locked-pdf example can be removed entirely, since 
it does not bring anything new to the table. Did I miss something?

> > […]
> 
> 
> That's true. The "wikimedia-at-ease" example illustrates that style, 
> because there aren't any methods we'd want there at all.
> 
> The drawback I see is that on a longer block, you have to come up with 
> a name for that unused variable, and make sure you don't accidentally 
> unset or overwrite it.
> 
> 
> Apparently Java's workaround for that is to allow "unnamed variables", 
> which have the expected lifetime, but can't be accessed: 
> https://openjdk.org/jeps/456

Interesting, thank you for that insight.

> Notably, this is *not* the same meaning for "_" as, say, C#, where "_ = 
> foo()" tells the compiler to discard the return value of foo(): 
> https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards

Though I agree that such an unnamed variable is more commonly used as a 
discard in the programming languages I'm familiar with. For me a 
variable name like `$lock` or similar would be sufficiently descriptive 
to prevent the value from being overwritten. With the “variable backup” 
from the block scoping RFC even declaring the variable with a new block 
wouldn't cause issues, since the old value would be implicitly kept 
alive and restored when the inner block ends.

> In the case of a transaction, it feels logical for at least the 
> commit() and rollback() methods to be on the Transaction class, but 
> other methods would be a matter of style. I've removed execute() to 
> take the simplest route.

That makes sense to me.

> > […]
> 
> 
> This is true as long as nothing stores an additional reference to the 
> file handle. Having a separate "guard object" doesn't fully prevent 
> this - a reference to the guard object itself could leak - but does 
> make it less likely.
> 
> It also gives somewhere to customise the open and close behaviour, such 
> as adding locking, or converting false-on-error to an exception (more 
> on that below).

Yes. But since there was a specific example that didn't make use of this 
additional capability, I called out that specific example.

> To see what it looks like, I've added a "file-handle-unguarded" 
> scenario, which exposes the handle much more directly: it doesn't force 
> `fclose()`, and leaves the application to handle the failure of `fopen()`

Thank you. With regard to 
file-handle-unguarded/application/1b-raii-with-scope-block.php, I am not 
happy with either of the examples, since the style is bad in different 
ways. Based on the “Example showing the combination of let and if():” 
from the RFC and the generally accepted “happy path first” when having 
an if with an else block, I would personally write it like this:

     let ($fh = fopen('file.txt', 'w') if ($fh !== false) {
         try {
             foreach ($someThing as $value) {
                 fwrite($fh, serialize($value));
             }
         } catch (\Exception $e) {
             log('Failed processing the file in some way.');
         }
     } else {
         log('Failed to open file.');
     }

Here the “else” block is behaving quite similarly to a “catch” block in 
that it does the error handling.

> The CM example benefits from being able to "break" out of the using{} 
> block; as far as I can see, the current RFC doesn't allow that from a 
> let{} block, is that correct?

This is correct. But I believe with my previous suggestion of writing 
the example being able to break out of the block is not necessary. 
Personally I find it pretty unintuitive that `break;` would target the 
`using()` block for the context manager. It feels pretty arbitrary, why 
is it possible to break out of `using()`, but not out of `if ()` or 
`try` or `catch ()`. Currently my mental model is that `break;` is used 
with control structures that “do multiple things” (though switch should 
just not have fallthrough by default and then it also would need break).

If for some reason, you would like to break out of `let()`, there are 
some options that rely on `let()` being designed to compose well with 
existing functionality:

Using a do-while(false) loop. This is a pattern that is somewhat known 
from C as a “restricted” form of goto.

     <?php

     class Foo
     {
         public function __construct()
         {
             echo __METHOD__, PHP_EOL;
         }

         public function __destruct()
         {
             echo __METHOD__, PHP_EOL;
         }
     }

     let ($foo = new Foo()) do {
         if (random_int(0, 1)) {
             echo "Breaking out", PHP_EOL;
             break;
         } else {
             echo "Not breaking out", PHP_EOL;
         }

         echo "Bottom", PHP_EOL;
     } while (false);
     echo "After", PHP_EOL;

And of course a regular goto also works.

> > […]
> 
> 
> Well spotted. Fixed.

I'm not sure what you changed, but it's still referring to 
“Transaction”. I'm also now noticing that the same is true for 
locked-pdf/application/1a-raii-no-helpers.php.

> This really demonstrates the value of the "try using() { ... }" 
> short-hand Larry & Arnaud have added to the CM RFC - we need the try to 
> be on the *outside* of the block to catch the new exception. I presume 
> a similar "try let() { ... }" could be added to yours?

Yes, but if this is desired I would prefer not implement this as an 
explicit “try let” special case, but rather by allowing `try` to be 
followed by any statement (which includes block statements). This would 
then automatically compose with `let()`, just like `let()` composes with 
`if()` and would improve predictability of the language overall. It is 
not entirely trivial to implement, since the “dangling else” ambiguity 
(https://en.wikipedia.org/wiki/Dangling_else) would then exist as a 
“dangling catch” ambiguity, but it should be possible.

Best regards
Tim Düsterhus

---

## #129588 — Rowan Tommins [IMSoP] — 2025-12-11T21:45:35+00:00

URL: https://externals.io/message/129251#129588 · In reply to: #129582

> You missed db-transaction/application/1b-raii-with-scope-block.php.
>
> Let me begin with some more obvious fixes I found in this new iteration:
>
> - In db-transaction/application/1c-raii-with-scope-declaration.php I 
> am noticing that the comment mentions “extra indent” which is not a 
> case for the 1b version either, due to the “single statement” variant 
> being used.
> - file-handle/application/3a-ioc-current-closure.php is missing a 
> closing parens and semicolon in line 20.
> - file-handle/application/3b-ioc-auto-capture.php is missing the same.
> - Both also applies to the unguarded version and the file-object version.
> - file-handle-unguarded/application/1b-raii-with-scope-block.php is 
> missing a closing parens in line 11.
> - file-object/application/1a-raii-no-helpers.php is unsetting a 
> `$fileWrapper` variable, this should probably be `$fh`.
> - file-object/application/2-context-manager.php has broken indentation.


For little fixes like this, it would probably be most efficient if you 
raise a PR, or mail me a patch, rather then me hunting around for each one.


> FWIW: Using VS Code to view the examples with syntax highlighting 
> worked surprisingly well despite the new keywords. It allowed me to 
> easily spot these typos.


Interesting. I'm using PhpStorm, and it gets very confused by most of them.



> Less obvious issues with the locked-pdf example:
>
> - locked-pdf/application/0-linear-code.php: In this case you are 
> closing the lock before writing into the same output file, which makes 
> locking useless.


The idea was to lock the file while processing the data, then use the 
existing code (which knows nothing about locking) to write to it. You're 
right that there's a race condition between unlock and save, but it 
seems harsh to call it "useless".


> - If you would make the changes, this would effectively become 
> equivalent to the Transaction example or the file-object example just 
> with the extra `flock()` call and some extra business-specific logic.


This feels like the Monty Python "what have the Romans ever done for 
us?" sketch: "if you take away all the things that make it different 
from the other examples, it's the same as the other examples". 
Ultimately, they're all just "try { setup(); act(); } finally { 
cleanup(); }", but I was trying to write code that was different enough 
to play with different implications of each syntax.


> I think this example should be adjusted to make use of “external 
> locking”, i.e. using a dedicated reusable single-purpose lock object 
> that is independent of the resource in question, so that it is 
> sufficiently dissimilar from the transaction example (though I guess 
> it would then be equivalent to the wikimedia-at-ease example).


That would be a completely different scenario, which wouldn't illustrate 
what I was intending. It might be interesting to add though; feel free 
to contribute it.


> From what I see, the locked-pdf example can be removed entirely, since 
> it does not bring anything new to the table. Did I miss something?


What I was trying to illustrate with that scenario was something where 
you want to add logic next to the setup of some object, and logic next 
to the tear down of that same object, and encapsulate the whole thing in 
some way.

Coming up with realistic but concise examples is tricky.


> With regard to 
> file-handle-unguarded/application/1b-raii-with-scope-block.php, I am 
> not happy with either of the examples, since the style is bad in 
> different ways. Based on the “Example showing the combination of let 
> and if():” from the RFC and the generally accepted “happy path first” 
> when having an if with an else block, I would personally write it like 
> this:
>
>     let ($fh = fopen('file.txt', 'w') if ($fh !== false) {
>         try {
>             foreach ($someThing as $value) {
>                 fwrite($fh, serialize($value));
>             }
>         } catch (\Exception $e) {
>             log('Failed processing the file in some way.');
>         }
>     } else {
>         log('Failed to open file.');
>     }
>
> Here the “else” block is behaving quite similarly to a “catch” block 
> in that it does the error handling.


I agree, in this case that nesting does read quite well (although see my 
thoughts in the other thread about the trailing "if").

I tried a few different versions, but found it quite hard to have an 
intuitive grasp of which statements to combine.

In particular, the implications of "if() let()" vs "let() if()", and how 
exactly the "else" block would behave, didn't come naturally. Maybe they 
would if I was using it regularly, but it perhaps demonstrates the 
"strangeness" I was talking about in the other thread.

It's perhaps also because I've so often been told that the non-block 
forms of if(), while(), etc should be avoided, so my instinct is to 
start with explicit braces everywhere.



> Personally I find it pretty unintuitive that `break;` would target the 
> `using()` block for the context manager. It feels pretty arbitrary, 
> why is it possible to break out of `using()`, but not out of `if ()` 
> or `try` or `catch ()`.


I guess it comes back to the idea that a Context Manager is like an 
Iterator that only yields once, so using() is like a loop that goes 
round once. But I agree it might not be immediately obvious.


> If for some reason, you would like to break out of `let()`, there are 
> some options that rely on `let()` being designed to compose well with 
> existing functionality:
>
> Using a do-while(false) loop. This is a pattern that is somewhat known 
> from C as a “restricted” form of goto.


Not being a seasoned C coder, this always looks weird to me. I have to 
read it a couple of times to realise a) that it's not really a loop, and 
b) that the false means "do it once", not "do it never".


> And of course a regular goto also works.


As far as I can remember, I've used "goto" exactly once in twenty years 
of PHP coding. And if I found that code now, I'd probably spot a way to 
make it read more naturally without.


If I really wanted to avoid the nesting, I'd probably look for some code 
I could break out into a helper function, and use "return" to abort that 
early.


> > […]
>
> I'm not sure what you changed, but it's still referring to 
> “Transaction”. I'm also now noticing that the same is true for 
> locked-pdf/application/1a-raii-no-helpers.php.


Apparently, I misread which file you were talking about, and fixed a 
different copy-paste error: 
https://gitlab.com/imsop/raii-vs-cm/-/commit/f3e1591c8da07f14399fe4e22710e3ad092ce743


> Yes, but if this is desired I would prefer not implement this as an 
> explicit “try let” special case, but rather by allowing `try` to be 
> followed by any statement (which includes block statements). This 
> would then automatically compose with `let()`, just like `let()` 
> composes with `if()` and would improve predictability of the language 
> overall. It is not entirely trivial to implement, since the “dangling 
> else” ambiguity (https://en.wikipedia.org/wiki/Dangling_else) would 
> then exist as a “dangling catch” ambiguity, but it should be possible. 


Yes, it would certainly be "purer" that way. I bet coding standards 
would immediately forbid its use with anything other than let blocks though.


-- 
Rowan Tommins
[IMSoP]

---

## #129603 — Larry Garfield — 2025-12-13T15:58:22+00:00

URL: https://externals.io/message/129251#129603 · In reply to: #129512

> We're very open to tweaking the keywords.  I can see the argument for 
> "as" being a little misleading in PHP's case.  Though I'd prefer to 
> have the EXPR first and VAR second, whatever the keyword is.
>
> Another potential we thought of: Just plain =>.  It already doesn't 
> imply equals, would have no keyword breaks, and is still only 2 
> characters.
>
> using (new SomeManager() => $someResource) {
>   // ...
> }
>
> Thoughts?
>
> --Larry Garfield

Rowan (or anyone else), did you have thoughts here?  Would => be a more self-explanatory symbol to use for the context manager block?

--Larry Garfield

---

## #129604 — Rowan Tommins [IMSoP] — 2025-12-13T17:07:43+00:00

URL: https://externals.io/message/129251#129604 · In reply to: #129603

>Rowan (or anyone else), did you have thoughts here?  Would => be a more self-explanatory symbol to use for the context manager block?

Yes, sorry, I must have replied in my head. I think that does make it clearer that one value produces the other, rather than just being assigned or aliased to it.

Although the most common use is key=>value, we also have fn()=>return_expression and get=>property_expression.


Rowan Tommins
[IMSoP]

---

## #129617 — tim@bastelstu.be — 2025-12-16T08:58:05+00:00

URL: https://externals.io/message/129251#129617 · In reply to: #129588

Hi

Am 2025-12-11 22:45, schrieb Rowan Tommins [IMSoP]:
> For little fixes like this, it would probably be most efficient if you 
> raise a PR, or mail me a patch, rather then me hunting around for each 
> one.

Sorry about that. I don't have my personal GitLab account (that I barely 
use these days) set up on my work machine (thus a PR wouldn't work) and 
it didn't occur me to just send a patch file. I've just went across all 
files once more and fixed the obvious syntax errors that VSC pointed out 
to me. I've also added a stub file to satisfy my language server from 
pointing out unknown functions / classes. Patch attached.

I didn't fix the 
db-transaction/application/1c-raii-with-scope-declaration.php comment, 
since that's not a straight-forward fix.

I'll try to work through the rest of your email later, but wanted to get 
this patch out already.

Best regards
Tim Düsterhus

---

## #129643 — tim@bastelstu.be — 2025-12-17T19:59:54+00:00

URL: https://externals.io/message/129251#129643 · In reply to: #129577

Hi

Am 2025-12-10 00:19, schrieb Rowan Tommins [IMSoP]:
> The drawback I see is that on a longer block, you have to come up with 
> a name for that unused variable, and make sure you don't accidentally 
> unset or overwrite it.
> 
> 
> Apparently Java's workaround for that is to allow "unnamed variables", 
> which have the expected lifetime, but can't be accessed: 
> https://openjdk.org/jeps/456
> 
> try-with-resources is given as one of the example use cases ("var" 
> infers the type; "_" in place of the name makes it an "unnamed 
> variable"):
> 
> try (var _ = ScopedContext.acquire()) {
>     ... no use of acquired resource ...
> }
> 
> Notably, this is *not* the same meaning for "_" as, say, C#, where "_ = 
> foo()" tells the compiler to discard the return value of foo(): 
> https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards

Something that came to my mind would be that the semantics of `let()` 
already allow for the following to guarantee that a value stays alive 
for the entire block even if accidentally reassigned:

     class Scoped {
         public function __construct() { echo __METHOD__, PHP_EOL; }
         public function __destruct() { echo __METHOD__, PHP_EOL; }
     }

     echo "Before scope", PHP_EOL;
     let (
         $scoped = new Scoped(),
         $scoped,
     ) {
         echo "Start of scope", PHP_EOL;

         $scoped = 'something else';

         echo "End of scope", PHP_EOL;
     }
     echo "After scope", PHP_EOL;

which outputs:

     Before scope
     Scoped::__construct
     Start of scope
     End of scope
     Scoped::__destruct
     After scope

It is definitely on the more obscure end, but I believe it is reasonably 
easy to understand *why* it works when seeing that it does work. This 
semantics of redeclaring/shadowing variables within the same (not a 
nested!) scope is something that folks might also know from Rust to 
ensure that a temporary input stays alive for long enough without taking 
up a valuable variable name:

     // 
https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=e5c84849b529aab5c1a5033ddfde2870
     fn get_bar() -> String {
         return "bar".to_owned();
     }

     fn id(v: &str) -> &str {
         return v;
     }

     fn main() {
         let bar = id(get_bar().as_str());

         println!("{:?}", bar);
     }

This is not currently valid, as the String returned from `get_bar()` is 
destroyed at the semicolon, but a reference to it is still stored in 
`bar`. The following however would work:

     let bar = get_bar();
     let bar = id(bar.as_str());

     println!("{:?}", bar);

Best regards
Tim Düsterhus

---

## #129646 — Rowan Tommins [IMSoP] — 2025-12-17T23:04:21+00:00

URL: https://externals.io/message/129251#129646 · In reply to: #129643

>    let (
>        $scoped = new Scoped(),
>        $scoped,
>    ) {


My attempts to guess what this would do went something like this: 

1) The second mention of $scoped does nothing, you've already declared it as scoped to this block

2) That seems trivial for the compiler to spot, so probably an Error

3) Maybe it overwrites the variable to null? But that would make the lifetime shorter, not longer

4) So, somehow, there are two variables, with the same name, and they both live until the end of the block?

5) Wait, does that mean this is just a sequence of declaration statements in disguise?


It feels like this completely goes against everything you've been saying about avoiding the ambiguity of ALGOL-style declarations.

If you can do that, presumably you can do this: 

let(
     $foo = bar($baz), // What is $baz referring to? Particularly if it is a by-reference out parameter.
     $baz = 1,
 )

Which is a direct translation of an example you gave here: <https://externals.io/message/129059#129583>

Thinking about it, even the dynamic coding features of PHP you say would be so difficult aren't automatically prohibited: 

let(
    $foo = compact('bar'),
    $bar = extract($foo),
)
     
and so on. 


I kind of hope I'm misunderstanding something here, because this feels like a pretty big hole in the premise.



Rowan Tommins
[IMSoP]

---

## #129647 — tim@bastelstu.be — 2025-12-18T00:17:55+00:00

URL: https://externals.io/message/129251#129647 · In reply to: #129646

Hi


> 5) Wait, does that mean this is just a sequence of declaration statements in disguise?

Yes.

     let ($foo, $bar) { … }

is equivalent to

     let ($foo) {
         let($bar) { … }
     }

And by extension

     let ($scoped, $scoped) { … }

is equivalent to

     let ($scoped) {
         let ($scoped) { … }
     }

In the example the outer `$scoped` will then effectively be shadowed by 
the inner `$scoped`, preventing it from being overwritten inside the block.

The initializer behaves like regular assignments that PHP users are 
already familiar with, just with the extra feature that the old value 
will be backed up and then restored after the associated statement 
(list) finishes.

The (effective) desugaring is showcased in the Proposal section of the 
RFC and the first example in the “Examples” section also showcase all 
possible situations.

I have just updated the RFC to write this out more explicitly:

https://wiki.php.net/rfc/optin_block_scoping?do=diff&rev2%5B0%5D=1765892090&rev2%5B1%5D=1766015568&difftype=sidebyside

> If you can do that, presumably you can do this:
> 
> let(
>       $foo = bar($baz), // What is $baz referring to? Particularly if it is a by-reference out parameter.
>       $baz = 1,
>   )
> 
> Which is a direct translation of an example you gave here: <https://externals.io/message/129059#129583>

The `$baz` in `bar($baz)` is referring to whatever value `$baz` has at 
that point in time.

> Thinking about it, even the dynamic coding features of PHP you say would be so difficult aren't automatically prohibited:

I assume you are referring to this email here: 
https://news-web.php.net/php.internals/129641? I was specifically 
mentioning the dynamic coding features as problematic in combination 
with a possible “temporal dead zone”.

Since the `let()` construct requires all variables to be declared at the 
start of the block in a dedicated section there is no (or less) issue of 
there being multiple equally-valid interpretations for the behavior of 
variables that are declared “halfway through” a block:

1. The “temporal dead zone” is not something that can exist.
2. And users do not need to wonder if declarations are hoisted.

For the example in the email you linked, I am including it here once 
more (with an additional $baz = 2 assignment at the start):

     $baz = 2;
     {
          let $foo = bar($baz);

          let $baz = 1;
      }

1. If there is a temporal dead zone, the call `bar($baz)` is invalid 
(throws an Error).
2. If declarations are hoisted, the call to `bar($baz)` could be (1) an 
access to an undefined variable (if it behaves as if there was an 
`unset($baz)`, which would be behavior that is technically different 
from the TDZ). It could also be a valid access to a variable containing 
`null` (if all variables are initialized to `null`). *Theoretically* it 
could also be `1`, if only constant expressions are legal and the 
initializer is also hoisted.
3. If the lifetime of the block-scoped `$baz` only starts at the point 
of declaration - effectively an invisible nested block - it behaves as 
if it was `bar(2)`, since the current value of `$baz` is `2`.

To me the syntax of the `let()` construct very strongly suggests (3) and 
when there is only one variable declared (or one knows the desugaring of 
let($foo, bar) == let($foo) let($bar)) there is no other possible 
interpretation.

This is what I meant by “there is a less rigid relationship between the 
individual statements” in the previous email. Note that it also said 
“Forcing all the declarations into a single statement would resolve that
ambiguity […]”, since that would be isomorphic to the `let()` construct 
if the declaration is forced to be at the top of the block.

Best regards
Tim Düsterhus

---

## #129649 — Rowan Tommins [IMSoP] — 2025-12-18T09:16:31+00:00

URL: https://externals.io/message/129251#129649 · In reply to: #129647

> Yes.
>
>     let ($foo, $bar) { … }
>
> is equivalent to
>
>     let ($foo) {
>         let($bar) { … }
>     }


Yeah, I guess I didn't realise the implications of that equivalence.

 From the syntax alone, I vaguely assumed that the two declarations 
happened "simultaneously", and didn't think very hard what that meant.


> > […]
>
> The `$baz` in `bar($baz)` is referring to whatever value `$baz` has at 
> that point in time.


As written, that sentence doesn't really say anything; but from the 
context of the discussion, I get what you're trying to say.

The point though is that any answer you give is just a design decision 
you've made; and the exact same decision could be made with more 
traditional syntax, and apply to the original example.


> 1. The “temporal dead zone” is not something that can exist.
> 2. And users do not need to wonder if declarations are hoisted.


This is just plain false. The exact same ambiguity exists, you have just 
chosen how to resolve it.


> For the example in the email you linked, I am including it here once 
> more (with an additional $baz = 2 assignment at the start):
>
>     $baz = 2;
>     {
>          let $foo = bar($baz);
>
>          let $baz = 1;
>      }
>
> 1. If there is a temporal dead zone, the call `bar($baz)` is invalid 
> (throws an Error).
> 2. If declarations are hoisted, the call to `bar($baz)` could be (1) 
> an access to an undefined variable (if it behaves as if there was an 
> `unset($baz)`, which would be behavior that is technically different 
> from the TDZ). It could also be a valid access to a variable 
> containing `null` (if all variables are initialized to `null`). 
> *Theoretically* it could also be `1`, if only constant expressions are 
> legal and the initializer is also hoisted.
> 3. If the lifetime of the block-scoped `$baz` only starts at the point 
> of declaration - effectively an invisible nested block - it behaves as 
> if it was `bar(2)`, since the current value of `$baz` is `2`.


Agreed.

It's probably worth calling out that (1) is effectively a subset of (2) 
designed to avoid the confusion that full hoisting causes. There's also 
a variant of (3) where variable shadowing is forbidden, so the "let $baz 
= 1" would throw an Error.

And all of those options are available to a comma-separated version as well.


> To me the syntax of the `let()` construct very strongly suggests (3)


I don't really see why. As I said above, the comma-separated list made 
me think of "simultaneous" action, which I think would imply (1), 
because accessing a variable "while it's being declared" would make no 
sense.

Option 3 is not necessarily a "wrong" choice, but it's a choice you have 
made, and you could equally use more traditional syntax and make that 
same choice.


> This is what I meant by “there is a less rigid relationship between 
> the individual statements” in the previous email. Note that it also 
> said “Forcing all the declarations into a single statement would 
> resolve that
> ambiguity […]”, since that would be isomorphic to the `let()` 
> construct if the declaration is forced to be at the top of the block.


I don't think separating the declarations with "," vs ";" makes any 
difference. The only way to fully avoid the ambiguity is to limit the 
initialisers to constant expressions. As soon as you allow "let $foo = 
$bar" and "let $bar", with whatever punctuation you choose, there is a 
chance for ambiguity about how to resolve "$bar".

The only thing that using commas automatically does is forbids jump 
targets (goto labels or switch cases) in between the declarations.



>
> And with that I'm *really* going to be off for vacation. 


Have a great break :)


-- 
Rowan Tommins
[IMSoP]

---

## #129758 — tim@bastelstu.be — 2026-01-15T09:03:36+00:00

URL: https://externals.io/message/129251#129758 · In reply to: #129649

Hi


> The point though is that any answer you give is just a design decision
> you've made; and the exact same decision could be made with more
> traditional syntax, and apply to the original example.

That is true from a purely technical perspective, since syntax is 
independent from semantics. But the choice of syntax influences users' 
expectations with regard to semantics and as I've outlined, we believe 
that the semantics we've chosen fits PHP best and that the syntax we've 
chosen hints at the semantics best.

> > […]
> 
> 
> I don't really see why. As I said above, the comma-separated list made
> me think of "simultaneous" action, which I think would imply (1),
> because accessing a variable "while it's being declared" would make no
> sense.

Part of it probably is probably a subconscious feeling, but I'll try 
with an explanation that you may or may not be able to agree with.

Existing constructs in PHP that take a list of multiple “items” behave 
equivalent to consecutive single-item entries. Or in less abstract 
terms:

     const FOO = 1, BAR = 2;

is equivalent to:

     const FOO = 1;
     const BAR = 2;

So to me it is only natural to extend that logic to the `let()` 
construct, which only leaves the let($a) let ($b) desugaring which then 
implies (3).

In fact, the following works:

     const FOO = 1, BAR = FOO + 1;
     var_dump(FOO, BAR);

and defines `BAR = 2` and

     const FOO = BAR + 1, BAR = 1;

throws, because `BAR` is undefined when defining `FOO`. This is exactly 
matching the proposed semantics of `let()`.

> Option 3 is not necessarily a "wrong" choice, but it's a choice you 
> have
> made, and you could equally use more traditional syntax and make that
> same choice.

Yes, but the “more traditional syntax” would also leave the possibility 
of (1) or (2) - unless restricted to the start of the block as in C90. 
However it would compose less well with the existing control structures, 
e.g. by requiring the dedicated syntax for declarations that should live 
for a single `if()` construct.

> The only thing that using commas automatically does is forbids jump
> targets (goto labels or switch cases) in between the declarations.

It also forbids nested blocks and any other “arbitrarily complex code” 
in-between declarations. Yes, I understand that folks can use arbitrary 
expressions on the right side of the declaration (including 
assignments), but that is more unlikely compared to a more “free-form” 
syntax that allows arbitrary statements, including arbitrary control 
flow.

I also believe that the explicit separation of declaration and usage 
provides for a nice visual barrier of "before declaration, definitely 
old variable", "after declaration, definitely new variable" and 
"declaration phase, a small chance of ambiguity when declaring a 
variable that is used in the declaration of an earlier one".

Best regards
Tim Düsterhus

---

## #129798 — Rowan Tommins [IMSoP] — 2026-01-20T21:41:23+00:00

URL: https://externals.io/message/129251#129798 · In reply to: #129758

> Existing constructs in PHP that take a list of multiple “items” behave 
> equivalent to consecutive single-item entries. Or in less abstract terms:
>
>     const FOO = 1, BAR = 2;
>
> is equivalent to:
>
>     const FOO = 1;
>     const BAR = 2;
>
> So to me it is only natural to extend that logic to the `let()` 
> construct, which only leaves the let($a) let ($b) desugaring which 
> then implies (3).


There's a jump here. In a grouped "const", "global", or "static" 
statement, the expansion is to a *series of statements* with a common 
prefix. In your proposed syntax, the expansion is to a *nested set of 
blocks*.

That makes the semantics more complicated, and in my opinion more 
surprising...


Rather than "const", a better example is probably "static", where this 
is legal:

```
$bar = 42;
static $foo=$bar + 1, $bar = 1;
echo "$foo,$bar";
```

It desugars to a series of separate statements:

```
$bar = 42;
static $foo=$bar + 1;
static $bar = 1;
echo "$foo,$bar";
```

The static $bar silently shadows the local one, and you get an output of 
43,1


Interestingly, this is *not* legal PHP:

```
static $foo=$bar + 1, $bar = 1, $foo = -1;
```

"Fatal error: Duplicate declaration of static variable $foo"


Note that these are two largely arbitrary design decisions:

- A static variable with the same name as a local one shadows the 
previous variable, with no hoisting or dead zone
- A static variable with the same name as an existing static variable is 
forbidden



> This is exactly matching the proposed semantics of `let()`.


Not quite.

It works for this:

```
$bar = 42;
let($foo=$bar + 1, $bar = 1) {
     echo "$foo,$bar";
}
```

The block-scoped $bar shadows the function-local $bar, and the output is 
43,1 just like with "static".


But what if we declare the same variable twice:

```
$bar = 42;
let($foo=$bar + 1, $bar = 1, $foo=-1) {
     echo "$foo,$bar";
}
```

My intuition from "static", and from every other language I've used, is 
that this would be an Error: there's a single block, and you're trying 
to declare two different local variables called "$foo" inside that block.

Instead, what happens is that the second declaration of $foo shadows the 
first.


The justification is that this is not actually one block at all, it's 
secretly a whole set of nested blocks:

```
$bar = 42;
{
     let($foo=$bar + 1) {
         let($bar = 1) {
             let($foo=-1) {
                 echo "$foo,$bar";
            }
        }
    }
}
```


> Yes, but the “more traditional syntax” would also leave the 
> possibility of (1) or (2) - unless restricted to the start of the 
> block as in C90.


I suspect this is where we've been talking past each other a bit: 
looking at the syntax, I interpreted the comma-separated list as 
defining a set of variables in the same scope.

That leaves all the same options as the "traditional" syntax:

$bar = 42;
let($foo=$bar+1, $bar=1) {
     echo $foo;
}

- You could treat $bar as "hoisted" within the let() construct, giving 
$foo==2
- You could treat $bar as creating a Dead Zone within the let() 
construct, giving an Error
- You could treat the declarations as sequential, and shadow the value, 
giving $foo==43


What I completely missed, and seemed obvious to you, is that there are 
actually two separate scopes being declared, nested inside each other.

(Even so, there is another possibility: variable shadowing could be 
forbidden, as in C#, making the "$bar=1" declaration an error).


So I guess it all comes down to the same thing: the syntax just doesn't 
seem intuitive to me, because I keep expecting it to behave like JS, C, 
Java, Perl, etc.

I can't get past the fact that copying from one of those languages would 
be so much more obvious.


-- 
Rowan Tommins
[IMSoP]
