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]
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']);
}
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;
}
For a transaction it might look like this. Rather auto-capture I'd
suggest explicit complete scope capture, by using the use keyword
without parenthesis.
class Transaction implements TryWithContext {
public function tryWith(): \Closure
{
$this->db->beginTransaction();
return function (?\Throwable $e = null) use {
if ($e) {
$this->db->rollbackTransaction();
return;
}
$this->db->commitTransaction();
};
}
}