Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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) { ... }
  • name is bound to scope.enter(), not to scope itself. A Session can hand out itself; a Weak<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. An enter() 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, while and for. The braces are required.
  • name cannot 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, so exit() 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.