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]
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: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]