Re: Examples comparing Block Scoped RAII and Context Managers
| From: | Larry Garfield | Date: | Tue, 18 Nov 2025 17:23:35 +0000 |
| Subject: | Re: Examples comparing Block Scoped RAII and Context Managers | ||
| References: | 1 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-129288@lists.php.net to get a copy of this message | ||
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.