Scoped blocks with with
A with block enters a scope object, binds what it hands out, and cleans up
when the object is destroyed. It plays the role of Python’s with and C#’s
using, but the cleanup is ordinary destruction: it runs through the same
reference counting as every other object (see
Memory and reference counting).
import async;
# A module-scope counter: const, and changed through an Atomic.
const open_sessions = async.Atomic<int>(0);
# A scope object: enter() hands out what the block works with, and the
# destructor calls exit() when the last reference goes away.
struct Session {
var user: String;
construct(this.user = "") {
open_sessions.fetch_add(1);
}
destruct {
this.exit();
}
Session enter() {
return this;
}
exit() {
open_sessions.fetch_sub(1);
}
}
extend Session with Scoped<Session>;
# enter() may return a null reference: then the body is skipped and the
# `else` block runs.
struct Lookup {
var found: Session;
construct(Session session) {
found = session;
}
Session enter() {
return found;
}
exit() {}
}
extend Lookup with Scoped<Session>;
first_user(): String {
with (var session = Session("ada")) {
# `return` leaves the block too: the session still closes.
return session.user;
}
return "";
}
main(): int {
with (var session = Session("grace")) {
assert(open_sessions.load() == 1, "open inside the block");
assert(session.user == "grace", "bound to what enter() returned");
}
assert(open_sessions.load() == 0, "closed at the end of the block");
assert(first_user() == "ada", "the value was returned");
assert(open_sessions.load() == 0, "closed on return as well");
var hits = 0;
with (var s = Lookup(Session("linus"))) {
hits += 1;
} else {
hits -= 100;
}
with (var s = Lookup(__null)) {
hits -= 100;
} else {
hits += 1;
}
assert(hits == 2, "the body ran for a session, the else block for null");
return 0;
}
The header
with (var name = scope) { ... } else { ... }
with (Type name = scope) { ... }
with (scope) { ... }
nameis bound toscope.enter(), not toscopeitself. ASessioncan hand out itself; aWeak<T>hands out a strong reference to the object it points at, or null once that object is gone.- Without a binding, the block still holds the result of
enter()until it ends. Anenter()that returns nothing (declared without a return type) has nothing to bind, and binding it is an error. - The parentheses are optional, as with
if,whileandfor. The braces are required. namecannot be reassigned inside the block.
Skipping the body
When enter() returns a struct reference that is null, the body is skipped
and the optional else block runs instead. else is only allowed when
enter() returns a struct type, since nothing else can be null.
The Scoped<T> protocol
A type works in a with block when it has the methods of the Scoped<T>
trait (runtime/scoped.pep, documented in the Scoped
reference):
export trait Scoped<T> {
enter(): T; # what the header binds; null skips the body
exit(); # the cleanup; call it from the destructor
}
A with block goes by the methods a type has, not by the trait: a type
without enter() is a compile error in a with header. Declaring the trait
(extend Session with Scoped<Session>;) documents the intent, and the
compiler then checks that the type has both enter() and exit().
When exit() runs
The block keeps two locals: the scope object and the bound value. Both are
released on every path out of the block (the end of the body, return,
break and continue), the bound value first. Implementations call exit()
from their destructor:
destruct {
this.exit();
}
so exit() runs when the scope object’s last reference is released:
- A scope object created in the header (
with (var s = Session("grace"))) is released by the block, soexit()runs at the end of the block. - A scope object that something else also holds (a field, another local) lives
on after the block, and
exit()waits for its last owner. This is the same rule as any other object. - A copy of the bound value that outlives the block (stored in a field, returned) keeps that value alive, but not the scope object.
A panic stops the program without running destructors, so exit() does not
run then.