17809, "dlongnecke-cray", "Should we introduce new syntax for context managers?", "2021-05-24T04:48:23Z"
Before we can discuss what the various pieces of syntax for a context
manager could/should look like, I think it's important to ask the
question of whether or not we should bother introducing new syntax
at all.
As far as I know, Python is the only language to introduce explicit
support for context managers, using syntax that looks like:
# Open a file and use it within the scope of a managed block. The file
# will be automatically closed when the block ends.
txt = 'foo'
with open('MyFile.txt', 'w') as myFile:
myFile.write(txt)
An immediate translation of this syntax to Chapel might look like:
var txt = 'foo';
with open('MyFile.txt', 'w') as ref myFile do
myFile.write(txt);
In most other languages that I have looked at, similar support for
"context management" can be achieved without additional syntax by
making use of closures.
If we had more advanced support for closures today, we could imagine
that that the imaginary function openWith
might accept a closure:
var txt = 'foo';
// Here the function 'openWith' accepts a file path, mode, and closure.
// It opens the file, executes the closure, and then closes the file.
openWith('MyFile.txt', 'w', lambda(ref f) throws {
f.write(txt);
});
It may even be possible to construct a more procedural variant of this
closure scheme so that we do not need to tie it to concrete methods
such as openWith
.
Let us imagine that participating types must define enterThis()
and
exitThis()
. The enterThis()
method may return a resource, or
none
if there is no resource to be managed. We could even codify
this expectation via an interface called "Managed" (though the idea
would work even if we left things generic):
// Manage a resource by reference.
interface ManagedRef(type Resource) {
proc enterThis(self) ref: Resource throws;
proc leaveThis(e: owned Error?): bool throws;
}
// Manage a resource by const reference.
interface ManagedConstRef(type Resource) {
proc enterThis(self) const ref: Resource throws;
proc leaveThis(e: owned Error?): bool throws;
}
// Manage a resource by value.
interface ManagedVar(type Resource) {
proc enterThis(self): Resource throws;
proc leaveThis(e: owned Error?): bool throws;
}
We could then define a function called manage
that accepts a context
object to manage, and a closure.
Its implementation might look like:
proc manage(manager: ?T, block: func(ref T) throws
where T implements ManagedRef {
ref resource = manager.enterThis();
try {
block(resource);
} catch e {
const doNotRethrow = manager.leaveThis(e);
if !doNotRethrow then
throw e;
}
}
And an implementation of the ManagedRef might be:
record myManager {
var x = 0;
}
// This syntax is probably not what we actually use for interfaces.
// The intention is what matters...
implement ManagedRef(type Resource) for myManager {
proc enterThis(self) ref: Resource throws {
writeln('entering');
return this;
}
proc leaveThis(e: owned Error?) {
writeln('leaving');
return false;
}
}
And a use might be:
var myManager = new r();
writeln(r);
try! manage(myManager, lambda(ref x: r) {
r.x += 1;
});
writeln(r);
The implementation of manage()
looks almost exactly like what the
compiler would generate for the context manager statement under the
covers.
By using closures, we can avoid introducing new syntax to the language
and still end up with a very similar looking construct.
I've decided to use interfaces to explain the general idea, but they
are not necessary to achieve a variant of this construct today. All that
is necessary would be to add support for closures. Since closures are
a feature that we're intending to add anyway, it seems like it would
be good to ask the question:
Do we want to add syntax for context managers at all?