On Sat, Nov 15, 2025, at 5:11 PM, Rowan Tommins [IMSoP] wrote:
> 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.

Reply via email to